Изменение баз объекта на основе аргументов на __init__

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

Короче говоря, у меня есть иерархия, C --> B --> A, но я хочу динамически менять A на другие реализации A, если определенные вещи передаются C для построения.

Поскольку C — это то, что реализуют пользователи этой библиотеки, я не хотел заставлять их писать что-то, чего не поймет новичок, поэтому мой план состоял в том, чтобы магия происходила внутри B, который выходит только для того, чтобы отвлечь A. к соответствующей реализации.

Основываясь на моем понимании метаклассов и __new__ Мне удалось:

class A(object):
  pass

class Aimpl1(object):
  def foo(self):
    print "FOO"

class Aimpl2(object):
  def foo(self):
    print "BAR"

class AImplChooser(type):
  def __call__(self, *args, **kwargs):
    print "In call"
    return super(AImplChooser,self).__call__(*args,**kwargs)

  def __new__(cls, clsname, bases, dct):
    print "Creating: " + clsname + ", " + ','.join([str(x) for x in bases])
    return super(AImplChooser,cls).__new__(cls, clsname, bases, dct)

class B(A):
  __metaclass__ = AImplChooser
  def __init__(self, arg1, arg, arg3):
    pass

class C(B):
  def __init__(self, arg1, arg2=0, arg3=[]):
    super(C, self).__init__(arg1, arg2, arg3)

c=C('')

print type(c)
print dir(type(c))
print c.__class__.__bases__

c.foo()

Мой план состоял в том, чтобы перенаправить базы внутри B.__new__ на основе аргументов B.__call__, но, конечно, они вообще не вызываются в таком порядке, так что это не вариант.

Я думал о том, чтобы полностью отказаться от __new__ и сделать все это внутри __call__, но проблема в том, что к этому моменту объекты уже существуют, поэтому менять базы уже поздно.

Что мне не хватает в классах и метаклассах? Есть ли способ сделать это?


