Я считаю, что мне удалось реализовать метакласс, о котором вы просили. Я не уверен, что это лучший возможный дизайн, но он работает. Каждый условный экземпляр 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
C
, и, насколько я могу понять, для этого все равно потребовалось бы написать две версииC
. - person Flexo   schedule 30.04.2014__mro__
в нескольких типах, прозрачно для пользователя. - person Flexo   schedule 30.04.2014xyz
. Для этого вам нужны два объекта класса. - person shx2   schedule 30.04.2014B.__new__
, но в конечном счете я думаю, что это бесперспективно. Если пользователи пишутC
, они могут делать там все, что захотят, поэтому вы никак не можете надежно вмешаться в то, что происходит при создании экземпляраC
. Он находится ниже в цепочке наследования, поэтому его реализации всегда будут вызываться до любой магии, которую вы пытаетесь добавить в его основу. - person BrenBarn   schedule 30.04.2014class C(B): ...
, но сделать так, чтобы записьC('a','b','c')
выбирала базовый класс (AimplN
) на основе значений, переданных во время построения. Я предполагаю, что на самом деле это означает, что динамически создаваемых типов будет N. Мета-классы могут быть здесь большой ошибкой, я думал, что они были способом подключиться к созданию экземпляра, чтобы получить доступ, который мне нужен для его достижения. - person Flexo   schedule 30.04.2014C
и добавлять к ним любое поведение, которое они хотят? Существует большая разница между написанием класса, который делает это, и написанием базового класса, который позволит (или заставит) сделать это любому наследующему классу. - person BrenBarn   schedule 30.04.2014C
, но на самом деле необходимо, чтобы правильный базовый класс был прозрачно выбран, потому что они (AimplN
) представляют собой обернутые в C++ экземпляры специализаций шаблонов. (Наследование и отличие типов важны) - person Flexo   schedule 30.04.2014C
с динамически производным типомA
тогда - было бы справедливо описать это как это? например: Кто-то пишет C, и его класс должен быть создан с подходящимA
в качестве основы за кулисами... C-›A работает как обычно для каждого класса, ноA
явно выбирается писательC
... - person Jon Clements♦   schedule 30.04.2014A
неявно выбирается авторомC
из типов, которые переходят кB
при создании типа. - person Flexo   schedule 30.04.2014C
, которые имеют одну и ту же базуAImpX
, имели один и тот же класс? То есть нужно лиtype(C(*args)) == type(C(*args))
для всех допустимыхargs
? - person Blckknght   schedule 30.04.2014