防止在 __init__ 之外创建新属性

发布于 2024-09-17 06:17:19 字数 372 浏览 5 评论 0原文

我希望能够创建一个类(在 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__ ,但我想知道是否有“正确”的方法来做到这一点?

I want to be able to create a class (in Python) that once initialized with __init__, does not accept new attributes, but accepts modifications of existing attributes. There's several hack-ish ways I can see to do this, for example having a __setattr__ method such as

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

and then editing __dict__ directly inside __init__, but I was wondering if there is a 'proper' way to do this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(13

救赎№ 2024-09-24 06:17:19

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

I wouldn't use __dict__ directly, but you can add a function to explicitly "freeze" a instance:

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
花落人断肠 2024-09-24 06:17:19

插槽是可行的方法:

Pythonic 方法是使用插槽而不是使用 __setattr__。虽然它可以解决问题,但不会带来任何性能改进。对象的属性存储在字典“__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. 使用插槽也有其自身的成本。它将破坏序列化(例如pickle)。它还将打破多重继承。一个类不能从多个类继承,这些类要么定义槽,要么具有用 C 代码定义的实例布局(如列表、元组或 int)。

Slots is the way to go:

The pythonic way is to use slots instead of playing around with the __setattr__. While it may solve the problem, it does not give any performance improvement. The attributes of objects are stored in a dictionary "__dict__"; this is the reason why you can dynamically add attributes to objects of classes that we have created so far. Using a dictionary for attribute storage is very convenient, but it can mean a waste of space for objects, which have only a small amount of instance variables.

Slots are a nice way to work around this space consumption problem. Instead of having a dynamic dict that allows adding attributes to objects dynamically, slots provide a static structure which prohibits additions after the creation of an instance.

When we design a class, we can use slots to prevent the dynamic creation of attributes. To define slots, you have to define a list with the name __slots__. The list has to contain all the attributes, you want to use. We demonstrate this in the following class, in which the slots list contains only the name for an attribute "val".

class S(object):

    __slots__ = ['val']

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


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

x.new = "not possible"

=> It fails to create an attribute "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'

Notes:

  1. Since Python 3.3 the advantage optimizing the space consumption is not as impressive any more. With Python 3.3 Key-Sharing Dictionaries are used for the storage of objects. The attributes of the instances are capable of sharing part of their internal storage between each other, i.e. the part which stores the keys and their corresponding hashes. This helps to reduce the memory consumption of programs, which create many instances of non-builtin types. But still is the way to go to avoid dynamically created attributes.

  2. Using slots come also with it's own cost. It will break serialization (e.g. pickle). It will also break multiple inheritance. A class can't inherit from more than one class that either defines slots or has an instance layout defined in C code (like list, tuple or int).

愛上了 2024-09-24 06:17:19

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

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

If someone is interested in doing that with a decorator, here is a working solution:

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

Pretty straightforward to use:

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

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

Result:

>>> Class Foo is frozen. Cannot set foobar = no way
守护在此方 2024-09-24 06:17:19

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

Actually, you don't want __setattr__, you want __slots__. Add __slots__ = ('foo', 'bar', 'baz') to the class body, and Python will make sure that there's only foo, bar and baz on any instance. But read the caveats the documentation lists!

南风起 2024-09-24 06:17:19

我非常喜欢使用装饰器的解决方案,因为它很容易在项目中的许多类中使用它,并且每个类的添加量最少。但它不适用于继承。
所以这是我的版本:它只覆盖 __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

I like very much the solution that uses a decorator, because it's easy to use it for many classes across a project, with minimum additions for each class. But it doesn't work well with inheritance.
So here is my version: It only overrides the __setattr__ function - if the attribute doesn't exist and the caller function is not __init__, it prints an error message.

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
脸赞 2024-09-24 06:17:19

正确的方法是重写__setattr__。这就是它的用途。

The proper way is to override __setattr__. That's what it's there for.

心作怪 2024-09-24 06:17:19

@dataclass(slots=True) Nirvana (Python 3.10)

我爱上了这个@dataclass thing:

main.py

from dataclasses import dataclass

@dataclass(slots=True)
class C:
    n: int
    s: str

c = C(n=1, s='one')
assert c.n == 1
assert c.s == 'one'
c.n == 2
c.s == 'two'
c.asdf = 2

结果:

Traceback (most recent call last):
  File "/home/ciro/main.py", line 15, in <module>
    c.asdf = 2
AttributeError: 'C' object has no attribute 'asdf'

注意 @dataclass 仅需要我们定义我们的使用键入注释一次属性

n: int
s: str

,然后不重复 我们免费获得:

  • def __init__(n, s):
        自我.n = n
        self.s = s
    
  • __slots__ = ['n', 's']

本例中未显示的其他免费内容:

在Python 3.10.7、Ubuntu 22.10上测试。

@dataclass(slots=True) Nirvana (Python 3.10)

I'm in love with this @dataclass thing:

main.py

from dataclasses import dataclass

@dataclass(slots=True)
class C:
    n: int
    s: str

c = C(n=1, s='one')
assert c.n == 1
assert c.s == 'one'
c.n == 2
c.s == 'two'
c.asdf = 2

Outcome:

Traceback (most recent call last):
  File "/home/ciro/main.py", line 15, in <module>
    c.asdf = 2
AttributeError: 'C' object has no attribute 'asdf'

Note how @dataclass only requires us to define our attributes once with type annotations

n: int
s: str

and then, without any repetition we get for free:

  • def __init__(n, s):
        self.n = n
        self.s = s
    
  • __slots__ = ['n', 's']

Other free things not shown in this example:

Tested on Python 3.10.7, Ubuntu 22.10.

梦冥 2024-09-24 06:17:19

这是我想出的方法,不需要 _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

Here is approach i came up with that doesn't need a _frozen attribute or method to freeze() in init.

During init i just add all class attributes to the instance.

I like this because there is no _frozen, freeze(), and _frozen also does not show up in the vars(instance) output.

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
丘比特射中我 2024-09-24 06:17:19

这个怎么样:

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)

What about this:

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)
心如荒岛 2024-09-24 06:17:19

