Python3.5: инициализация класса с использованием наследования

Недавно я наткнулся на метаклассы в Python и решил использовать их, чтобы упростить некоторые функции. (Используя Python 3.5)

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

Я могу легко зарегистрировать класс:

class MetaComponent(type):
    def __init__(cls, *args, **kargs):
        super().__init__(*args, **kargs)
        RegisterComponent(cls)

class BaseComponent(metaclass=MetaComponent):
    pass

class Component(BaseComponent):
    """This is the actual class to use when writing components"""

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

Но классы не имеют возможности инициализировать себя (по крайней мере, в Python 3.5) и могут вызвать некоторые проблемы, например:

class Manager(Component):
    SubManagers = []

    @classmethod
    def ListSubManagers(cls):
        for manager in cls.SubManagers:
            print(manager)

    @classmethod
    def RegisterSubManager(cls, manager):
        cls.SubManagers.append(manager)
        return manager

@Manager1.RegisterSubManager
class Manager2(Manager):
    pass

@Manager2.RegisterSubManager
class Manager3(Manager):
    pass

# Now the fun:
Manager1.ListSubManagers()

# Displays:
# > Manager2
# > Manager3

Теперь это проблема, потому что идея заключалась в том, чтобы иметь уникальный список подчиненных менеджеров для каждого менеджера. Но поле SubManager является общим для всех подклассов... Таким образом, добавление в один список добавляет к каждому. РВАТЬ.

Поэтому следующей идеей было реализовать своего рода инициализатор:

class BaseManager(Component):
    @classmethod
    def classinit(cls):
        cls.SubManagers = []

Но теперь мне нужен способ вызвать этот метод после создания класса. Итак, давайте снова сделаем это с метаклассами:

class MetaComponent(type):
    def __init__(cls, *args, **kargs):
        super().__init__(*args, **kargs):
        cls.classinit(**kargs)
        RegisterComponent(cls)

class BaseComponent(metaclass=MetaComponent):
    @classmethod
    def classinit(cls, **kargs):
        print('BASE', cls)

class Component(BaseComponent):
    @classmethod
    def classinit(cls, **kargs):
        super().classinit(**kargs) # Being able to use super() is the goal
        print('COMPONENT', cls)

Я бы считал, что с этим покончено. Несколько элегантный способ сделать это ИМО. classinit() будет вызываться из каждого создаваемого класса (в отличие от 3.6 __init_subclass__, который вызывается для родителя). По крайней мере, мне это нравилось, пока Python 3.5 не заплакал в RuntimeError: super(): empty __class__ cell...

Я читал, что это было потому, что я вызывал метод из метода __init__ метакласса, и хотя класс был создан (отсюда мое желание поместить код в __init__, чтобы инициализировать что-то уже созданное), ему не хватает этой ячейки __class__, по крайней мере, в тот момент ...

Я попытался запустить точно такой же код в Python 3.6, и это сработало, поэтому я предполагаю, что что-то было не так, но это было исправлено...

Мои настоящие вопросы:

  • Можем ли мы действительно инициализировать классы с помощью метаклассов в Python 3.5?
  • Есть ли способ избежать ограничения использования super() в процедуре инициализации?
  • Почему это работает в 3.6?
  • Если все потерпит неудачу, как лучше всего по-прежнему обеспечивать инициализацию класса и разрешать вызовы super(...)? (Например, мне нужно явно ссылаться на суперкласс?)

Заранее благодарны за Вашу помощь.

ИЗМЕНИТЬ:

Цель состоит в том, чтобы иметь возможность создавать компоненты и иметь возможность инициализировать каждый класс относительно его родителя "простым" способом:

class Manager(Component):
    def classinit(cls, **kargs):
        cls.SubManagers = []

    @classmethod
    def RegisterSubManager(cls, manager):
        cls.SubManagers.append(manager)
        return manager

@Manager.RegisterSubManager
class EventManager(Manager):
    def classinit(cls, **kargs):
        super().classinit(**kargs) # keep the old behaviour
        cls.Events = []

    # ...

@EventManager.RegisterSubManager
class InputManager(EventManager):
    def classinit(cls, **kargs):
        super().classinit(**kargs) # again, keep old behaviour
        cls.Inputs = []

    # use parts of EventManager, but define specialized methods
    # for input management

Менеджеры - это одна проблема, у меня есть несколько концепций, которые зависят от компонентов и их способности инициализировать свой класс.


person WKnight02    schedule 24.08.2017    source источник
comment
Я не понимаю, как @Manager2.RegisterSubManager может работать, поскольку Manager2 не является подклассом Manager1.   -  person Blckknght    schedule 25.08.2017
comment
Моя ошибка, исправил   -  person WKnight02    schedule 25.08.2017


Ответы (3)


