Не допускати створення нових атрибутів за межами __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

Дуже круто! Думаю, я схоплю цю частинку коду і почну її використовувати. (Хм, цікаво, чи можна це зробити як декоратор, чи це не буде гарною ідеєю ...)
weronika

5
Пізній коментар: Я деякий час успішно використовував цей рецепт, поки не змінив атрибут на властивість, де отримувач піднімав NotImplementedError. Мені знадобилося багато часу, щоб з’ясувати, що це пов’язано з тим, що фактичні hasattrдзвінки getattrвідкидають результат і повертають False у разі помилок, див. Цей блог . Знайшов обхідний шлях, замінивши not hasattr(self, key)на key not in dir(self). Це може бути повільніше, але вирішило проблему для мене.
Bas Swinckels,

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"

=> Не вдається створити атрибут "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'

Примітка:

  1. З мовою Python 3.3 перевага в оптимізації споживання місця вже не така вражаюча. У Python 3.3 Словники спільного використання ключів використовуються для зберігання об’єктів. Атрибути примірників можуть спільно використовувати частину свого внутрішнього сховища між собою, тобто частину, яка зберігає ключі та відповідні їм хеші. Це допомагає зменшити споживання пам'яті програмами, які створюють багато екземплярів невбудованих типів. Але все-таки це шлях, щоб уникнути динамічно створених атрибутів.
  1. Використання слотів також постачається за власну вартість. Це порушить серіалізацію (наприклад, соління). Це також порушить багаторазове успадкування. Клас не може успадкувати більше ніж одного класу, який або визначає слоти, або має макет екземпляра, визначений у коді С (наприклад, список, кортеж або int).

20

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


12
Використовуючи __slots__працює, але це порушить серіалізацію (наприклад, розсолення), серед іншого ... Як правило, це погана ідея використовувати слоти для управління створенням атрибутів, а не зменшувати витрати пам'яті, на мій погляд, у будь-якому випадку ...
Джо Кінгтон

Я знаю, і я не поспішаю використовувати його сам, але робити додаткову роботу, щоб заборонити нові атрибути, теж теж погана ідея;)

2
Використання __slots__також порушує множинне успадкування. Клас не може успадковуватись від більш ніж одного класу, який або визначає слоти, або розміщує макет екземпляра, визначений у коді C (наприклад list, tupleабо int).
Feuermurmel

Якщо __slots__ви ламаєте свої соління, ви використовуєте стародавній протокол соління. Перейдіть protocol=-1до методів розсолювання для останнього доступного протоколу, тобто 2 у Python 2 ( представлений у 2003 році ) Обробляються і протоколи за замовчуванням, і останні протоколи в Python 3 (3 та 4 відповідно) __slots__.
Нік Маттео

ну, більшу частину часу я з жалем взагалі використовую мариновані огірки: benfrederickson.com/dont-pickle-your-data
Ерік Аронесті

7

Правильний спосіб - перевизначити __setattr__. Ось для чого це там.


Який тоді правильний спосіб встановлювати змінні __init__? Це встановити їх __dict__безпосередньо?
astrofrog

1
Я хотів би перевизначити __setattr__в __init__, по self.__setattr__ = <new-function-that-you-just-defined>.
Катріель

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)

2

Ось такий підхід, який я придумав, не потребує атрибута _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

Здається, це не працює, якщо одне з полів є списком. Скажімо names=[]. Потім d.names.append['Fido']вставить 'Fido'в обидва d.namesі e.names. Я недостатньо знаю про Python, щоб зрозуміти, чому.
Reinier Torenbeek

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 потім з’являється під час друку класу .__ 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.