Как автоматически зарегистрировать класс, когда он определен - PullRequest
36 голосов
/ 04 марта 2011

Я хочу, чтобы экземпляр класса был зарегистрирован, когда класс определен.В идеале приведенный ниже код справился бы с задачей.

registry = {}

def register( cls ):
   registry[cls.__name__] = cls() #problem here
   return cls

@register
class MyClass( Base ):
   def __init__(self):
      super( MyClass, self ).__init__() 

К сожалению, этот код генерирует ошибку NameError: global name 'MyClass' is not defined.

Что происходит в строке #problem here, которую я пытаюсьсоздать экземпляр MyClass, но декоратор еще не вернулся, поэтому его не существует.

Как-то обойтись с помощью метаклассов или чего-то еще?

Ответы [ 5 ]

42 голосов
/ 04 марта 2011

Да, мета-классы могут сделать это. Метод мета-класса '__new__ возвращает класс, поэтому просто зарегистрируйте этот класс, прежде чем возвращать его.

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class MyClass(object):
    __metaclass__ = MetaClass

Предыдущий пример работает в Python 2.x. В Python 3.x определение MyClass немного отличается (хотя MetaClass не отображается, потому что оно не изменилось - за исключением того, что super(MetaClass, cls) может стать super(), если хотите):

#Python 3.x

class MyClass(metaclass=MetaClass):
    pass

Начиная с Python 3.6, существует также новый метод __init_subclass__ (см. PEP 487 ), который можно использовать вместо мета-класса (спасибо @matusko за ответ ниже):

class ParentClass:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        register(cls)

class MyClass(ParentClass):
    pass

[править: исправлено отсутствие cls аргумент super().__new__()]

[править: добавлен пример Python 3.x]

[править: исправлен порядок аргументов в super () и улучшено описание различий в 3.x]

[править: добавить Python 3.6 __init_subclass__ пример]

16 голосов
/ 30 апреля 2018

Начиная с Python 3.6, вам не нужны метаклассы для решения этой проблемы

В Python 3.6 была введена более простая настройка создания класса ( PEP 487 ).

Хук __init_subclass__, который инициализирует все подклассы данного класса.

Предложение включает в себя следующий пример регистрации подкласса

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

В этом примере PluginBase.subclasses будет содержать простой список все подклассы во всем дереве наследования. Следует отметить, что это также хорошо работает как класс mixin.

11 голосов
/ 08 июня 2012

Проблема на самом деле не в указанной вами строке, а в вызове super в методе __init__. Проблема остается, если вы используете метакласс, как предложено dappawit; Причина, по которой пример из этого ответа работает, заключается в том, что dappawit упростил ваш пример, пропустив класс Base и, следовательно, вызов super. В следующем примере ни ClassWithMeta, ни DecoratedClass не работают:

registry = {}
def register(cls):
    registry[cls.__name__] = cls()
    return cls

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class Base(object):
    pass


class ClassWithMeta(Base):
    __metaclass__ = MetaClass

    def __init__(self):
        super(ClassWithMeta, self).__init__()


@register
class DecoratedClass(Base):
    def __init__(self):
        super(DecoratedClass, self).__init__()

Проблема одинакова в обоих случаях; функция register вызывается (либо метаклассом, либо напрямую в качестве декоратора) после создания объекта класса , но до того, как он был связан с именем. Именно здесь super становится грубым (в Python 2.x), потому что для этого требуется, чтобы вы обращались к классу в вызове super, что вы можете только разумно сделать, используя глобальное имя и полагая, что оно будет привязан к этому имени к тому времени, когда вызывается вызов super. В этом случае это доверие неуместно.

Я думаю, что метакласс здесь неправильное решение. Метаклассы предназначены для создания семейства классов , имеющих общее пользовательское поведение, точно так же, как классы предназначены для создания семейства экземпляров, имеющих общее пользовательское поведение. Все, что вы делаете, это вызываете функцию в классе. Вы не определили бы класс для вызова функции в строке, также вы не должны определять метакласс для вызова функции в классе.

Итак, проблема заключается в фундаментальной несовместимости между: (1) использованием хуков в процессе создания класса для создания экземпляров класса и (2) использованием super.

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

Другой способ - не подключать register к созданию класса. Добавление register(MyClass) после каждого из ваших определений классов довольно эквивалентно добавлению @register перед ними или __metaclass__ = Registered (или как вы называете метакласс) в них. Строка внизу намного менее самодокументирована, чем хорошее объявление вверху класса, так что это не очень хорошо, но опять же это может быть разумным решением.

Наконец, вы можете обратиться к неприятным взломам, которые, вероятно, будут работать. Проблема в том, что имя ищется в глобальной области видимости модуля непосредственно перед тем, как оно там было связано. Чтобы вы могли обмануть, следующим образом:

def register(cls):
    name = cls.__name__
    force_bound = False
    if '__init__' in cls.__dict__:
        cls.__init__.func_globals[name] = cls
        force_bound = True
    try:
        registry[name] = cls()
    finally:
        if force_bound:
            del cls.__init__.func_globals[name]
    return cls

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

  1. Сначала мы проверим, находится ли __init__ в cls.__dict__ (в отличие от того, имеет ли он атрибут __init__, который всегда будет истинным). Если он унаследовал метод __init__ от другого класса, мы, вероятно, в порядке (потому что суперкласс будет уже связан с его именем обычным образом), и волшебство, которое мы собираемся сделать, не ' t работает на object.__init__, поэтому мы хотим избежать попытки, если класс использует значение по умолчанию __init__.
  2. Мы ищем метод __init__ и берем его словарь func_globals, в котором будут выполняться глобальные поиски (например, для поиска класса, на который ссылается вызов super). Обычно это глобальный словарь модуля, в котором изначально был определен метод __init__. Такой словарь - около , чтобы вставить cls.__name__ в него, как только вернется register, поэтому мы просто вставляем его сами по себе.
  3. Наконец мы создаем экземпляр и вставляем его в реестр. Это в блоке try / finally, чтобы убедиться, что мы удалили созданную нами привязку, независимо от того, создает ли экземпляр исключение; это вряд ли необходимо (так как 99,999% времени имя все равно будет отскочить), но лучше держать такую ​​странную магию как можно более изолированной, чтобы минимизировать вероятность того, что когда-нибудь другая странная магия плохо взаимодействует с это.

Эта версия register будет работать независимо от того, вызывается ли она в качестве декоратора или метаклассом (что я до сих пор считаю неправильным использованием метакласса). Однако есть несколько неясных случаев, когда он потерпит неудачу:

  1. Я могу представить себе странный класс, который не имеет метод __init__, но наследует тот, который вызывает self.someMethod, и someMethod переопределяется в определяемом классе и создает super вызов. Вероятно, вряд ли.
  2. Метод __init__ мог быть первоначально определен в другом модуле, а затем использован в классе, выполнив __init__ = externally_defined_function в блоке класса. Тем не менее, атрибут func_globals другого модуля означает, что наша временная привязка сожжет любое определение имени этого класса в этом модуле (упс). Опять маловероятно.
  3. Возможно, другие странные случаи, о которых я даже не думал.

Вы можете попытаться добавить больше хаков, чтобы сделать их немного более надежными в этих ситуациях, но природа Python заключается в том, что такие хаки возможны и что невозможно сделать их абсолютно пуленепробиваемыми.

1 голос
/ 04 марта 2011

Вызов прямого класса Base должен работать (вместо использования super ()):

  def __init__(self):
        Base.__init__(self)
0 голосов
/ 21 марта 2018

Ответы здесь не работали для меня в python3 , потому что __metaclass__ не работал.

Вот мой код, регистрирующий все подклассы класса во время их определения:

registered_models = set()

class RegisteredModel(type):
    def __new__(cls, clsname, superclasses, attributedict):
        newclass = type.__new__(cls, clsname, superclasses, attributedict)
        # condition to prevent base class registration
        if superclasses:
            registered_models.add(newclass)
        return newclass

class CustomDBModel(metaclass=RegisteredModel):
    pass

class BlogpostModel(CustomDBModel):
    pass

class CommentModel(CustomDBModel):
   pass

# prints out {<class '__main__.BlogpostModel'>, <class '__main__.CommentModel'>}
print(registered_models)
...