【问题标题】:Prevent creating new attributes outside __init__防止在 __init__ 之外创建新属性
【发布时间】:2011-04-05 22:24:28
【问题描述】:

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

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

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

【问题讨论】:

  • katrielalex 带来了好处。没有什么可笑的。你可以避免使用__setattr__,但这可能会很老套。
  • 我不明白为什么这是 hacky?这是我能想到的最好的解决方案,而且比其他一些人提出的要简洁得多。

标签: python python-3.x class oop python-datamodel


【解决方案1】:

我不会直接使用__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

【讨论】:

  • 非常酷!我想我会抓住那段代码并开始使用它。 (嗯,我想知道它是否可以作为装饰器来完成,或者这不是一个好主意......)
  • 迟到的评论:我成功地使用了这个配方有一段时间了,直到我将一个属性更改为一个属性,其中 getter 引发了 NotImplementedError。我花了很长时间才发现这是因为hasattr 实际调用getattr,删除结果并在出现错误时返回False,请参阅this blog。通过将not hasattr(self, key) 替换为key not in dir(self) 找到了解决方法。这可能会更慢,但为我解决了问题。
【解决方案2】:

插槽是要走的路:

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 中,Key-Sharing 字典用于存储对象。实例的属性能够在彼此之间共享其内部存储的一部分,即存储密钥及其相应哈希的部分。这有助于减少程序的内存消耗,这些程序会创建许多非内置类型的实例。但仍然是避免动态创建属性的方法。
  1. 使用插槽也需要自己付费。它会破坏序列化(例如pickle)。它还会破坏多重继承。一个类不能从多个定义插槽或具有用 C 代码定义的实例布局(如列表、元组或 int)的类继承。

【讨论】:

    【解决方案3】:

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

    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 样式警告”。
    • 这个解决方案如何与遗产一起使用?例如如果我有一个 Foo 的子类,这个子类默认是冻结类?
    • 这个装饰器有pypi包吗?
    • 如何增强装饰器使其适用于继承的类?
    【解决方案4】:

    其实,你不想要__setattr__,你想要__slots__。将__slots__ = ('foo', 'bar', 'baz') 添加到类主体中,Python 将确保任何实例上都只有 foo、bar 和 baz。但请阅读文档列表中的注意事项!

    【讨论】:

    • 使用__slots__ 有效,但它会破坏序列化(例如pickle)等等......使用插槽来控制属性创建而不是减少内存开销通常是一个坏主意,在无论如何,我的意见......
    • 我知道,但我犹豫要不要自己使用它 - 但是做额外的工作来禁止新属性通常也是一个坏主意;)
    • 使用__slots__ 也会破坏多重继承。一个类不能继承自多个定义slots 或具有在C 代码中定义的实例布局的类(如listtupleint)。
    • 如果__slots__ 破坏了你的泡菜,你正在使用一个古老的泡菜协议。将protocol=-1 传递给最新可用协议的pickle 方法,在Python 2 (introduced in 2003) 中为2。 Python 3 中的默认和最新协议(分别为 3 和 4)都处理 __slots__
    • 好吧,大多数时候我都后悔使用泡菜了:benfrederickson.com/dont-pickle-your-data
    【解决方案5】:

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

    【讨论】:

    • 那么在__init__ 中设置变量的正确方法是什么?是直接设置在__dict__吗?
    • 我将覆盖 __init__ 中的 __setattr__,由 self.__setattr__ = &lt;new-function-that-you-just-defined&gt; 覆盖。
    • @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
    

    【讨论】:

      【解决方案7】:

      这个呢:

      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)
      

      【讨论】:

        【解决方案8】:

        pystricta pypi installable decorator 的灵感来自这个 stackoverflow 问题,可以与类一起使用来冻结它们。 README 中有一个示例说明了为什么即使您的项目中运行了 mypy 和 pylint 也需要这样的装饰器:

        pip install pystrict

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

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

        【讨论】:

          【解决方案9】:

          这是我想出的方法,它不需要 _frozen 属性或方法在 init 中进行 freeze()。

          init 期间,我只是将所有类属性添加到实例中。

          我喜欢这个,因为没有 _frozen、freeze(),而且 _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'] 将在d.namese.names 中插入'Fido'。我对 Python 的了解还不够,无法理解为什么。
          【解决方案10】:

          我喜欢 Jochen Ritzel 的《冰雪奇缘》。不方便的是在打印 Class.__dict 时会出现 isfrozen 变量 我通过创建授权属性列表(类似于 slots)以这种方式解决了这个问题:

          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
          

          【讨论】:

            【解决方案11】:

            Jochen Ritzel 的FrozenClass 很酷,但是每次初始化课程时调用_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
            

            【讨论】:

              【解决方案12】:

              没有一个答案提到覆盖__setattr__ 对性能的影响,这在创建许多小对象时可能是个问题。 (而__slots__ 将是高效的解决方案,但会限制泡菜/继承)。

              所以我想出了这个变体,它在初始化后安装我们较慢的 settatr:

              class FrozenClass:
              
                  def freeze(self):
                      def frozen_setattr(self, key, value):
                          if not hasattr(self, key):
                              raise TypeError("Cannot set {}: {} is a frozen class".format(key, self))
                          object.__setattr__(self, key, value)
                      self.__setattr__ = frozen_setattr
              
              class Foo(FrozenClass): ...
              

              如果您不想在__init__ 的末尾调用freeze,如果继承是一个问题,或者如果您不想在vars() 中使用它,也可以进行调整:例如这里是基于pystrict答案的装饰器版本:

              import functools
              def strict(cls):
                  cls._x_setter = getattr(cls, "__setattr__", object.__setattr__)
                  cls._x_init = cls.__init__
                  @functools.wraps(cls.__init__)
                  def wrapper(self, *args, **kwargs):
                      cls._x_init(self, *args, **kwargs)
                      def frozen_setattr(self, key, value):
                          if not hasattr(self, key):
                              raise TypeError("Class %s is frozen. Cannot set '%s'." % (cls.__name__, key))
                          cls._x_setter(self, key, value)
                      cls.__setattr__ = frozen_setattr
                  cls.__init__ = wrapper
                  return cls
              
              @strict
              class Foo: ...
              

              【讨论】:

                猜你喜欢
                • 2014-09-26
                • 2017-11-28
                • 2011-09-06
                • 1970-01-01
                • 2020-04-10
                • 2011-10-27
                • 2013-10-17
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多