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

82

Я хочу иметь возможность создать класс (в 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__но это, вероятно, было бы взломано.
aaronasterling
Я не понимаю, почему это взломано? Это лучшее решение, которое я мог придумать, и оно намного более лаконично, чем некоторые из других предложенных.
Chris B

Ответы:

81

Я бы не стал использовать __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
Йохен Ритцель
источник
Очень круто! Думаю, я возьму этот фрагмент кода и начну его использовать. (Хм, интересно, можно ли это сделать в качестве декоратора, или это будет плохой идеей ...)
Вероника
5
Поздний комментарий: я успешно использовал этот рецепт в течение некоторого времени, пока я не изменил атрибут на свойство, где геттер генерировал NotImplementedError. Мне потребовалось много времени, чтобы выяснить, что это произошло из-за того, что hasattrпри вызовах getattractall результат удаляется и в случае ошибок возвращается False, см. Этот блог . Нашел обходной путь, заменив not hasattr(self, key)на key not in dir(self). Это могло быть медленнее, но решило проблему для меня.
Bas Swinckels 03
31

Если кому-то интересно сделать это с помощью декоратора, вот рабочее решение:

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 за версию декоратора. Это то, что я бы использовал для более крупного проекта, в более крупном сценарии это перебор (возможно, если бы он был в стандартной библиотеке ...). Пока есть только «предупреждения в стиле IDE».
Томаш Гандор
2
Как это решение работает с наследием? например, если у меня есть дочерний класс Foo, этот дочерний класс по умолчанию является замороженным?
mrgiesel
Есть ли пакет pypi для этого декоратора?
winni2k
Как можно улучшить декоратор, чтобы он работал для унаследованных классов?
Иван Нечипайко
30

Слоты - это то, что вам нужно:

Питонический способ - использовать слоты вместо игры с __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:

  1. Начиная с Python 3.3, преимущество оптимизации использования пространства уже не так впечатляюще. В Python 3.3 словари совместного использования ключей используются для хранения объектов. Атрибуты экземпляров могут совместно использовать часть своего внутреннего хранилища между собой, то есть часть, в которой хранятся ключи и их соответствующие хэши. Это помогает снизить потребление памяти программами, которые создают множество экземпляров не встроенных типов. Но все же это способ избежать динамически создаваемых атрибутов.
  1. Использование слотов также требует собственных затрат. Это нарушит сериализацию (например, рассол). Это также нарушит множественное наследование. Класс не может наследовать более чем от одного класса, который либо определяет слоты, либо имеет макет экземпляра, определенный в коде C (например, list, tuple или int).
DhiaTN
источник
20

Собственно, не хочешь __setattr__, хочешь __slots__. Добавьте __slots__ = ('foo', 'bar', 'baz')в тело класса, и Python позаботится о том, чтобы в любом экземпляре были только foo, bar и baz. Но прочтите списки документации!


источник
12
Использование __slots__работает, но, помимо прочего, это нарушит сериализацию (например, рассол) ... Как правило, использование слотов для управления созданием атрибутов - плохая идея, вместо того, чтобы уменьшать накладные расходы памяти, на мой взгляд, в любом случае ...
Джо Кингтон
Я знаю, и я не решаюсь использовать его сам - но делать дополнительную работу, чтобы запретить новые атрибуты, тоже обычно плохая идея;)
2
Использование __slots__также нарушает множественное наследование. Класс не может наследовать более чем от одного класса, который определяет слоты или имеет макет экземпляра, определенный в коде C (например list, tupleили int).
Feuermurmel 01
Если __slots__соления ломаются, значит, вы используете древний протокол. Перейдите protocol=-1к методам pickle для самого последнего доступного протокола, который равен 2 в Python 2 ( представлен в 2003 году ). Протоколы по умолчанию и последние версии в Python 3 (3 и 4 соответственно) обрабатывают __slots__.
Ник Маттео,
ну, в большинстве случаев я вообще сожалею об
Erik Aronesty
7

Правильный способ - переопределить __setattr__. Вот для чего он нужен.

Катриэль
источник
Каков тогда правильный способ установки переменных __init__? Чтобы установить их __dict__напрямую?
astrofrog
1
Я хотел бы переопределить __setattr__в __init__, по self.__setattr__ = <new-function-that-you-just-defined>.
Катриэль, 01
6
@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
Эран Фридман
источник
4

Что насчет этого:

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)
Clementerf
источник
2

Вот подход, который я придумал, для которого не нужен атрибут или метод _frozen для freeze () в 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
gswilcox01
источник
Это не работает, если одно из полей является списком. Скажем так names=[]. Потом d.names.append['Fido']вставлю 'Fido'в оба d.namesи e.names. Я недостаточно знаю Python, чтобы понять, почему.
Рейнир Торенбек
2

pystrict- это устанавливаемый декоратор pypi, вдохновленный этим вопросом stackoverflow, который можно использовать с классами для их замораживания. В README есть пример, который показывает, зачем нужен такой декоратор, даже если в вашем проекте запущены mypy и pylint:

pip install pystrict

Тогда просто используйте декоратор @strict:

from pystrict import strict

@strict
class Blah
  def __init__(self):
     self.attr = 1
Эрик Аронести
источник
1

Мне нравится «Холодное сердце» Йохена Ритцеля. Неудобно то, что переменная isfrozen затем появляется при печати Class .__ 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
Артур Бовиль
источник
1

FrozenClassЙохен RITZEL это круто, но называть , _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
Endle_Zhenbo
источник