person Flexo    schedule 29.04.2014    source источник
comment
Разве вы не можете просто создать фабричную функцию, которая создает экземпляр соответствующего класса, вместо того, чтобы пытаться сделать это внутри самого класса? Это было бы намного проще.   -  person BrenBarn    schedule 30.04.2014
comment
@BrenBarn не совсем так, поскольку это потребовало бы большего от людей, которые пишут объекты C, и, насколько я могу понять, для этого все равно потребовалось бы написать две версии C.   -  person Flexo    schedule 30.04.2014
comment
То, что вы пытаетесь сделать, не складывается, потому что вы пытаетесь изменить mro класса при создании экземпляра... Что вы ожидаете? если позже будет создан другой экземпляр с другим набором аргументов? Другими словами, это, вероятно, проблема XY.   -  person shx2    schedule 30.04.2014
comment
@shx2 shx2, что я хотел, так это эффективно создать несколько классов путем клонирования, а затем изменить класс во время создания по требованию. По сути, как только это сработает, я ожидаю, что будет несколько __mro__ в нескольких типах, прозрачно для пользователя.   -  person Flexo    schedule 30.04.2014
comment
В этом случае заводское предложение @BrenBarn - это то, что нужно ИМО. У вас не может быть одного класса с двумя разными mro, точно так же, как у вас не может быть одного экземпляра с двумя разными атрибутами с именем xyz. Для этого вам нужны два объекта класса.   -  person shx2    schedule 30.04.2014
comment
Можете ли вы уточнить немного более конкретно поведение, которое вы хотите? В нынешнем виде я не понимаю, как можно удовлетворить ваши требования. Метакласс управляет созданием класса, а не экземпляра, поэтому в то время, когда метакласс активен, экземпляров C еще не существует, поэтому вы не можете знать, какие аргументы будут переданы для создания экземпляра. Это.   -  person BrenBarn    schedule 30.04.2014
comment
Вы можете попробовать добавить свою магию прямо в B.__new__, но в конечном счете я думаю, что это бесперспективно. Если пользователи пишут C, они могут делать там все, что захотят, поэтому вы никак не можете надежно вмешаться в то, что происходит при создании экземпляра C. Он находится ниже в цепочке наследования, поэтому его реализации всегда будут вызываться до любой магии, которую вы пытаетесь добавить в его основу.   -  person BrenBarn    schedule 30.04.2014
comment
@BrenBarn Я хочу написать class C(B): ..., но сделать так, чтобы запись C('a','b','c') выбирала базовый класс (AimplN) на основе значений, переданных во время построения. Я предполагаю, что на самом деле это означает, что динамически создаваемых типов будет N. Мета-классы могут быть здесь большой ошибкой, я думал, что они были способом подключиться к созданию экземпляра, чтобы получить доступ, который мне нужен для его достижения.   -  person Flexo    schedule 30.04.2014
comment
Это все, что вам нужно, или вы хотите создать систему, позволяющую другим людям писать такие классы C и добавлять к ним любое поведение, которое они хотят? Существует большая разница между написанием класса, который делает это, и написанием базового класса, который позволит (или заставит) сделать это любому наследующему классу.   -  person BrenBarn    schedule 30.04.2014
comment
@BrenBarn Я думаю, что это ключевой вопрос здесь ...   -  person Jon Clements♦    schedule 30.04.2014
comment
@BrenBarn цель состоит в том, чтобы получить простое написание классов C, но на самом деле необходимо, чтобы правильный базовый класс был прозрачно выбран, потому что они (AimplN) представляют собой обернутые в C++ экземпляры специализаций шаблонов. (Наследование и отличие типов важны)   -  person Flexo    schedule 30.04.2014
comment
@Flexo, поэтому смешивание написанного пользователем C с динамически производным типом A тогда - было бы справедливо описать это как это? например: Кто-то пишет C, и его класс должен быть создан с подходящим A в качестве основы за кулисами... C-›A работает как обычно для каждого класса, но A явно выбирается писатель C...   -  person Jon Clements♦    schedule 30.04.2014
comment
Тип - A неявно выбирается автором C из типов, которые переходят к B при создании типа.   -  person Flexo    schedule 30.04.2014
comment
@Flexo Я хотел написать не выбрано явно... неявно определенно было бы проще :)   -  person Jon Clements♦    schedule 30.04.2014
comment
Вам нужно, чтобы специализации C, которые имеют одну и ту же базу AImpX, имели один и тот же класс? То есть нужно ли type(C(*args)) == type(C(*args)) для всех допустимых args?   -  person Blckknght    schedule 30.04.2014


Ответы (4)


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

Вот мой код:

Во-первых, нам нужно определить специализации класса A. Это можно сделать как угодно, но для своего тестирования я использовал понимание списка для создания группы классов с разными именами и разными значениями в переменной класса num.

As = [type('A_{}'.format(i), (object,), {"num":i}) for i in range(10)]

Затем у нас есть «фиктивный» неспециализированный класс A, который на самом деле является просто местом для подключения метакласса. Метакласс A AMeta выполняет поиск специализированных классов A в списке, который я определил выше. Если вы используете другой метод для определения специализированных A классов, измените AMeta._get_specialization, чтобы их можно было найти. Возможно, здесь даже будут созданы новые специализации A по запросу, если вы захотите.

class AMeta(type):
    def _get_specialization(cls, selector):
        return As[selector]

class A(object, metaclass=AMeta): # I'm using Python 3 metaclass declarations
    pass # nothing defined in A is ever used, it is a pure dummy

Теперь мы подошли к классу B и его метаклассу BMeta. Именно здесь происходит фактическая специализация наших подклассов. Метод __call__ метакласса использует метод _get_specialization для построения специализированной версии класса на основе аргумента selector. _get_specialization кэширует свои результаты, поэтому для каждой специализации на данном уровне дерева наследования создается только один класс.

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

Обратите внимание, что хотя класс B здесь пуст, вы можете указать ему методы и переменные класса, которые будут появляться в каждой специализации (в виде неглубоких копий).

class BMeta(AMeta):
    def __new__(meta, name, bases, dct):
        cls = super(BMeta, meta).__new__(meta, name, bases, dct)
        cls._specializations = {}
        return cls

    def _get_specialization(cls, selector):
        if selector not in cls._specializations:
            name = "{}_{}".format(cls.__name__, selector)
            bases = (cls.__bases__[0]._get_specialization(selector),)
            dct = cls.__dict__.copy()
            specialization = type(name, bases, dct) # not a BMeta!
            cls._specializations[selector] = specialization
        return cls._specializations[selector]

    def __call__(cls, selector, *args, **kwargs):
        cls = cls._get_specialization(selector)
        return type.__call__(cls, *args, **kwargs) # selector could be passed on here

class B(A, metaclass=BMeta):
    pass

С помощью этой настройки ваши пользователи могут определить любое количество классов C, которые наследуются от B. За кулисами они действительно будут определять целое семейство классов специализации, которые наследуются от различных специализаций B и A.

class C(B):
    def print_num(self):
        return self.num

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

>>> C(1)
<__main__.C_1 object at 0x00000000030231D0>
>>> C(2)
<__main__.C_2 object at 0x00000000037101D0>
>>> C(1).print_num()
1
>>> C(2).print_num()
2
>>> type(C(1)) == type(C(2))
False
>>> type(C(1)) == type(C(1))
True
>>> isinstance(C(1), type(B(1)))
True

Но вот, возможно, неочевидное поведение:

>>> isinstance(C(1), C)
False

Если вы хотите, чтобы неспециализированные типы B и C притворялись суперклассами своих специализаций, вы можете добавить к BMeta следующие функции:

def __subclasscheck__(cls, subclass):
    return issubclass(subclass, tuple(cls._specializations.values()))

def __instancecheck__(cls, instance):
    return isinstance(instance, tuple(cls._specializations.values()))

Это убедит встроенные функции isinstance и issubclass рассматривать экземпляры, возвращенные из B и C, как экземпляры своего "фабричного" класса.

person Blckknght    schedule 30.04.2014
comment
Это выглядит точно так же, как решение, которое я предусмотрел. Точка проверки is instance полезна, хотя в любом случае никто не должен слишком внимательно на нее смотреть. - person Flexo; 30.04.2014

Вот самое близкое, что я могу собрать на данный момент:

class A(object):
    pass

class Aimpl1(object):
    def foo(self):
        print "FOO"

class Aimpl2(object):
    def foo(self):
        print "BAR"

class B(object):
    @classmethod
    def makeIt(cls, whichA):
        if whichA == 1:
            impl = Aimpl1
        elif whichA == 2:
            impl = Aimpl2
        else:
            impl = A
        print "Instantiating", impl, "from", cls
        TmpC = type(b'TmpC', (cls,impl), dict(cls.__dict__))

        return TmpC(whichA)

    def __init__(self, whichA):
        pass

class C(B):
    def __init__(self, whichA):
        super(C, self).__init__(whichA)

Его можно использовать следующим образом:

>>> c = C.makeIt(1)
Instantiating <class '__main__.Aimpl1'> from <class '__main__.C'>
>>> c.__class__.__mro__
(<class '__main__.TmpC'>,
 <class '__main__.C'>,
 <class '__main__.B'>,
 <class '__main__.Aimpl1'>,
 <type 'object'>)
>>> c.foo()
FOO
>>> c = C.makeIt(2)
Instantiating <class '__main__.Aimpl2'> from <class '__main__.C'>
>>> c.__class__.__mro__
(<class '__main__.TmpC'>,
 <class '__main__.C'>,
 <class '__main__.B'>,
 <class '__main__.Aimpl2'>,
 <type 'object'>)
>>> c.foo()
BAR

Он отличается от вашей настройки несколькими способами:

  1. Класс C должен быть создан с использованием метода класса makeIt, а не напрямую с помощью C(blah). Это сделано для того, чтобы избежать бесконечного цикла. Если __new__ используется в B для обработки делегирования, но волшебным образом созданный новый класс с переключенными базами должен наследовать от исходного C, тогда новый класс унаследует B.__new__, и попытка создать его внутри снова вызовет магию. Вероятно, это также можно было бы обойти, используя __new__ и добавив «секретный» атрибут к динамически созданному классу и проверив его, чтобы пропустить магию.

  2. B не наследует от A, поэтому, когда C наследует от B, он не будет наследоваться и от A; вместо этого он наследует от правильно замененной базы реализации.

person BrenBarn    schedule 29.04.2014
comment
Выглядит хорошо, 2. Это именно то, на что я надеялся, я мог бы поиграть с этим, чтобы посмотреть, смогу ли я заставить секретный атрибут работать аккуратно. - person Flexo; 30.04.2014
comment
Я предложил декоратор класса, но на данном этапе он не сильно отличается, за исключением отсутствия classmethod и другого синтаксиса (декорирование вместо наследования от B) @Flexo - person Jon Clements♦; 30.04.2014

Обновление: возможной альтернативой является использование декоратора класса для выполнения текущей роли B:

(Это все еще требует немного работы, хотя).

class A1(object):
    def foo(self):
        print 'foo'
class A2(object):
    def foo(self):
        print 'bar'

from functools import wraps
def auto_impl_A(cls):
    @wraps(cls)
    def f(val, *args, **kwargs):
        base = {1: A1, 2: A2}.get(val, object)
        return type(cls.__name__, (cls, base,), dict(cls.__dict__))(*args, **kwargs)
    return f

@auto_impl_A
class MyC(object):
    pass

Таким образом, пользователи украшают свой класс вместо того, чтобы наследовать его, и пишут C как обычно, но его основой будет соответствующий A...


Исходное предложение: если я правильно понимаю, проще использовать фабричную функцию и с самого начала создать новую type с подходящими базами...

class A1(object): pass
class A2(object): pass
class ANOther(object): pass

def make_my_C_obj(someval, *args, **kwargs):
    base = {1: A1, 2: A2}.get(someval, ANOther)
    return type('C', (base,), {})(*args, **kwargs)

for i in xrange(3):
    print i, type(make_my_C_obj(i)).mro()

0 [<class '__main__.C'>, <class '__main__.ANOther'>, <type 'object'>]
1 [<class '__main__.C'>, <class '__main__.A1'>, <type 'object'>]
2 [<class '__main__.C'>, <class '__main__.A2'>, <type 'object'>]

Это эквивалентно:

class Aimpl1(object):
  def foo(self):
    print "FOO"

class Aimpl2(object):
  def foo(self):
    print "BAR"

def C_factory(base):
  class C(base):
    pass
  return C

for b in (Aimpl1, Aimpl2):
  c=C_factory(b)()
  c.foo()
  print type(c)
person Community    schedule 29.04.2014
comment
Это примерно то, что я предложил. Проблема с этим заключается в том, что он не позволяет пользователю фактически добавлять поведение в C, чего, похоже, хочет OP. - person BrenBarn; 30.04.2014
comment
@BrenBarn хорошее замечание ... подумал, что, возможно, стоит получить ответ в какой-то форме, чтобы ОП было легче прокомментировать, правильна ли функциональность или нет ... Такие вопросы достаточно сложны сами по себе, не имея какая-то база для сравнения... - person Jon Clements♦; 30.04.2014
comment
@BrenBarn только что вспомнил щелкнуть поле CW, чтобы оно могло служить в дальнейшем, если это необходимо ... - person Jon Clements♦; 30.04.2014
comment
По сути, это то поведение, на которое я надеялся, но не в той форме, которую я искал, потому что а) оно предоставляет type людям, пишущим C, и б) C не реализует абстрактные методы в AimplN. - person Flexo; 30.04.2014
comment
@JonClements, это аналогичный, но все же не идеальный обходной путь - он по-прежнему не скрывает логику выбора правильного AimplN на самом деле (что является просто деталью реализации базовой библиотеки), и он по-прежнему не скрывает сложность множественных типы от людей, реализующих C - person Flexo; 30.04.2014

Вы можете сделать это с помощью обертки:

class Bwrapper(object):
    def __init__(self, impl):
        self._a = Aimpl2() if impl == 2 else Aimpl1()

    def foo(self):
        return self._a.foo()
person kitti    schedule 29.04.2014