TL;DR — у вас действительно будет RuntimeError: super(): empty __class__ cell..., если вы попытаетесь использовать пустой вызов super из методов метакласса __new__ или __init__: на этом этапе неявная «магическая» переменная __class__, которая super используется внутри, еще не создана. (Проверив это, я только что узнал, что это было исправлено в Python 3.6, то есть: методы классов, использующие super без параметров, могут быть вызваны из метакласса __init__ в Python 3.6, но дают эту ошибку в 3.5)

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

--

Ваша предпоследняя идея об использовании методов классов в качестве декораторов классов для регистрации может быть реализована путем автоматического создания атрибута SubManagers с метаклассом с помощью простого преобразования имен Python для автоматического создания уникального атрибута SubManagers каждого класса менеджера путем проверки одного собственное пространство имен класса в его __dict__ (и это можно сделать и без метакласса)

Используя метаклассы, просто добавьте эти 2 строки в конец вашего метакласса __init__:

if getattr(cls, "SubManagers") and not "SubManagers" in cls.__dict__:
    cls.SubManagers = []

Если ваш подход к классу-декоратору в противном случае исключает метаклассы, вам не нужно использовать метакласс только для этого - измените свой метод регистрации, чтобы выполнить вышеуказанное создание "собственного" списка подменеджеров:

@classmethod
def RegisterSubManager(cls, manager):
   if not "SubManagers" in cls.__dict__:
       cls.SubManagers = []
   cls.SubManagers.append(manager)
person jsbueno    schedule 24.08.2017
comment
Благодарю за ваш ответ. Я больше искал обходной путь, если таковой имеется, чтобы разрешить автоматическое super()-подобное разрешение в подклассах, но я думаю, что буду использовать явный вызов родительского класса в подклассах. С другой стороны, декоратор регистрации предназначен только для менеджеров, а не для всех компонентов: разные компоненты работают как одноэлементные классы и могут нуждаться в разных инициализациях (записывать это звучит почти как не такая блестящая идея с использованием классов, как я, но в идеальном мире, таком как 3.6, это работает... но не в 3.5 :(). - person WKnight02; 25.08.2017
comment
Возможно, вы можете сделать простой декоратор класса только для вызова метода initclass — декоратор применяется после запуска метакласса, и тогда должен работать super(). - person jsbueno; 25.08.2017
comment
Делал это, пытался избежать этого, используя метаклассы :p. По крайней мере теперь регистрация компонента (не менеджера) работает без декораторов. Но я немного расстроен из-за этого ограничения на запуск методов в metaclass.__init__... А также из-за того, что в следующей версии Python (3.6) это было исправлено... Я действительно думал, что возможны какие-то обходные пути на чистом Python , даже самый близкий подход, который я нашел, был немного... Hardcore: stackoverflow.com/a/4885951/7983255. Будет ли путь по этой ссылке? (Я немного расстроен, должен сказать) - person WKnight02; 25.08.2017

Если вам нужно дополнительное поведение для ваших типов Manager, возможно, вы хотите, чтобы у них был свой собственный, более совершенный метакласс, а не просто тот, который они унаследовали от Component. Попробуйте написать метакласс MetaManager, который наследуется от MetaComponent. Вы даже можете переместить методы класса из Manager1 в метакласс (где они станут обычными методами):

class MetaManager(MetaComponent):
    def __init__(cls, *args, **kwargs):
        super().__init__(*args, **kwargs)
        cls.SubManagers = [] # each class gets its own SubManagers list

    def ListSubManagers(cls):
        for manager in cls.SubManagers:
            print(manager)

    def RegisterSubManager(cls, manager):
        cls.SubManagers.append(manager)
        return manager

class Manager(Component, metaclass=MetaManager): # inherit from this to get the metaclass
    pass

class Manager1(Manager):
    pass

@Manager1.RegisterSubManager
class Manager2(Manager):
    pass

@Manager2.RegisterSubManager
class Manager3(Manager):
    pass
person Blckknght    schedule 25.08.2017
comment
Мне нравится эта идея, но я хотел, чтобы конечный разработчик мог писать свои собственные менеджеры и свою собственную инициализацию (при этом ссылаясь на super().__init__, чтобы база cls.SubManagers тоже инициализировалась). Ваша идея работает, но она не обеспечивает модульности, которую я хотел, более конкретно, она немного усложняет ситуацию :). Спасибо, что уделили мне время, чтобы найти ответ! Я тоже все еще копаю, никогда не знаешь, что найдем! (На самом деле я теряю на этом немного времени, но как только это будет сделано, я раскрою довольно интересный момент о Python) - person WKnight02; 25.08.2017
comment
Хотя я никогда не думал о размещении методов класса в метаклассе, я нахожу это довольно крутым, это действительно кажется естественным! (Но я бы зарезервировал этот стиль определения для классов, которым абсолютно потребуются метаклассы, я не чувствую, что менеджеры в моем случае...) - person WKnight02; 25.08.2017
comment
Я не уверен, что понимаю ваше беспокойство. Как вы думаете, почему нельзя где-то использовать super? Вам не нужно выдумывать метод classinit, __init__ метакласса уже обрабатывает инициализацию списка подчиненных менеджеров. - person Blckknght; 26.08.2017
comment
В двух словах, у меня есть классы менеджеров, которые необходимо инициализировать, но каждый подкласс (class SubManagerX(Manager)) может захотеть инициализировать себя, как ванильный менеджер, плюс некоторую дополнительную инициализацию для себя. Итак, чтобы иметь возможность инициализировать подменеджеры, сохраняя при этом старое поведение, мне нужно было иметь возможность вызывать super().classinit() из подменеджеров classinit(), потому что менеджеры могут быть производными друг от друга. Но если я использую метаклассы только для инициализации, каждый раз, когда кто-то захочет создать подкласс, это будет немного больно... - person WKnight02; 26.08.2017
comment
Что ж, в какой-то момент, возможно, вам следует пересмотреть использование классов и метаклассов вместо использования классов и экземпляров. Пользователям проще настраивать поведение экземпляров, чем настраивать создание классов (если только они не умеют писать свои собственные метаклассы). - person Blckknght; 26.08.2017

