如何在Python中声明常量?

在Java中,我们做:

public static final String CONST_NAME = "Name";

当前回答

我最近发现了一个非常简洁的更新,它会自动引发有意义的错误消息,并阻止通过__dict__访问:

class CONST(object):
    __slots__ = ()
    FOO = 1234

CONST = CONST()

# ----------

print(CONST.FOO)    # 1234

CONST.FOO = 4321              # AttributeError: 'CONST' object attribute 'FOO' is read-only
CONST.__dict__['FOO'] = 4321  # AttributeError: 'CONST' object has no attribute '__dict__'
CONST.BAR = 5678              # AttributeError: 'CONST' object has no attribute 'BAR'

我们将自己定义为一个实例,然后使用插槽来确保不能添加其他属性。这也删除了__dict__访问路由。当然,整个对象仍然可以重新定义。

编辑-原始解决方案

我可能忽略了一个技巧,但这似乎对我有用:

class CONST(object):
    FOO = 1234

    def __setattr__(self, *_):
        pass

CONST = CONST()

#----------

print CONST.FOO    # 1234

CONST.FOO = 4321
CONST.BAR = 5678

print CONST.FOO    # Still 1234!
print CONST.BAR    # Oops AttributeError

创建实例允许神奇的__setattr__方法介入并拦截设置FOO变量的尝试。如果您愿意,可以在这里抛出异常。通过类名实例化实例可以防止直接通过类进行访问。

对于一个值来说,这非常麻烦,但是您可以将许多值附加到CONST对象上。有一个上层的类,类名似乎也有点难看,但我认为它总体上是相当简洁的。

其他回答

您可以使用namedtuple作为一种变通方法来有效地创建一个常量,其工作方式与Java中的静态final变量(Java“常量”)相同。作为一种变通方法,它是优雅的。(更优雅的方法是简单地改进Python语言——什么样的语言可以让您重新定义math.pi?——但我跑题了。)

(当我写这篇文章时,我意识到这个问题的另一个答案提到了namedtuple,但我将在这里继续,因为我将展示一种更接近于您在Java中所期望的语法,因为不需要像namedtuple那样创建命名类型。)

跟着你的例子,你会记得在Java中,我们必须在某个类中定义常量;因为你没有提到类名,我们称它为Foo。下面是Java类:

public class Foo {
  public static final String CONST_NAME = "Name";
}

这里是等效的Python。

from collections import namedtuple
Foo = namedtuple('_Foo', 'CONST_NAME')('Name')

我想在这里补充的关键点是,您不需要单独的Foo类型(“匿名命名元组”会很好,尽管这听起来有点矛盾),所以我们将命名元组命名为_Foo,这样它就不会转义到导入模块中。

这里的第二点是,我们立即创建了nametuple的一个实例,称其为Foo;没有必要在单独的步骤中执行此操作(除非您想这样做)。现在你可以做你在Java中可以做的事情:

>>> Foo.CONST_NAME
'Name'

但你不能给它赋值:

>>> Foo.CONST_NAME = 'bar'
…
AttributeError: can't set attribute

确认:我认为我发明了命名元组方法,但后来我看到其他人给出了类似的答案(尽管不那么紧凑)。然后我还注意到Python中的“命名元组”是什么?,这就指出了sys。version_info现在是一个命名元组,所以可能Python标准库早就提出了这个想法。

注意,不幸的是(这仍然是Python),你可以完全删除整个Foo赋值:

>>> Foo = 'bar'

(facepalm)

但至少我们阻止了福星。CONST_NAME值,这比什么都没有好。祝你好运。

不幸的是,Python还没有常数,这是耻辱。ES6已经为JavaScript添加了支持常量(https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/const),因为它在任何编程语言中都非常有用。 正如在Python社区的其他回答中所回答的那样,使用约定的用户大写变量作为常量,但它不能防止代码中的任意错误。 如果您愿意,您可能会发现一个有用的单文件解决方案 (参见文档字符串如何使用它)。

文件constants.py

import collections


__all__ = ('const', )


class Constant(object):
    """
    Implementation strict constants in Python 3.

    A constant can be set up, but can not be changed or deleted.
    Value of constant may any immutable type, as well as list or set.
    Besides if value of a constant is list or set, it will be converted in an immutable type as next:
        list -> tuple
        set -> frozenset
    Dict as value of a constant has no support.

    >>> const = Constant()
    >>> del const.temp
    Traceback (most recent call last):
    NameError: name 'temp' is not defined
    >>> const.temp = 1
    >>> const.temp = 88
    Traceback (most recent call last):
        ...
    TypeError: Constanst can not be changed
    >>> del const.temp
    Traceback (most recent call last):
        ...
    TypeError: Constanst can not be deleted
    >>> const.I = ['a', 1, 1.2]
    >>> print(const.I)
    ('a', 1, 1.2)
    >>> const.F = {1.2}
    >>> print(const.F)
    frozenset([1.2])
    >>> const.D = dict()
    Traceback (most recent call last):
        ...
    TypeError: dict can not be used as constant
    >>> del const.UNDEFINED
    Traceback (most recent call last):
        ...
    NameError: name 'UNDEFINED' is not defined
    >>> const()
    {'I': ('a', 1, 1.2), 'temp': 1, 'F': frozenset([1.2])}
    """

    def __setattr__(self, name, value):
        """Declaration a constant with value. If mutable - it will be converted to immutable, if possible.
        If the constant already exists, then made prevent againt change it."""

        if name in self.__dict__:
            raise TypeError('Constanst can not be changed')

        if not isinstance(value, collections.Hashable):
            if isinstance(value, list):
                value = tuple(value)
            elif isinstance(value, set):
                value = frozenset(value)
            elif isinstance(value, dict):
                raise TypeError('dict can not be used as constant')
            else:
                raise ValueError('Muttable or custom type is not supported')
        self.__dict__[name] = value

    def __delattr__(self, name):
        """Deny against deleting a declared constant."""

        if name in self.__dict__:
            raise TypeError('Constanst can not be deleted')
        raise NameError("name '%s' is not defined" % name)

    def __call__(self):
        """Return all constans."""

        return self.__dict__


