如何在Python中声明常量?

在Java中,我们做:

public static final String CONST_NAME = "Name";

当前回答

注意:这是一个糟糕的想法和糟糕的实现。此外,它只适用于最后的小例子,一个完整的实现将意味着大量的工作,这是我太懒了。而且,在Python 3.8之前,审计钩子可能是不可用的。

我基本上回答了另一个问题,结果和这个问题有关。它的思想是,你可以利用审计钩子来捕捉每一行的执行,解析代码对象,如果它满足某些条件(例如某个前缀并且已经定义过一次),你可以抛出一个错误。

你可能不得不支持其他赋值类型(例如,对于导入的东西,可能对于函数内部的局部变量,解包等),不使用全局变量,因为字典可以很容易地修改,实际上调查这是否安全,接受这个实现将对你的整个应用程序造成的性能损失,确保它在REPL之外工作,在ipython内部工作,等等等等。不管怎样,我们开始吧:

>>> import sys
>>> import ast
>>> import dis
>>> import types
>>> 
>>> 
>>> def hook(name, tup):
...     if name == "exec" and tup:
...         if tup and isinstance(tup[0], types.CodeType):
...             code = tup[0]
...             store_instruction_arg = None
...             instructions = [dis.opname[op] for op in code.co_code]
...             
...             for i, instruction in enumerate(instructions):
...                 if instruction == "STORE_NAME":
...                     store_instruction_arg = code.co_code[i + 1]
...                     break
...             
...             if store_instruction_arg is not None:
...                 var_name = code.co_names[store_instruction_arg]
...                 if var_name in globals():
...                     raise Exception("Cannot re-assign variable")
... 
>>> 
>>> sys.addaudithook(hook)
>>> 
>>> a = '123'
>>> a = 456
Traceback (most recent call last):
  File "<stdin>", line 16, in hook
Exception: Cannot re-assign variable
>>> 
>>> a
'123'

如果你以这种方式结束,你不应该,除了修复和泛化代码,你可能会想要找到一种方法,只让一些东西不变,例如,只有那些有特殊前缀的对象或只有对象有一些注释。

其他回答

没有完美的方法可以做到这一点。据我所知,大多数程序员只是将标识符大写,所以PI = 3.142可以很容易地理解为一个常数。

另一方面,如果你想要一个像常数一样的东西,我不确定你能找到它。无论你做什么,总会有一些方法来编辑“常数”,所以它不是一个真正的常数。这里有一个非常简单的例子:

def define(name, value):
  if (name + str(id(name))) not in globals():
    globals()[name + str(id(name))] = value

def constant(name):
  return globals()[name + str(id(name))]

define("PI",3.142)

print(constant("PI"))

这看起来像是一个php风格的常量。

在现实中,所有需要改变值的人是这样的:

globals()["PI"+str(id("PI"))] = 3.1415

这对于你在这里找到的所有其他解决方案都是一样的——即使是那些创建类并重新定义set属性方法的聪明的解决方案——总有一种方法可以绕过它们。Python就是这样的。

我的建议是避免所有的麻烦,将标识符大写。它不是一个合适的常数,但也没有什么合适的常数。

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

from collections import namedtuple


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

使用的例子

CONSTS = make_consts("baz1",
                     foo=1,
                     bar=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

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

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

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

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

请随意提出改进建议。

你可以通过collections.namedtuple和itertools来实现:

import collections
import itertools
def Constants(Name, *Args, **Kwargs):
  t = collections.namedtuple(Name, itertools.chain(Args, Kwargs.keys()))
  return t(*itertools.chain(Args, Kwargs.values()))

>>> myConstants = Constants('MyConstants', 'One', 'Two', Three = 'Four')
>>> print myConstants.One
One
>>> print myConstants.Two
Two
>>> print myConstants.Three
Four
>>> myConstants.One = 'Two'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute