禁止在__init__之外创建新属性


80

我希望能够创建一个用Python初始化的类(在Python中),该类__init__不接受新属性,但接受对现有属性的修改。我可以通过几种方法来做到这一点,例如,使用__setattr__诸如

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

然后__dict__直接在内部进行编辑__init__,但是我想知道是否存在“正确”的方法?


1
katrielalex带来了优点。没什么好说的。您可以避免使用,__setattr__但这可能很hack。
aaronasterling

我不明白为什么这很hacky?这是我能想到的最佳解决方案,并且比其他一些提议要简洁得多。
克里斯·B

Answers:


76

我不会__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

很酷!我想我会抓住那段代码并开始使用它。(嗯,我想知道它是否可以做为装饰工,或者这不是一个好主意...)
weronika 2011年

5
最新评论:在一段时间内,我一直在成功使用此食谱,直到将属性更改为属性,而吸气剂却引发了NotImplementedError。我花了很长时间才发现这是由于实际hasattr调用getattr,丢弃结果并在出现错误的情况下返回False所致,请参阅此博客。通过替换not hasattr(self, key)为找到了解决方法key not in dir(self)。这可能会比较慢,但可以为我解决问题。
巴斯·温克尔斯

30

如果有人有兴趣用装饰器做这件事,这里有一个可行的解决方案:

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

+1为装饰器版本。这就是我在较大的项目中使用的内容,在较大的脚本中这是过大的(也许如果他们在标准库中有它……)。目前只有“ IDE样式警告”。
Tomasz Gandor

2
此解决方案如何与传统一起使用?例如,如果我有一个Foo子类,则该子类默认为冻结类?
mrgiesel '16

该装饰器是否有pypi包?
winni2k

如何增强装饰器使其适用于继承的类?
Ivan Nechipayko

26

老虎机是必经之路:

pythonic的方法是使用插槽而不是使用__setter__。虽然它可以解决问题,但并不能改善性能。对象的属性存储在字典“ __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"

=>无法创建属性“ 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中,密钥共享字典用于存储对象。实例的属性能够在彼此之间共享其内部存储的一部分,即存储密钥及其对应哈希的部分。这有助于减少程序的内存消耗,程序会创建许多非内置类型的实例。但是仍然是避免动态创建属性的方法。

  2. 使用插槽也需要自己付费。它将破坏序列化(例如,泡菜)。它还将破坏多重继承。一个类不能从一个以上的类中继承,该类定义插槽或使用C代码定义的实例布局(如list,tuple或int)。


20

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


12
使用__slots__作品,但除其他事项外,它会破坏序列化(例如泡菜)……无论如何,使用插槽控制属性创建通常不是一个好主意,反而是减少内存开销,在我看来……
Joe Kington

我知道,我很想亲自使用它-但做额外的工作以禁止使用新属性通常也是个坏主意;)

2
使用__slots__还会破坏多重继承。一个类不能从一个以上的类中继承,该类可以定义插槽或使用C代码定义的实例布局(例如listtupleint)。
Feuermurmel,2012年

如果__slots__您的腌菜破裂,则说明您使用的是古老的腌菜协议。传递protocol=-1给最新的可用协议的pickle方法,这是Python 2中的2(于2003年引入)。Python 3(分别为3和4)中的默认协议和最新协议__slots__
Nick Matteo

好吧,大多数时候我都后悔根本不使用泡菜: 根本不 benfrederickson.com/dont-pickle-your-data
Erik Aronesty

7

正确的方法是重写__setattr__。那就是它的目的。


那么在中设置变量的正确方法是__init__什么?是否可以__dict__直接设置它们?
astrofrog

1
我会覆盖__setattr____init__,通过self.__setattr__ = <new-function-that-you-just-defined>
卡特里尔2010年

6
@katrielalex:不适用于新式类,因为 __xxx__方法仅在类而不是实例上查找。
伊桑·弗曼

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

4

那这个呢:

class A():
    __allowed_attr=('_x', '_y')

    def __init__(self,x=0,y=0):
        self._x=x
        self._y=y

    def __setattr__(self,attribute,value):
        if not attribute in self.__class__.__allowed_attr:
            raise AttributeError
        else:
            super().__setattr__(attribute,value)

2

这是我想出的方法,不需要_frozen属性或init中的Frozen()方法。

初始化期间我只是将所有类属性添加到实例。

我喜欢这个,因为没有_frozen,frozen(),并且_frozen也没有出现在vars(instance)输出中。

class MetaModel(type):
    def __setattr__(self, name, value):
        raise AttributeError("Model classes do not accept arbitrary attributes")

class Model(object):
    __metaclass__ = MetaModel

    # init will take all CLASS attributes, and add them as SELF/INSTANCE attributes
    def __init__(self):
        for k, v in self.__class__.__dict__.iteritems():
            if not k.startswith("_"):
                self.__setattr__(k, v)

    # setattr, won't allow any attributes to be set on the SELF/INSTANCE that don't already exist
    def __setattr__(self, name, value):
        if not hasattr(self, name):
            raise AttributeError("Model instances do not accept arbitrary attributes")
        else:
            object.__setattr__(self, name, value)


# Example using            
class Dog(Model):
    name = ''
    kind = 'canine'

d, e = Dog(), Dog()
print vars(d)
print vars(e)
e.junk = 'stuff' # fails

如果字段之一是列表,这似乎不起作用。比方说names=[]。然后d.names.append['Fido']将插入'Fido'd.namese.names。我对Python不够了解,无法理解为什么。
Reinier Torenbeek '17

2

pystrict是受此stackoverflow问题启发的pypi可安装装饰器,可与类一起使用以冻结它们。自述文件中有一个示例,说明了即使在项目上运行了mypy和pylint的情况,为什么也需要这样的装饰器:

pip install pystrict

然后只需使用@strict装饰器:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1

1

我喜欢Jochen Ritzel的“ Frozen”。不方便的是,在打印Class时,会出现isfrozen变量。__dict 通过创建授权属性(类似于slot)列表来解决此问题:

class Frozen(object):
    __List = []
    def __setattr__(self, key, value):
        setIsOK = False
        for item in self.__List:
            if key == item:
                setIsOK = True

        if setIsOK == True:
            object.__setattr__(self, key, value)
        else:
            raise TypeError( "%r has no attributes %r" % (self, key) )

class Test(Frozen):
    _Frozen__List = ["attr1","attr2"]
    def __init__(self):
        self.attr1   =  1
        self.attr2   =  1

1

FrozenClassJochen Ritzel的by很酷,但是_frozen()每次初始化课程时打电话都不是很酷(并且您需要冒险忘记它)。我添加了一个__init_slots__功能:

class FrozenClass(object):
    __isfrozen = False
    def _freeze(self):
        self.__isfrozen = True
    def __init_slots__(self, slots):
        for key in slots:
            object.__setattr__(self, key, None)
        self._freeze()
    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)
class Test(FrozenClass):
    def __init__(self):
        self.__init_slots__(["x", "y"])
        self.x = 42#
        self.y = 2**3


a,b = Test(), Test()
a.x = 10
b.z = 10 # fails
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.