如何在Python中声明常量?

在Java中,我们做:

public static final String CONST_NAME = "Name";

当前回答

(这一段本来是对那些提到namedtuple的答案的注释,但它太长了,不适合注释,所以,就这样吧。)

上面提到的命名元组方法绝对是创新的。不过,为了完整起见,在其官方文档的NamedTuple部分的末尾,它如下所示:

枚举常量可以用命名元组实现,但使用简单的类声明更简单高效: 类的状态: 打开,待处理,关闭= range(3)

换句话说,官方文档更倾向于使用一种实用的方式,而不是实际实现只读行为。我想这是Python的另一个禅宗的例子:

简单比复杂好。 实用胜过纯粹。

其他回答

在其他语言中没有const关键字,但是可以创建一个具有“getter函数”来读取数据,但没有“setter函数”来重写数据的Property。这从本质上保护标识符不被更改。

下面是一个使用class属性的替代实现:

请注意,对于想了解常量的读者来说,代码远非简单。见下面的解释。

def constant(f):
    def fset(self, value):
        raise TypeError
    def fget(self):
        return f()
    return property(fget, fset)

class _Const(object):
    @constant
    def FOO():
        return 0xBAADFACE
    @constant
    def BAR():
        return 0xDEADBEEF

CONST = _Const()

print(hex(CONST.FOO))  # -> '0xbaadfaceL'

CONST.FOO = 0
##Traceback (most recent call last):
##  File "example1.py", line 22, in <module>
##    CONST.FOO = 0
##  File "example1.py", line 5, in fset
##    raise TypeError
##TypeError

代码的解释:

定义一个接受表达式的函数常量,并使用它来构造一个“getter”——一个仅返回表达式值的函数。 setter函数引发TypeError,因此它是只读的 使用我们刚刚创建的常量函数作为装饰来快速定义只读属性。


用另一种更传统的方式:

(代码相当棘手,下面有更多解释)

class _Const(object):
    def FOO():
        def fset(self, value):
            raise TypeError
        def fget(self):
            return 0xBAADFACE
        return property(**locals())
    FOO = FOO()  # Define property.

CONST = _Const()

print(hex(CONST.FOO))  # -> '0xbaadfaceL'

CONST.FOO = 0
##Traceback (most recent call last):
##  File "example2.py", line 16, in <module>
##    CONST.FOO = 0
##  File "example2.py", line 6, in fset
##    raise TypeError
##TypeError

要定义标识符FOO,首先定义两个函数(fset, fget -名称由我选择)。 然后使用内置的属性函数构造一个可以“set”或“get”的对象。 注意属性函数的前两个参数名为fset和fget。 利用我们为自己的getter和setter选择这些名称的事实,并使用应用于该作用域的所有本地定义的**(双星号)创建一个关键字字典,将参数传递给属性函数

不幸的是,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中,不能将变量或值声明为常量。


为了让程序员知道变量是常量,通常用大写:

CONST_NAME = "Name"

要在常量发生变化时引发异常,请参阅Alex Martelli的《Python中的常量》。注意,这在实践中并不常用。


从Python 3.8开始,有一个类型。最后一个变量注释,它将告诉静态类型检查器(如myypy)您的变量不应该被重新分配。这是最接近于Java的final。然而,它实际上并不能阻止重新分配:

from typing import Final

a: Final[int] = 1

# Executes fine, but mypy will report an error if you run mypy on this:
a = 2

我知道这是一个老问题,但由于新的解决方案仍在添加,我想使可能的解决方案列表更加完整。你可以通过从类中继承属性来实现实例中的常量,如下所示:

class ConstantError(Exception):
    pass  # maybe give nice error message

class AllowConstants:
    _constants = None
    _class_constants = None

    def __init__(self):
        self._constants = {}
        if self._class_constants is not None:
            self._constants.update(self._class_constants)

    def constant(self, name, value):
        assert isinstance(name, str)
        assert self._constants is not None, "AllowConstants was not initialized"
        if name in self._constants or name in self.__dict__:
            raise ConstantError(name)
        self._constants[name] = value

    def __getattr__(self, attr):
        if attr in self._constants:
            return self._constants[attr]
        raise AttributeError(attr)

    def __setattr__(self, attr, val):
        if self._constants is None:
            # not finished initialization
            self.__dict__[attr] = val
        else:
            if attr in self._constants:
                raise ConstantError(attr)
            else:
                self.__dict__[attr] = val

    def __dir__(self):
        return super().__dir__() + list(self._constants.keys())

子类化this时,你创建的常量将受到保护:

class Example(AllowConstants):
    def __init__(self, a, b):
        super().__init__()
        self.constant("b", b)
        self.a = a

    def try_a(self, value):
        self.a = value

    def try_b(self, value):
        self.b = value

    def __str__(self):
        return str({"a": self.a, "b": self.b})

    def __repr__(self):
        return self.__str__()


example = Example(1, 2)
print(example)  # {'a': 1, 'b': 2}

example.try_a(5)
print(example)  # {'a': 5, 'b': 2}

example.try_b(6)  # ConstantError: b

example.a = 7
print(example)  # {'a': 7, 'b': 2}

example.b = 8  # ConstantError: b