我喜欢乔亨·里策尔的《冰雪奇缘》。不方便的是打印 Class.__dict 时会出现 isfrozen 变量
我通过创建授权属性列表(类似于插槽)来解决这个问题:

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

I like the "Frozen" of Jochen Ritzel. The inconvenient is that the isfrozen variable then appears when printing a Class.__dict
I went around this problem this way by creating a list of authorized attributes (similar to 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
韶华倾负 2024-09-24 06:17:19

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

The FrozenClass by Jochen Ritzel is cool, but calling _frozen() when initialing a class every time is not so cool (and you need to take the risk of forgetting it). I added a __init_slots__ function:

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
冰葑 2024-09-24 06:17:19

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

所以我想出了这个变体,它在初始化后安装我们较慢的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: ...

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: ...
灯下孤影 2024-09-24 06:17:19

我写了 pystrict 作为这个问题的解决方案。它太大了,无法将所有代码粘贴到 stackoverflow 中。

pystrict 是一个 pypi 可安装的装饰器,可以与类一起使用来冻结它们。这里的许多解决方案都不能正确支持继承。

如果 __slots__ 不适合您(由于继承问题),这是一个不错的选择。

自述文件中有一个示例,说明了为什么即使您的项目上运行了 mypy 和 pylint,也需要这样的装饰器:

pip install pystrict

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

from pystrict import strict

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

I wrote pystrict as a solution to this problem. It's too large to paste all of the code in stackoverflow.

pystrict is a pypi installable decorator that can be used with classes to freeze them. Many solutions here don't properly support inheritance.

If __slots__ doesn't work for you (because of inheritance issues), this is a good alternative.

There is an example to the README that shows why a decorator like this is needed even if you have mypy and pylint running on your project:

pip install pystrict

Then just use the @strict decorator:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文