Итак, после некоторых экспериментов мне удалось предоставить «исправление», чтобы разрешить инициализацию класса, позволяющую использовать super().

Во-первых, модуль для «исправления» метода инициализации:

# ./PythonFix.py
import inspect
import types

def IsCellEmpty(cell):
    """Lets keep going, deeper !"""
    try:
        cell.cell_contents
        return False
    except ValueError:
        return True

def ClosureFix(cls, functionContainer):
    """This is where madness happens.
    I didn't want to come here. But hey, lets get mad.
    Had to do this to correct a closure problem occuring in
     Python < 3.6, joy.
    Huge thanks: https://stackoverflow.com/a/4885951/7983255
    """

    # Is the class decorated with @classmethod somehow
    isclassmethod = inspect.ismethod(functionContainer) and functionContainer.__self__ is cls
    if isclassmethod:
        function = functionContainer.__func__
    else:
        function = functionContainer

    # Get cells and prepare a cell holding ref to __class__
    ClosureCells = function.__closure__ or ()
    ClassCell_Fix = (lambda: cls).__closure__[0]

    # Shortcut
    c = function.__code__
    HasClassFreevar = '__class__' in c.co_freevars
    HasEmptyCells = any(IsCellEmpty(cell) for cell in ClosureCells)
    if HasClassFreevar and not HasEmptyCells: # No fix required.
        return classmethod(function)

    Freevars_Fixed = c.co_freevars
    Closure_Fixed = ClosureCells

    if not HasClassFreevar:
        Freevars_Fixed += ('__class__',)
        Closure_Fixed += (ClassCell_Fix,)

    elif HasEmptyCells: # This is silly, but for what I'm doing its ok.
        Closure_Fixed = tuple(ClassCell_Fix if IsCellEmpty(cell) else cell for cell in ClosureCells)

    # Now the real fun begins
    PyCode_fixedFreevars = types.CodeType(
        c.co_argcount, c.co_kwonlyargcount, c.co_nlocals,
        c.co_stacksize, c.co_flags, c.co_code, c.co_consts, c.co_names,
        c.co_varnames, c.co_filename, c.co_name, c.co_firstlineno,
        c.co_lnotab, Freevars_Fixed, c.co_cellvars
    )

    # Lets fetch the last closure to add our __class__ fix
    FixedFunction = types.FunctionType(
        PyCode_fixedFreevars, function.__globals__, function.__name__,
        function.__defaults__, Closure_Fixed
    )

    # Lets rewrap it so it is an actual classmethod (which it should be):
    return classmethod(FixedFunction)

А теперь код компонента:

class MetaComponent(type):
    def __init__(cls:type, *args, **kargs) -> None:
        super().__init__(*args, **kargs)
        if hasattr(cls, 'classinit'):
            cls.classinit = PythonFix.ClosureFix(cls, cls.classinit)
            cls.classinit(**kargs)
        RegisterComponent(cls)

    def classinit(cls:type, **kargs) -> None:
        """The default classinit method."""
        pass

class Component(metaclass=MetaComponent):
    """This class self registers, inherit from this"""

Честно говоря, я доволен тем, что это сделано. Надеюсь, это поможет кому-то, кто тоже хочет инициализировать классы (по крайней мере, в среде до Python3.6...).

person WKnight02    schedule 25.08.2017