const = Constant()


if __name__ == '__main__':
    import doctest
    doctest.testmod()

如果这还不够,请参阅完整的测试用例。

import decimal
import uuid
import datetime
import unittest

from ..constants import Constant


class TestConstant(unittest.TestCase):
    """
    Test for implementation constants in the Python
    """

    def setUp(self):

        self.const = Constant()

    def tearDown(self):

        del self.const

    def test_create_constant_with_different_variants_of_name(self):

        self.const.CONSTANT = 1
        self.assertEqual(self.const.CONSTANT, 1)
        self.const.Constant = 2
        self.assertEqual(self.const.Constant, 2)
        self.const.ConStAnT = 3
        self.assertEqual(self.const.ConStAnT, 3)
        self.const.constant = 4
        self.assertEqual(self.const.constant, 4)
        self.const.co_ns_ta_nt = 5
        self.assertEqual(self.const.co_ns_ta_nt, 5)
        self.const.constant1111 = 6
        self.assertEqual(self.const.constant1111, 6)

    def test_create_and_change_integer_constant(self):

        self.const.INT = 1234
        self.assertEqual(self.const.INT, 1234)
        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.INT = .211

    def test_create_and_change_float_constant(self):

        self.const.FLOAT = .1234
        self.assertEqual(self.const.FLOAT, .1234)
        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.FLOAT = .211

    def test_create_and_change_list_constant_but_saved_as_tuple(self):

        self.const.LIST = [1, .2, None, True, datetime.date.today(), [], {}]
        self.assertEqual(self.const.LIST, (1, .2, None, True, datetime.date.today(), [], {}))

        self.assertTrue(isinstance(self.const.LIST, tuple))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.LIST = .211

    def test_create_and_change_none_constant(self):

        self.const.NONE = None
        self.assertEqual(self.const.NONE, None)
        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.NONE = .211

    def test_create_and_change_boolean_constant(self):

        self.const.BOOLEAN = True
        self.assertEqual(self.const.BOOLEAN, True)
        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.BOOLEAN = False

    def test_create_and_change_string_constant(self):

        self.const.STRING = "Text"
        self.assertEqual(self.const.STRING, "Text")

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.STRING += '...'

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.STRING = 'TEst1'

    def test_create_dict_constant(self):

        with self.assertRaisesRegexp(TypeError, 'dict can not be used as constant'):
            self.const.DICT = {}

    def test_create_and_change_tuple_constant(self):

        self.const.TUPLE = (1, .2, None, True, datetime.date.today(), [], {})
        self.assertEqual(self.const.TUPLE, (1, .2, None, True, datetime.date.today(), [], {}))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.TUPLE = 'TEst1'

    def test_create_and_change_set_constant(self):

        self.const.SET = {1, .2, None, True, datetime.date.today()}
        self.assertEqual(self.const.SET, {1, .2, None, True, datetime.date.today()})

        self.assertTrue(isinstance(self.const.SET, frozenset))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.SET = 3212

    def test_create_and_change_frozenset_constant(self):

        self.const.FROZENSET = frozenset({1, .2, None, True, datetime.date.today()})
        self.assertEqual(self.const.FROZENSET, frozenset({1, .2, None, True, datetime.date.today()}))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.FROZENSET = True

    def test_create_and_change_date_constant(self):

        self.const.DATE = datetime.date(1111, 11, 11)
        self.assertEqual(self.const.DATE, datetime.date(1111, 11, 11))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.DATE = True

    def test_create_and_change_datetime_constant(self):

        self.const.DATETIME = datetime.datetime(2000, 10, 10, 10, 10)
        self.assertEqual(self.const.DATETIME, datetime.datetime(2000, 10, 10, 10, 10))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.DATETIME = None

    def test_create_and_change_decimal_constant(self):

        self.const.DECIMAL = decimal.Decimal(13123.12312312321)
        self.assertEqual(self.const.DECIMAL, decimal.Decimal(13123.12312312321))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.DECIMAL = None

    def test_create_and_change_timedelta_constant(self):

        self.const.TIMEDELTA = datetime.timedelta(days=45)
        self.assertEqual(self.const.TIMEDELTA, datetime.timedelta(days=45))

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.TIMEDELTA = 1

    def test_create_and_change_uuid_constant(self):

        value = uuid.uuid4()
        self.const.UUID = value
        self.assertEqual(self.const.UUID, value)

        with self.assertRaisesRegexp(TypeError, 'Constanst can not be changed'):
            self.const.UUID = []

    def test_try_delete_defined_const(self):

        self.const.VERSION = '0.0.1'
        with self.assertRaisesRegexp(TypeError, 'Constanst can not be deleted'):
            del self.const.VERSION

    def test_try_delete_undefined_const(self):

        with self.assertRaisesRegexp(NameError, "name 'UNDEFINED' is not defined"):
            del self.const.UNDEFINED

    def test_get_all_defined_constants(self):

        self.assertDictEqual(self.const(), {})

        self.const.A = 1
        self.assertDictEqual(self.const(), {'A': 1})

        self.const.B = "Text"
        self.assertDictEqual(self.const(), {'A': 1, 'B': "Text"})

优点: 1. 访问整个项目的所有常量 2. 严格控制常数值

缺乏: 1. 不支持自定义类型和'dict'类型

注:

使用Python3.4和Python3.5进行测试(我使用“tox”进行测试) 测试环境:

.

$ uname -a
Linux wlysenko-Aspire 3.13.0-37-generic #64-Ubuntu SMP Mon Sep 22 21:28:38 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

所有给出的答案基本上有两种类型:

创建一些你可以实现的对象 创建一旦定义就不能更改的属性。 使用约定(比如常量全部大写,或者对于Python 3.8,使用最后一个限定符来表示一个或多个名称是常量。

他们可以总结为“你不能用Python做你想做的事情”。

然而,实际上有一种方法可以创建具有真正常量的模块。这样做的代码相当复杂,我将只给出需要做什么,因为它在开源许可下已经可用。

使用导入钩子来创建自定义模块。这里可以找到我为此使用的通用代码。 创建一个特殊的字典,允许只添加一次符合您所选模式的项(例如,名称全部大写),并防止此类名称的值被更改。为此,你需要定义自己的方法,如__setitem__, __delitem__等。这种字典的代码(比如在这个文件中找到的,超过250行)大约有100行长。 普通Python模块的dict不能被修改。因此,在创建模块时,您需要首先执行特殊字典中的代码,然后使用其内容更新模块的字典。 为了防止从模块外部修改常量的值(即monkeypatching),您可以用重新定义的__setattr__和__delattr__方法将模块的__class__替换为自定义的__class__。

关于这个示例的文档可以在这里找到。它可能应该更新,以反映这个问题的答案的数量。

在Python中,人们使用命名约定,例如私有方法使用__method,受保护方法使用_method。

所以用同样的方式,你可以简单地将常量声明为全大写,例如:

MY_CONSTANT = "one"

如果你想让这个常量永远不变,你可以挂钩到属性访问并做一些技巧,但更简单的方法是声明一个函数:

def MY_CONSTANT():
    return "one"

唯一的问题是,在任何地方都必须执行MY_CONSTANT(),但MY_CONSTANT = "one"在Python中是正确的方式(通常)。


你也可以使用namedtuple()来创建常量:

>>> from collections import namedtuple
>>> Constants = namedtuple('Constants', ['pi', 'e'])
>>> constants = Constants(3.14, 2.718)
>>> constants.pi
3.14
>>> constants.pi = 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

我忍不住要提供我自己的极简元类实现(这可能是前面元类答案的变体)。

常量存储在容器类中(不需要实例化)。值只能设置一次,但设置后不能更改(或删除)。

就我个人而言,我目前还没有这个用例,但这是一个有趣的练习。

class MetaConstant(type):
    ''' Metaclass that allows underlying class to store constants at class-level (subclass instance not needed).
        Non-existent attributes of underlying class (constants) can be set initially, but cannot be changed or deleted.
    '''

    def __setattr__(klass, attr, value):
        'If attribute (constant) doesn\'t exist, set value. If attribute exists, raise AttributeError.'
        if hasattr(klass, attr):
            raise AttributeError(f'Can\'t change the value of the constant {klass.__name__}.{attr} to {value}'
                                 f' (the value of {klass.__name__}.{attr} is already set to'
                                 f' {getattr(klass, attr)}).')
        super().__setattr__(attr, value)

    def __delattr__(klass, attr):
        if hasattr(klass, attr):
            raise AttributeError(f'Can\'t delete constant {klass.__name__}.{attr}'
                                 f' (set to {getattr(klass, attr)}).')


class Constants(metaclass=MetaConstant):
    'Container class for constants. No instantiation required.'
    #pass               # uncomment if no constants set upon class creation
    B = 'Six'           # sets Constants.B to 'Six'


Constants.B = 6         # AttributeError
del Constants.B         # AttributeError

Constants.A = 'Five'    # sets Constants.A to 'Five'
Constants.A = 5         # AttributeError
del Constants.A         # AttributeError

请随意提出改进建议。