簡體   English   中英

防止在 __init__ 之外創建新屬性

[英]Prevent creating new attributes outside __init__

我希望能夠創建一個用__init__初始化的類(在 Python 中),它不接受新屬性,但接受對現有屬性的修改。 我可以看到有幾種 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__ ,但我想知道是否有“正確”的方法來做到這一點?

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

插槽是要走的路:

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)的類繼承。

如果有人有興趣使用裝飾器來做這件事,這里有一個可行的解決方案:

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

實際上,您不想要__setattr__ ,您想要__slots__ __slots__ = ('foo', 'bar', 'baz')添加到類主體中,Python 將確保任何實例上都只有 foo、bar 和 baz。 但請閱讀文檔列表中的注意事項!

正確的方法是覆蓋__setattr__ 這就是它的用途。

我非常喜歡使用裝飾器的解決方案,因為它很容易用於項目中的許多類,每個類的添加量最少。 但它不適用於繼承。 所以這是我的版本:它只覆蓋 __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

那這個呢:

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)

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

pystrict一個 pypi 可安裝裝飾器,其靈感來自這個 stackoverflow 問題,可與類一起使用以凍結它們。 README 中有一個示例,說明即使您的項目上運行了 mypy 和 pylint,為什么需要這樣的裝飾器:

pip install pystrict

然后只需使用@strict 裝飾器:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 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

Jochen FrozenClass的 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

沒有一個答案提到覆蓋__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: ...

我在這里和其他地方吸收了最好的想法,制作了一個裝飾得像插槽和冷凍的裝飾器。

https://gist.github.com/rsheftel/3c05c7d699c79efdf7004576fd692aff

我寫了pystrict作為這個問題的解決方案。 將所有代碼粘貼到stackoverflow中太大了。

pystrict是一個 pypi 可安裝的裝飾器,可以與類一起使用來凍結它們。 這里的許多解決方案都不能正確支持繼承。

如果__slots__對您不起作用(由於繼承問題),這是一個不錯的選擇。

README 中有一個示例說明了為什么即使您的項目上運行了 mypy 和 pylint 也需要這樣的裝飾器:

pip install pystrict

然后只需使用 @strict 裝飾器:

from pystrict import strict

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM