防止在__init__之外创建新属性

ast*_*rog 66 python oop class python-datamodel python-3.x

我希望能够创建一个类(在Python中),一旦初始化__init__,不接受新属性,但接受现有属性的修改.我可以看到有几种黑客方法可以做到这一点,例如有一个__setattr__方法,比如

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

然后__dict__直接在里面编辑__init__,但我想知道是否有"正确"的方法来做到这一点?

Joc*_*zel 69

我不会__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
Run Code Online (Sandbox Code Playgroud)

  • 迟到评论:我成功地使用了这个配方一段时间,直到我将属性更改为属性,其中getter引发了NotImplementedError.我花了很长时间才发现这是因为`hasattr` actuall调用`getattr`,拖拽结果并在出现错误时返回False,请参阅[this blog](https:// hynek.我/用品/ hasattr /).通过`key而不是dir(self)`替换`not hasattr(self,key)`找到了一个解决方法.这可能会慢一点,但我解决了这个问题. (2认同)

Yoa*_*ann 27

如果有人有兴趣使用装饰器这样做,这是一个有效的解决方案:

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
Run Code Online (Sandbox Code Playgroud)

非常简单易用:

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

foo = Foo()
foo.bar = 42
foo.foobar = "no way"
Run Code Online (Sandbox Code Playgroud)

结果:

>>> Class Foo is frozen. Cannot set foobar = no way
Run Code Online (Sandbox Code Playgroud)

  • 这个解决方案如何与遗产合作?例如,如果我有一个Foo的子类,这个孩子默认是一个冻结的类? (2认同)

小智 18

实际上,你不想要__setattr__,你想要的__slots__.添加__slots__ = ('foo', 'bar', 'baz')到类主体,Python将确保在任何实例上只有foo,bar和baz.但请阅读文档列表中的警告!

  • 使用`__slots__`可以工作,但它会破坏序列化(例如pickle)等等......在我看来,使用插槽来控制属性创建通常是一个坏主意,而不是减少内存开销...... (11认同)
  • 使用`__slots__`也会破坏多重继承.类不能从多个定义__slots__的类继承,也不能从C代码中定义的实例布局(如`list`,`tuple`或`int`)继承. (2认同)

Dhi*_*aTN 18

插槽是要走的路:

pythonic的方式是使用插槽而不是玩游戏__setter__.虽然它可以解决问题,但它不会带来任何性能提升.对象的属性存储在字典" __dict__"中,这就是为什么你可以动态地将属性添加到我们到目前为止创建的类的对象的原因.使用字典进行属性存储非常方便,但它可能意味着浪费空间,而对象只有少量的实例变量.

插槽是解决这个空间消耗问题的好方法.插槽提供静态结构,禁止在创建实例后添加,而不是具有允许动态向对象添加属性的动态dict.

当我们设计一个类时,我们可以使用插槽来阻止动态创建属性.要定义插槽,您必须使用名称定义列表__slots__.该列表必须包含您要使用的所有属性.我们在下面的类中演示了这一点,其中槽列表仅包含属性"val"的名称.

class S(object):

    __slots__ = ['val']

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


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

x.new = "not possible"
Run Code Online (Sandbox Code Playgroud)

=>无法创建属性"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'
Run Code Online (Sandbox Code Playgroud)

注意:

  1. 从Python 3.3开始,优化空间消耗的优势就不那么令人印象深刻了.使用Python 3.3 密钥共享字典用于存储对象.实例的属性能够在彼此之间共享其内部存储的一部分,即存储密钥的部分及其对应的散列.这有助于减少程序的内存消耗,从而创建许多非内置类型的实例.但仍然是避免动态创建属性的方法.

  2. 使用插槽也需要它自己的成本.它将打破序列化(例如泡菜).它还将打破多重继承.类不能从多个类继承,这些类要么定义槽,要么在C代码中定义实例布局(如list,tuple或int).

  • 这些引言出自哪里?您需要引用您的消息来源。使用别人的作品而不注明出处就近乎抄袭。详细信息请参见[引用帮助](/help/referencing) (2认同)

Kat*_*iel 6

正确的方法是覆盖__setattr__.这就是它的用途.

  • @katrielalex:这对新式类不起作用,因为`__xxx__`方法只在类上查找,而不是在实例上查找. (6认同)

Era*_*man 6

我非常喜欢使用装饰器的解决方案,因为它很容易在项目中的许多类中使用它,每个类的添加最少。但它不适用于继承。所以这是我的版本:它只覆盖 __setattr__ 函数 - 如果该属性不存在并且调用者函数不是 __init__,它会打印一条错误消息。

import inspect                                                                                                                             

def froze_it(cls):                                                                                                                      

    def frozensetattr(self, key, value):                                                                                                   
        if not hasattr(self, key) and inspect.stack()[1][3] != "__init__":                                                                 
            print("Class {} is frozen. Cannot set {} = {}"                                                                                 
                  .format(cls.__name__, key, value))                                                                                       
        else:                                                                                                                              
            self.__dict__[key] = value                                                                                                     

    cls.__setattr__ = frozensetattr                                                                                                        
    return cls                                                                                                                             

@froze_it                                                                                                                                  
class A:                                                                                                                                   
    def __init__(self):                                                                                                                    
        self._a = 0                                                                                                                        

a = A()                                                                                                                                    
a._a = 1                                                                                                                                   
a._b = 2 # error
Run Code Online (Sandbox Code Playgroud)