Предотвратить создание новых атрибутов вне функции 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__, но мне было интересно, есть ли "правильный" способ сделать это?

11   53  

11 ответов:

Я бы не стал использовать __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

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

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

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

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

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

=> не удается создать атрибут "new":

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 Ключ-Обмен словари используются для хранения объектов. Атрибуты экземпляров способны совместно использовать часть их внутренней памяти между собой, т. е. часть, которая хранит ключи и их соответствующие хэши. Это помогает уменьшить потребление памяти программ, которые создают много экземпляров не встроенных типов. Но все же это способ избежать динамически создаваемых атрибутов.

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

правильный способ-переопределить __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

вот подход, который я придумал, что не нужен атрибут _frozen или метод для замораживания () в 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

Как насчет этого:

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)

Мне нравится "замороженный" Йохен Ритцель. Неудобно то, что 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

The FrozenClass по Йохен Ритцель это круто, но вызов _frozen() при инициализации класса каждый раз не так круто (и вам нужно рискнуть забыть об этом). Я добавил :

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

улучшение по сравнению с этим отличное решение используя __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'