在Python中更新“常量”属性时引发异常

3 投票
5 回答
2937 浏览
提问于 2025-04-15 13:59

因为Python没有常量的概念,所以如果一个“常量”属性被更新,能否抛出一个异常呢?怎么做呢?

class MyClass():
    CLASS_CONSTANT = 'This is a constant'
    var = 'This is a not a constant, can be updated'

#this should raise an exception    
MyClass.CLASS_CONSTANT = 'No, this cannot be updated, will raise an exception'

#this should not raise an exception    
MyClass.var = 'updating this is fine'

#this also should raise an exception    
MyClass().CLASS_CONSTANT = 'No, this cannot be updated, will raise an exception'

#this should not raise an exception    
MyClass().var = 'updating this is fine'

任何试图改变CLASS_CONSTANT这个类属性或者实例属性的操作都应该抛出一个异常。

而改变var这个属性,无论是作为类属性还是实例属性,都不应该抛出异常。

5 个回答

2

你可以使用元类来实现这个功能:

class ImmutableConstants(type):
    def __init__(cls, name, bases, dct):
        type.__init__(cls, name, bases, dct)

        old_setattr = cls.__setattr__
        def __setattr__(self, key, value):
            cls.assert_attribute_mutable(key)
            old_setattr(self, key, value)
        cls.__setattr__ = __setattr__

    def __setattr__(self, key, value):
        self.assert_attribute_mutable(key)
        type.__setattr__(self, key, value)

    def assert_attribute_mutable(self, name):
        if name.isupper():
            raise AttributeError('Attribute %s is constant' % name)

class Foo(object):
    __metaclass__ = ImmutableConstants
    CONST = 5
    class_var = 'foobar'

Foo.class_var = 'new value'
Foo.CONST = 42 # raises

不过,你确定这真的是个问题吗?你真的在到处不小心设置常量吗?其实你可以通过一个命令很容易找到这些常量,命令是 grep -r '\.[A-Z][A-Z0-9_]*\s*=' src/

2

你可以这样做:

(来自 http://www.siafoo.net/snippet/108)

class Constants:
  # A constant variable
  foo = 1337

  def __setattr__(self, attr, value):
    if hasattr(self, attr):
      raise ValueError, 'Attribute %s already has a value and so cannot be written to' % attr
    self.__dict__[attr] = value

然后像这样使用它:

>>> const = Constants()
>>> const.test1 = 42
>>> const.test1
42
>>> const.test1 = 43
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __setattr__
ValueError: Attribute test1 already has a value and so cannot be written to
>>> const.test1
42
4

在每个类中自定义 __setattr__(比如我之前提到的那个例子,还有@ainab的回答和其他一些回答),只能阻止对实例属性的赋值,而不能阻止对类属性的赋值。所以,现有的回答其实都不能完全满足你的需求。

如果你问的确实是你想要的东西,那你可以考虑使用一些自定义的元类和描述符的组合,比如:

class const(object):
  def __init__(self, val): self.val = val
  def __get__(self, *_): return self.val
  def __set__(self, *_): raise TypeError("Can't reset const!")

class mcl(type):
  def __init__(cls, *a, **k):
    mkl = cls.__class__
    class spec(mkl): pass
    for n, v in vars(cls).items():
      if isinstance(v, const):
        setattr(spec, n, v)
    spec.__name__ = mkl.__name__
    cls.__class__ = spec

class with_const:
  __metaclass__ = mcl

class foo(with_const):
  CLASS_CONSTANT = const('this is a constant')

print foo().CLASS_CONSTANT
print foo.CLASS_CONSTANT
foo.CLASS_CONSTANT = 'Oops!'
print foo.CLASS_CONSTANT

这些内容比较复杂,所以你可能会更喜欢其他回答中提到的简单的 __setattr__ 方法,尽管它并不能完全满足你的需求(也就是说,你可能会合理地选择放宽一些要求,以便让事情变得简单一些;-)。不过这里的技术还是挺有意思的:自定义的描述符类型 const 是另一种方法(在我看来比在每个需要常量的类中重写 __setattr__ 要好得多,而且可以避免把所有属性都变成常量,而是可以选择性地设置...)来阻止对实例属性的赋值;其余的代码是关于一个自定义元类创建独特的每类子元类,以便充分利用这个自定义描述符,达到你特别要求的功能。

撰写回答