Предотвратить создание новых атрибутов вне функции init
Я хочу иметь возможность создать класс (в 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__
, но мне было интересно, есть ли "правильный" способ сделать это?
11 ответов:
Я бы не стал использовать
__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
на самом деле, вы не хотите
__setattr__
вы хотите__slots__
. Добавить__slots__ = ('foo', 'bar', 'baz')
к телу класса, и Python будет убедиться, что есть только foo, bar и baz на любом экземпляре. Но прочитайте предостережения списки документации!
Если кто-то заинтересован в этом с декоратором, вот рабочее решение:
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
слоты-это путь:
питонический способ заключается в использовании слотов вместо того, чтобы играть с
__setter__
. Хотя это может решить проблему, она не дает никакого улучшения производительности. Атрибуты объектов хранятся в словаре "__dict__
", это является причиной, почему вы можете динамически добавлять атрибуты к объектам классов, которые мы создали до сих пор. Использование словаря для хранения атрибутов очень удобно, но это может означать отходы космических объектов, которые имеют только небольшое количество переменных экземпляра.слоты хороший способ обойти эту проблему потребления пространства. Вместо того, чтобы иметь динамический 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'
NB:
так как Python 3.3 преимущество оптимизации потребления пространства является уже не так впечатляет. С Python 3.3 Ключ-Обмен словари используются для хранения объектов. Атрибуты экземпляров способны совместно использовать часть их внутренней памяти между собой, т. е. часть, которая хранит ключи и их соответствующие хэши. Это помогает уменьшить потребление памяти программ, которые создают много экземпляров не встроенных типов. Но все же это способ избежать динамически создаваемых атрибутов.
используя слоты приходят также с его собственной ценой. Это нарушит сериализацию (например, рассол). Это также нарушит множественное наследование. Класс не может наследовать от более чем одного класса, который либо определяет слоты, либо имеет макет экземпляра, определенный в коде C (например, list, tuple или int).
Мне очень нравится решение, которое использует декоратор, потому что его легко использовать для многих классов в проекте, с минимальными добавлениями для каждого класса. Но это не очень хорошо работает с наследованием. Итак, вот моя версия: он только переопределяет функцию __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
вот подход, который я придумал, что не нужен атрибут _frozen или метод для замораживания () в init.
во время 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
Как насчет этого:
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)
Мне нравится "замороженный" Йохен Ритцель. Неудобно то, что isfrozen переменная затем появляется при печати класса.__dict Я обошел эту проблему таким образом, создав список авторизованных атрибутами (подобно слоты):
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
The
FrozenClass
по Йохен Ритцель это круто, но вызов_frozen()
при инициализации класса каждый раз не так круто (и вам нужно рискнуть забыть об этом). Я добавил :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
улучшение по сравнению с этим отличное решение используя
__slots__
было бы позволить__init__
метод определяет допустимые атрибуты, а затем устанавливает__slots__
атрибут с текущим классом__dict__
преимущество заключается в том, что он избегает двойного обновления при добавлении атрибута в
__init__
:class A: def __init__(self): self.a = 12 self.b = 34 self.__slots__ = self.__dict__ a = A() a.b = 33 # okay a.c = 22 # AttributeError: 'A' object has no attribute 'c'