print(hasattr(example, "b"))  # True

#  To show that constants really do immediately become constant: 

class AnotherExample(AllowConstants):
    def __init__(self):
        super().__init__()
        self.constant("a", 2)
        print(self.a)
        self.a=3


AnotherExample()  # 2  ConstantError: a


# finally, for class constants:
class YetAnotherExample(Example):
    _class_constants = {
        'BLA': 3
    }

    def __init__(self, a, b):
        super().__init__(a,b)

    def try_BLA(self, value):
        self.BLA = value

ex3 = YetAnotherExample(10, 20)
ex3.BLA  # 3
ex3.try_BLA(10)  # ConstantError: BLA
ex3.BLA = 4  # ConstantError: BLA

常量是局部的(从AllowConstants继承的类的每个实例都有自己的常量),只要它们没有被重新赋值,就像普通的属性一样,并且编写从这个继承的类允许或多或少与支持常量的语言相同的风格。

此外,如果您想通过直接访问实例来防止任何人更改值。_constants,您可以使用其他答案中建议的许多不允许这样做的容器之一。最后,如果你真的觉得有必要,你可以阻止人们设置所有的实例。通过AllowConstants的更多属性访问,将_constants赋给一个新字典。(当然,这些都不是非常python化的,但这不是重点)。

编辑(因为使python非python化是一个有趣的游戏):为了使继承更容易一点,你可以修改AllowConstants如下:

class AllowConstants:
    _constants = None
    _class_constants = None

    def __init__(self):
        self._constants = {}
        self._update_class_constants()

    def __init_subclass__(cls):
        """
        Without this, it is necessary to set _class_constants in any subclass of any class that has class constants
        """
        if cls._class_constants is not None:
            #prevent trouble where _class_constants is not overwritten
            possible_cases = cls.__mro__[1:-1] #0 will have cls and -1 will have object
            for case in possible_cases:
                if cls._class_constants is case._class_constants:
                    cls._class_constants = None
                    break

    def _update_class_constants(self):
        """
        Help with the inheritance of class constants
        """
        for superclass in self.__class__.__mro__:
            if hasattr(superclass, "_class_constants"):
                sccc = superclass._class_constants
                if sccc is not None:
                    for key in sccc:
                        if key in self._constants:
                            raise ConstantError(key)
                    self._constants.update(sccc)

    def constant(self, name, value):
        assert isinstance(name, str)
        assert self._constants is not None, "AllowConstants was not initialized"
        if name in self._constants or name in self.__dict__:
            raise ConstantError(name)
        self._constants[name] = value

    def __getattr__(self, attr):
        if attr in self._constants:
            return self._constants[attr]
        raise AttributeError(attr)

    def __setattr__(self, attr, val):
        if self._constants is None:
            # not finished initialization
            self.__dict__[attr] = val
        else:
            if attr in self._constants:
                raise ConstantError(attr)
            else:
                self.__dict__[attr] = val

    def __dir__(self):
        return super().__dir__() + list(self._constants.keys())

这样你就可以:

class Example(AllowConstants):
    _class_constants = {
        "BLA": 2
    }
    def __init__(self, a, b):
        super().__init__()
        self.constant("b", b)
        self.a = a

    def try_a(self, value):
        self.a = value

    def try_b(self, value):
        self.b = value

    def __str__(self):
        return str({"a": self.a, "b": self.b})

    def __repr__(self):
        return self.__str__()


class ChildExample1(Example):
    _class_constants = {
        "BLI": 88
    }


class ChildExample2(Example):
    _class_constants = {
        "BLA": 44
    }


example = ChildExample1(2,3)
print(example.BLA)  # 2
example.BLA = 8  # ConstantError BLA
print(example.BLI)  # 88
example.BLI = 8  # ConstantError BLI

example = ChildExample2(2,3)  # ConstantError BLA

您可以将一个常量包装在numpy数组中,将其标记为仅写,并始终通过下标0调用它。

import numpy as np

# declare a constant
CONSTANT = 'hello'

# put constant in numpy and make read only
CONSTANT = np.array([CONSTANT])
CONSTANT.flags.writeable = False
# alternatively: CONSTANT.setflags(write=0)

# call our constant using 0 index    
print 'CONSTANT %s' % CONSTANT[0]

# attempt to modify our constant with try/except
new_value = 'goodbye'
try:
    CONSTANT[0] = new_value
except:
    print "cannot change CONSTANT to '%s' it's value '%s' is immutable" % (
        new_value, CONSTANT[0])

# attempt to modify our constant producing ValueError
CONSTANT[0] = new_value



>>>
CONSTANT hello
cannot change CONSTANT to 'goodbye' it's value 'hello' is immutable
Traceback (most recent call last):
  File "shuffle_test.py", line 15, in <module>
    CONSTANT[0] = new_value
ValueError: assignment destination is read-only

当然,这只保护numpy的内容,而不是变量“CONSTANT”本身;你仍然可以:

CONSTANT = 'foo'

和CONSTANT会改变,然而,这将很快抛出TypeError第一次在脚本中调用CONSTANT[0]。

尽管……我想如果你在某个时候把它改成

CONSTANT = [1,2,3]

现在你不会再得到TypeError了。嗯……

https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.setflags.html