防止在__init__外创建新属性

113 投票
13 回答
39067 浏览
提问于 2025-04-16 03:29

我想创建一个类(用Python写的),一旦用__init__初始化后,就不再接受新的属性,但可以修改已有的属性。我看到有几种比较“hack”的方法可以做到这一点,比如可以写一个__setattr__的方法,像这样:

def __setattr__(self, attribute, value):
    if not attribute in self.__dict__:
        print "Cannot set %s" % attribute
    else:
        self.__dict__[attribute] = value

然后在__init__里面直接修改__dict__,但我在想有没有更“正规”的方法来实现这个功能呢?

13 个回答

38

如果有人想用装饰器来实现这个功能,这里有一个可行的解决方案:

from functools import wraps

def froze_it(cls):
    cls.__frozen = False

    def frozensetattr(self, key, value):
        if self.__frozen and not hasattr(self, key):
            print("Class {} is frozen. Cannot set {} = {}"
                  .format(cls.__name__, key, value))
        else:
            object.__setattr__(self, key, value)

    def init_decorator(func):
        @wraps(func)
        def wrapper(self, *args, **kwargs):
            func(self, *args, **kwargs)
            self.__frozen = True
        return wrapper

    cls.__setattr__ = frozensetattr
    cls.__init__ = init_decorator(cls.__init__)

    return cls

使用起来非常简单:

@froze_it 
class Foo(object):
    def __init__(self):
        self.bar = 10

foo = Foo()
foo.bar = 42
foo.foobar = "no way"

结果是:

>>> Class Foo is frozen. Cannot set foobar = no way
69

使用 slots 是个好主意:

在 Python 中,使用 slots 比起随意使用 __setattr__ 更加合适。虽然这样做可以解决一些问题,但并不会提高性能。对象的属性是存储在一个叫做 "__dict__" 的字典里的;这就是为什么我们可以动态地给自己创建的类的对象添加属性的原因。用字典来存储属性非常方便,但对于那些只有少量实例变量的对象来说,这可能会浪费空间。

Slots 是解决这个空间浪费问题的好方法。它们提供了一种静态结构,禁止在实例创建后再添加属性,而不是使用一个可以动态添加属性的字典。

在设计一个类的时候,我们可以使用 slots 来防止动态创建属性。要定义 slots,你需要创建一个名为 __slots__ 的列表。这个列表里要包含你想要使用的所有属性。下面的例子展示了一个类,其中 slots 列表只包含一个属性名 "val"。

class S(object):

    __slots__ = ['val']

    def __init__(self, v):
        self.val = v


x = S(42)
print(x.val)

x.new = "not possible"

=> 这样会导致无法创建一个名为 "new" 的属性:

42 
Traceback (most recent call last):
  File "slots_ex.py", line 12, in <module>
    x.new = "not possible"
AttributeError: 'S' object has no attribute 'new'

注意事项:

  1. 自从 Python 3.3 以来,优化空间消耗的优势不再那么明显。Python 3.3 引入了 键共享 字典来存储对象。实例的属性可以共享它们内部存储的一部分,也就是存储键和对应哈希值的部分。这有助于减少创建许多非内置类型实例的程序的内存消耗。但为了避免动态创建属性,使用 slots 仍然是个好办法。

  2. 使用 slots 也有其自身的代价。它会破坏序列化(例如,使用 pickle)。它还会影响多重继承。一个类不能同时继承多个定义了 slots 或者在 C 代码中定义了实例布局的类(比如 list、tuple 或 int)。

106

我不建议直接使用 __dict__,但是你可以添加一个函数来明确地“冻结”一个实例:

class FrozenClass(object):
    __isfrozen = False
    def __setattr__(self, key, value):
        if self.__isfrozen and not hasattr(self, key):
            raise TypeError( "%r is a frozen class" % self )
        object.__setattr__(self, key, value)

    def _freeze(self):
        self.__isfrozen = True

class Test(FrozenClass):
    def __init__(self):
        self.x = 42#
        self.y = 2**3

        self._freeze() # no new attributes after this point.

a,b = Test(), Test()
a.x = 10
b.z = 10 # fails

撰写回答