98

我希望能够创建一个用 初始化的类(在 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

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

4

12 回答 12

92

我不会__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
于 2010-08-30T20:04:00.747 回答
47

插槽是要走的路:

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

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

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
于 2015-03-31T12:26:13.650 回答
21

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

于 2010-08-30T19:38:39.637 回答
7

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

于 2010-08-30T19:22:54.717 回答
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
于 2016-04-27T18:57:02.277 回答
4

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

pip install pystrict

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

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
于 2019-09-13T15:27:50.597 回答
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)
于 2016-06-24T15:51:47.073 回答
2

这是我想出的方法,它不需要 _frozen 属性或方法来在 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
于 2016-02-19T17:47:49.223 回答
1

None of the answers mention the performance impact of overriding __setattr__, which can be an issue when creating many small objects. (And __slots__ would be the performant solution but limits pickle/inheritance).

So I came up with this variant which installs our slower settatr after init:

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): ...

If you don't want to call freeze at the end of __init__, if inheritance is an issue, or if you don't want it in vars(), it can also be adapted: for example here is a decorator version based on the pystrict answer:

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: ...
于 2021-03-17T11:20:32.110 回答
1

FrozenClassJochen Ritzel 编写的很酷,但是_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
于 2016-08-31T07:49:44.920 回答
1

我喜欢 Jochen Ritzel 的《冰雪奇缘》。不方便的是isfrozen 变量然后在打印 Class.__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
于 2016-06-06T04:14:58.140 回答