#python #python-3.x #class #attributes
#python #python-3.x #класс #ооп #python-datamodel
Вопрос:
Я хочу иметь возможность создавать класс (на 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__
, но мне было интересно, есть ли «правильный» способ сделать это?
Комментарии:
1. katrielalex приносит хорошие моменты. В этом нет ничего хакерского. Вы могли бы избежать использования
__setattr__
, но это, вероятно, было бы хакерским.2. Я не понимаю, почему это халтурно? Это лучшее решение, которое я мог бы придумать, и оно намного более лаконично, чем некоторые другие предложенные.
Ответ №1:
Я бы не стал использовать __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
Комментарии:
1. Очень круто! Я думаю, что возьму этот фрагмент кода и начну его использовать. (Хм, интересно, можно ли это сделать как декоратор, или это не было бы хорошей идеей …)
2. Последний комментарий: я успешно использовал этот рецепт в течение некоторого времени, пока не изменил атрибут на свойство, в котором получатель вызывал NotImplementedError . Мне потребовалось много времени, чтобы выяснить, что это связано с тем, что
hasattr
actuall вызываетgetattr
, удаляет результат и возвращает False в случае ошибок, см. Этот блог . Найдено обходное решение путем заменыnot hasattr(self, key)
наkey not in dir(self)
. Это может быть медленнее, но решило проблему для меня.3. Есть ли какой-либо другой способ, которым мы можем применить ту же технику к большему количеству подклассов? Например
__init__
, метод нового подкласса `Test2 (Test)? Единственное решение, которое я придумал, — это разморозить класс в начале инициализации и заморозить его в конце. Но это полный бардак. У вас есть еще какие-нибудь умные идеи?
Ответ №2:
Слоты — это путь:
Способ pythonic заключается в использовании слотов вместо того, чтобы играть с __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"
=> Не удается создать атрибут «новый»:
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'
Примечания:
- Начиная с Python 3.3, преимущество оптимизации потребления пространства уже не так впечатляет. В Python 3.3 словари совместного использования ключей используются для хранения объектов. Атрибуты экземпляров способны совместно использовать часть своего внутреннего хранилища друг с другом, то есть ту часть, в которой хранятся ключи и соответствующие им хэши. Это помогает уменьшить потребление памяти программами, которые создают много экземпляров не встроенных типов. Но все же это способ избежать динамически создаваемых атрибутов.
- Использование слотов также связано с его собственной стоимостью. Это нарушит сериализацию (например, рассол). Это также приведет к нарушению множественного наследования. Класс не может наследовать от более чем одного класса, который либо определяет слоты, либо имеет макет экземпляра, определенный в коде C (например, list, tuple или int).
Ответ №3:
Если кто-то заинтересован в том, чтобы сделать это с помощью декоратора, вот рабочее решение:
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
Комментарии:
1. 1 для версии декоратора. Это то, что я бы использовал для более крупного проекта, в более крупном сценарии это излишне (возможно, если бы у них это было в стандартной библиотеке …). На данный момент есть только «предупреждения в стиле IDE».
2. Как это решение работает с наследием? например, если у меня есть дочерний класс Foo, этот дочерний класс по умолчанию является замороженным классом?
3. Есть ли пакет pypi для этого декоратора?
4. Как можно улучшить декоратор, чтобы он работал для унаследованных классов?
Ответ №4:
На самом деле, вы не хотите __setattr__
, вы хотите __slots__
. Добавьте __slots__ = ('foo', 'bar', 'baz')
в тело класса, и Python позаботится о том, чтобы в любом экземпляре были только foo, bar и baz. Но прочитайте предостережения в списках документации!
Комментарии:
1. Использование
__slots__
работает, но, помимо прочего, это приведет к нарушению сериализации (например, pickle)… В любом случае, на мой взгляд, обычно использовать слоты для управления созданием атрибутов — плохая идея, а не уменьшать нагрузку на память…2. Я знаю, и я не решаюсь использовать его сам, но выполнение дополнительной работы по запрещению новых атрибутов, как правило, тоже плохая идея 😉
3. Использование
__slots__
также нарушает множественное наследование. Класс не может наследовать от более чем одного класса, который либо определяет слоты , либо закрывает макет экземпляра, определенный в коде C (напримерlist
,tuple
илиint
).4. Если
__slots__
у вас ломаются соленые огурцы, значит, вы используете древний протокол приготовления маринадов. Перейдитеprotocol=-1
к методам pickle для самого последнего доступного протокола, который равен 2 в Python 2 ( представлен в 2003 году). Стандартные и последние протоколы в Python 3 (3 и 4 соответственно) обрабатываются обоими__slots__
.5. ну, в большинстве случаев я в конечном итоге сожалею о том, что вообще использовал маринованный огурец: benfrederickson.com/dont-pickle-your-data
Ответ №5:
Правильный способ — это переопределить __setattr__
. Вот для чего он существует.
Комментарии:
1. Каков же тогда правильный способ установки переменных
__init__
? Это для того, чтобы установить их__dict__
напрямую?2. Я бы переопределил
__setattr__
in__init__
, byself.__setattr__ = <new-function-that-you-just-defined>
.3. @katrielalex: это не будет работать для классов нового стиля, поскольку
__xxx__
методы просматриваются только в классе, а не в экземпляре.
Ответ №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
Ответ №7:
Как насчет этого:
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)
Ответ №8:
Вот подход, который я придумал, для которого не требуется атрибут _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
Комментарии:
1. Похоже, это не работает, если одно из полей является списком. Допустим
names=[]
. Затемd.names.append['Fido']
будет вставлено'Fido'
в обаd.names
иe.names
. Я недостаточно знаю о Python, чтобы понять, почему.
Ответ №9:
Мне нравится «Замороженный» Йохен Ритцель. Неудобно то, что переменная 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
Ответ №10:
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
Ответ №11:
Ни в одном из ответов не упоминается влияние переопределения на производительность __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): ...
Если вы не хотите вызывать 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: ...
Ответ №12:
Я написал 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