根据__init__参数更改对象的基类

0 投票
4 回答
1633 浏览
提问于 2025-04-18 04:48

我正在尝试在Python中创建一个元类,这个元类可以根据创建实例时传入的参数,动态地改变类型的基类。

简单来说,我有一个层级关系,C --> B --> A,但我想做的是,如果在创建C时传入了特定的东西,就动态地把A替换成其他的A的实现。

因为C是这个库的用户实现的,我不想让他们写一些初学者看不懂的东西,所以我的计划是在B里面实现这个“魔法”,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__里面处理所有事情,但问题是到那时对象已经存在了,所以改变基类就太晚了。

我对类和元类的理解有什么遗漏吗?有没有办法做到这一点?

4 个回答

1

你可以通过使用一个包装器来实现这个功能:

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

    def foo(self):
        return self._a.foo()
2

这是我目前能想到的最接近的东西:

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)。这样做是为了避免无限循环。如果在 B 中使用 __new__ 来处理委托,但新创建的类需要从原始的 C 继承,那么这个新类就会继承 B.__new__,而内部尝试创建一个实例又会触发这个魔法。其实可以通过使用 __new__ 并给动态创建的类添加一个“秘密”属性来解决这个问题,然后检查这个属性来跳过魔法。

  2. B 不从 A 继承,所以当 C 从 B 继承时,它也不会从 A 继承;相反,它会从正确替换的实现基础中继承。

2

更新:一个可能的替代方案是使用类装饰器来代替当前的 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)
3

我觉得我已经实现了你所说的元类。虽然我不确定这是不是最好的设计,但它确实能工作。每个名义上的 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。在后台,它们实际上是在定义一个特化类的家族,这些特化类继承自 BA 的各种特化。

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

如果你想让未特化的 BC 类型假装是它们特化的超类,你可以在 BMeta 中添加以下函数:

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

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

这些函数会让内置的 isinstanceissubclass 函数将从 BC 返回的实例视为它们“工厂”类的实例。

撰写回答