根据__init__参数更改对象的基类
我正在尝试在Python中创建一个元类,这个元类可以根据创建实例时传入的参数,动态地改变类型的基类。
简单来说,我有一个层级关系,C --> B --> A
,但我想做的是,如果在创建C
时传入了特定的东西,就动态地把A
替换成其他的A
的实现。
因为C
是这个库的用户实现的,我不想让他们写一些初学者看不懂的东西,所以我的计划是在B
里面实现这个“魔法”,B
的存在只是为了把A
指向一个合适的实现。
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__
里面处理所有事情,但问题是到那时对象已经存在了,所以改变基类就太晚了。
我对类和元类的理解有什么遗漏吗?有没有办法做到这一点?
4 个回答
你可以通过使用一个包装器来实现这个功能:
class Bwrapper(object):
def __init__(self, impl):
self._a = Aimpl2() if impl == 2 else Aimpl1()
def foo(self):
return self._a.foo()
这是我目前能想到的最接近的东西:
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
它和你的设置有几个不同之处:
类 C 必须通过
makeIt
这个类方法来实例化,而不能直接用C(blah)
。这样做是为了避免无限循环。如果在B
中使用__new__
来处理委托,但新创建的类需要从原始的C
继承,那么这个新类就会继承B.__new__
,而内部尝试创建一个实例又会触发这个魔法。其实可以通过使用__new__
并给动态创建的类添加一个“秘密”属性来解决这个问题,然后检查这个属性来跳过魔法。B 不从 A 继承,所以当 C 从 B 继承时,它也不会从 A 继承;相反,它会从正确替换的实现基础中继承。
更新:一个可能的替代方案是使用类装饰器来代替当前的 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)
我觉得我已经实现了你所说的元类。虽然我不确定这是不是最好的设计,但它确实能工作。每个名义上的 C
实例实际上都是 C
的一个“特化”实例,而这个特化又是从 B
的一个特化派生的,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
,或者其他方式),而且你可能想把 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
返回的实例视为它们“工厂”类的实例。