Запретить создание новых атрибутов за пределами __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__
, но мне было интересно, есть ли "правильный" способ сделать это?
15 ответов
Я бы не использовал __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
Слоты это путь:
Питонический способ состоит в том, чтобы использовать слоты вместо игры с __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"
=> Не удается создать атрибут "новый":
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).
Если кто-то заинтересован сделать это с помощью декоратора, вот рабочее решение:
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)
pystrict
- это устанавливаемый декоратор pypi, вдохновленный этим вопросом stackru, который можно использовать с классами для их замораживания. В README есть пример, который показывает, зачем нужен такой декоратор, даже если в вашем проекте запущены mypy и pylint:
pip install pystrict
Тогда просто используйте декоратор @strict:
from pystrict import strict
@strict
class Blah
def __init__(self):
self.attr = 1
@dataclass(slots=True)
Нирвана (Питон 3.10)
Я влюблен в это дело :
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): self.n = n self.s = s
-
__slots__ = ['n', 's']
Другие бесплатные вещи, не показанные в этом примере:
-
__str__
-
__eq__
: Сравните экземпляры объектов на предмет равенства по их атрибутам. -
__hash__
если вы также используетеfrozen=True
: объект пользовательского типа в качестве словарного ключа
Протестировано на Python 3.10.7, Ubuntu 22.10.
Вот подход, который я придумал, который не нуждается в атрибуте или методе _frozen для freeze() в init.
Во время инициализации я просто добавляю все атрибуты класса к экземпляру.
Мне нравится это, потому что нет _frozen, freeze(), и _frozen также не отображается в выводе vars(экземпляр).
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
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
Мне нравится "Frozen" Йохена Ритцеля. Неудобно, что переменная 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
Ни в одном из ответов не упоминается влияние переопределения на производительность
__setattr__
, что может быть проблемой при создании большого количества небольших объектов. (И
__slots__
будет эффективным решением, но ограничивает рассол / наследование).
Итак, я придумал этот вариант, который устанавливает наш более медленный установщик после инициализации:
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): ...
Если ты не хочешь звонить
freeze
в конце
__init__
, если наследование является проблемой, или если вы не хотите, чтобы это происходило
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: ...
Я написал 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
Улучшение по сравнению с этим отличным решением с помощью __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'