Могут ли экземпляры получить собственную копию изменяемых значений по умолчанию на уровне класса без использования __init__? - PullRequest
1 голос
/ 09 марта 2012

Я пишу метакласс, чтобы сделать что-то классное, и часть его обработки заключается в проверке существования определенных атрибутов при создании класса.Некоторые из них являются изменяемыми и обычно устанавливаются в __init__, но поскольку __init__ не запускается до создания экземпляра, метакласс не будет знать, что атрибут будет создан , ивыдает ошибку.Я мог бы сделать что-то вроде:

class Test(meta=Meta):
    mutable = None
    def __init__(self):
        self.mutable = list()

Но у этого подхода есть несколько проблем:

  • он вызывает создание атрибута класса, который не совпадает с типом атрибута экземпляра
  • __init__ все еще должен скрывать атрибут класса, и Meta не проверяет, что
  • , если __init__ не затеняет атрибут класса, я все равно получу ошибки в конце строки(например, AttributeError: 'NoneType' object has no attribute 'append'), и попытка избежать таких ошибок является частью функции Meta
  • и, что не менее важно, она нарушает DRY.

Что янужен способ получить что-то вроде:

class Test(metaclass=Meta):
    mutable = list()

Но каждый экземпляр получит свою собственную копию mutable.

Цели разработки:

  • атрибут должен существовать в классе (тип не имеет значения - он не проверяется)
  • в какой-то момент раньшеатрибут используется в первый раз, копия атрибута помещается в экземпляр

Требуемый пример выполнения:

t1 = Test()
t2 = Test()
t1.mutable.append('one')
t2.mutable.append('two')
t1.mutable  # prints ['one']
t2.mutable  # prints ['two']

Есть какие-нибудь идеи о том, как этого можно достичь?

1 Ответ

1 голос
/ 09 марта 2012

Есть как минимум три способа сделать это:

  1. Пусть метакласс проверяет все атрибуты и, если они являются одним из изменяемых (list, dict, set и т. Д.), Заменяет атрибут дескриптором, который будет активирован при первом доступе и обновите экземпляр новой копией изменяемой версии.

  2. Укажите дескриптор из (1) в качестве декоратора, который будет использоваться при написании класса.

  3. Пусть метакласс добавит свой собственный метод __init__ в класс, который при запуске:
    • называет оригинал __init__
    • затем проверяет наличие обязательных атрибутов

Минусы (по методике):

  1. Дополнительные усилия требуются, если у класса есть изменяемый атрибут, который должен быть общим для всех экземпляров.

  2. Атрибут в классе становится функцией в классе (возможно, деформация ума;)

  3. Переместить точку ошибки в экземпляр класса вместо определения класса.

Я предпочитаю (2), он дает полный контроль автору класса, упрощает те случаи, когда изменяемый атрибут уровня класса должен быть общим для всех экземпляров, и сохраняет ошибку при определении класса.

Вот дескриптор-декоратор:

class ReplaceMutable:
    def __init__(self, func):
        self.func = func
    def __call__(self):
        return self
    def __get__(self, instance, owner):
        if instance is None:
            return self
        result = self.func()
        setattr(instance, self.func.__name__, result)
        return result

и тестовый класс:

class Test:
    @ReplaceMutable
    def mutable():
        return list()

Как это работает:

Так же, как property, ReplaceMutable является объектом дескриптора с тем же именем, что и атрибут, который он заменяет. В отличие от property, он не определяет ни __set__, ни __delete__, поэтому, когда код пытается перепривязать имя (mutable в приведенном выше тесте) в экземпляре Python, ему это разрешается. Эта же идея лежит в основе дескрипторов кэширования.

ReplaceMutable украшает функцию (с именем требуемого атрибута), которая просто возвращает то, с чем должен быть инициализирован атрибут уровня экземпляра (пустой list в примере выше). Поэтому при первом поиске атрибута в экземпляре он не будет найден в словаре экземпляров, и Python активирует дескриптор; дескриптор затем вызывает функцию для извлечения исходного объекта / данных / чего угодно, сохраняет его в экземпляре и затем возвращает его. При следующем обращении к этому атрибуту в этом экземпляре он будет в словаре экземпляра, и именно это будет использоваться.

Пример кода:

t1 = Test()
t2 = Test()
t1.mutable.append('one')
t2.mutable.append('two')
print(t1.mutable)
print(t2.mutable)
...