Запретить создание новых атрибутов вне __init__

#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'
 

Примечания:

  1. Начиная с Python 3.3, преимущество оптимизации потребления пространства уже не так впечатляет. В Python 3.3 словари совместного использования ключей используются для хранения объектов. Атрибуты экземпляров способны совместно использовать часть своего внутреннего хранилища друг с другом, то есть ту часть, в которой хранятся ключи и соответствующие им хэши. Это помогает уменьшить потребление памяти программами, которые создают много экземпляров не встроенных типов. Но все же это способ избежать динамически создаваемых атрибутов.
  1. Использование слотов также связано с его собственной стоимостью. Это нарушит сериализацию (например, рассол). Это также приведет к нарушению множественного наследования. Класс не может наследовать от более чем одного класса, который либо определяет слоты, либо имеет макет экземпляра, определенный в коде 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__ , by self.__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