如何在Python中声明常量?

在Java中,我们做:

public static final String CONST_NAME = "Name";

当前回答

在我的例子中,我需要不可变字节数组来实现包含许多文字数字的加密库,我想确保这些数字是常量。

这个答案是有效的,但是尝试重赋bytearray元素不会引发错误。

def const(func):
    '''implement const decorator'''
    def fset(self, val):
        '''attempting to set a const raises `ConstError`'''
        class ConstError(TypeError):
            '''special exception for const reassignment'''
            pass

        raise ConstError

    def fget(self):
        '''get a const'''
        return func()

    return property(fget, fset)


class Consts(object):
    '''contain all constants'''

    @const
    def C1():
        '''reassignment to C1 fails silently'''
        return bytearray.fromhex('deadbeef')

    @const
    def pi():
        '''is immutable'''
        return 3.141592653589793

常量是不可变的,但是常量bytearray赋值默默失败:

>>> c = Consts()
>>> c.pi = 6.283185307179586  # (https://en.wikipedia.org/wiki/Tau_(2%CF%80))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "consts.py", line 9, in fset
    raise ConstError
__main__.ConstError
>>> c.C1[0] = 0
>>> c.C1[0]
222
>>> c.C1
bytearray(b'\xde\xad\xbe\xef')

一种更强大、更简单,甚至可能更“python化”的方法涉及使用memoryview对象(<= python-2.6中的缓冲区对象)。

import sys

PY_VER = sys.version.split()[0].split('.')

if int(PY_VER[0]) == 2:
    if int(PY_VER[1]) < 6:
        raise NotImplementedError
    elif int(PY_VER[1]) == 6:
        memoryview = buffer

class ConstArray(object):
    '''represent a constant bytearray'''
    def __init__(self, init):
        '''
        create a hidden bytearray and expose a memoryview of that bytearray for
        read-only use
        '''
        if int(PY_VER[1]) == 6:
            self.__array = bytearray(init.decode('hex'))
        else:
            self.__array = bytearray.fromhex(init)

        self.array = memoryview(self.__array)

    def __str__(self):
        return str(self.__array)

    def __getitem__(self, *args, **kwargs):
       return self.array.__getitem__(*args, **kwargs)

ConstArray项赋值是一个TypeError:

>>> C1 = ConstArray('deadbeef')
>>> C1[0] = 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'ConstArray' object does not support item assignment
>>> C1[0]
222

其他回答

在其他语言中没有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选择这些名称的事实,并使用应用于该作用域的所有本地定义的**(双星号)创建一个关键字字典,将参数传递给属性函数

使用namedtuple有一种更干净的方法:

from collections import namedtuple


def make_consts(name, **kwargs):
    return namedtuple(name, kwargs.keys())(**kwargs)

使用的例子

CONSTS = make_consts("baz1",
                     foo=1,
                     bar=2)

使用这种方法,您可以为常数命名空间。

我将创建一个重写基对象类的__setattr__方法的类,并用它包装我的常量,注意我使用的是python 2.7:

class const(object):
    def __init__(self, val):
        super(const, self).__setattr__("value", val)
    def __setattr__(self, name, val):
        raise ValueError("Trying to change a constant value", self)

换行字符串:

>>> constObj = const("Try to change me")
>>> constObj.value
'Try to change me'
>>> constObj.value = "Changed"
Traceback (most recent call last):
   ...
ValueError: Trying to change a constant value
>>> constObj2 = const(" or not")
>>> mutableObj = constObj.value + constObj2.value
>>> mutableObj #just a string
'Try to change me or not'

这很简单,但如果你想像使用非常量对象一样使用常量(不使用constObj.value),它会更密集一些。这可能会导致问题,所以最好保留.value来显示和知道您正在使用常量进行操作(尽管可能不是最“python”的方式)。

扩展Raufio的答案,添加__repr__来返回值。

class const(object):
    def __init__(self, val):
        super(const, self).__setattr__("value", val)
    def __setattr__(self, name, val):
        raise ValueError("Trying to change a constant value", self)
    def __repr__(self):
        return ('{0}'.format(self.value))

dt = const(float(0.01))
print dt

那么对象的行为就更像你所期望的那样,你可以直接访问它而不是使用"。value "

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

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