Запретить создание новых атрибутов за пределами __init__ - PullRequest
59 голосов
/ 30 августа 2010

Я хочу иметь возможность создать класс (в 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 ]

66 голосов
/ 31 августа 2010

Я бы не стал использовать __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
25 голосов
/ 31 марта 2015

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

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
18 голосов
/ 30 августа 2010

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

14 голосов
/ 08 января 2018

Слоты - это путь:

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

Примечание:

  1. Начиная с Python 3.3 преимущество, оптимизирующее потребление пространства, уже не так впечатляет. В Python 3.3 Key-Sharing Словари используются для хранения объектов. Атрибуты экземпляров могут совместно использовать часть своего внутреннего хранилища между собой, то есть часть, в которой хранятся ключи и соответствующие им хэши. Это помогает уменьшить потребление памяти программами, которые создают множество экземпляров не встроенных типов. Но все же есть способ избежать динамически создаваемых атрибутов.

  2. Использование слотов также идет за свой счет. Это нарушит сериализацию (например, рассол). Это также нарушит множественное наследование. Класс не может наследоваться от более чем одного класса, который либо определяет слоты, либо имеет макет экземпляра, определенный в коде C (например, list, tuple или int).

6 голосов
/ 30 августа 2010

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

4 голосов
/ 27 апреля 2016

Мне очень нравится решение, которое использует декоратор, потому что его легко использовать для многих классов в проекте, с минимальными добавлениями для каждого класса. Но это не работает с наследованием. Итак, вот моя версия: она переопределяет только функцию __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
3 голосов
/ 24 июня 2016

Что по этому поводу:

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)
2 голосов
/ 19 февраля 2016

Вот подход, который я придумал, для которого не требуется атрибут или метод _frozen для freeze () в 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
1 голос
/ 31 августа 2016

Jochen Ritzel 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
1 голос
/ 06 июня 2016

Мне нравится "Замороженный" Йохена Ритцеля. Неудобно, что при печати Class появляется переменная isfrozen .__ dict Я решил эту проблему, создав список разрешенных атрибутов (аналогично slots ):

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
...