Python:子类整体层次结构

时间:2018-09-25 11:12:15

标签: python inheritance metaclass

根据宫城先生的例子,我的解决方案位于问题的底部


我不确定如何最好地表达标题。我的想法如下。我有一些实现的抽象基类。其中一些实现将彼此称为逻辑的一部分,简化如下:

import abc


# the abstract class
class X(abc.ABC):
    @abc.abstractmethod
    def f(self):
        raise NotImplementedError()

# first implementation
class X1(X):
    def f(self):
        return 'X1'

# second implementation, using instance of first implementation
class X2(X):
    def __init__(self):
        self.x1 = X1()

    def f(self):
        return self.x1.f() + 'X2'

# demonstration
x = X2()
print(x.f())  # X1X2
print(x.x1.f())  # X1

现在,我想在其他模块中使用这些类。但是,我想向层次结构中的所有类添加一些额外的功能(例如,功能g)。我可以通过将其添加到基类X中来做到这一点,但是我想单独定义功能。例如,我可能想定义如下新功能:

class Y(X):
    def g(self):
        return self.f() + 'Y1'

这将创建具有新功能的另一个基类,但是当然不会将其添加到现有实现X1X2中。我必须使用钻石继承来做到这一点:

class Y1(X1, Y):
    pass

class Y2(X2, Y):
    pass

# now I can do this:
y = Y2()
print(y.g())  # X1X2Y1

上面的方法可以正常工作,但是仍然存在问题。在X2.__init__中,创建X1的实例。为了使我的想法可行,必须在Y1中将其变为Y2.__init__。但是,事实并非如此:

print(y.x1.g())  # AttributeError: 'X1' object has no attribute 'g'

我认为我可能正在寻找一种将X转换为抽象元类的方法,以便其实现需要一个'base'参数成为类,然后可以将其实例化。然后,在类中使用此参数以实例化具有正确基数的其他实现。

在基类中创建具有新功能的实例将如下所示:

class Y:
    def g(self):
        return self.f() + 'Y1'

X2(Y)()

哪个对象将导致与以下类的实例等效:

class X2_with_Y:
    def __init__(self):
        self.x1 = X1(Y)()

    def f(self):
        return self.x1.f() + 'X2'

    def g(self):
        return self.f() + 'Y1'

但是我不知道如何创建一个可以做到这一点的元类。我想听听一个元类是否是正确的主意,如果是的话,该怎么做。


解决方案

使用宫城先生的例子,我可以得到一些我认为可行的东西。这种行为接近我的元类想法。

import abc


class X(abc.ABC):
    base = object  # default base class

    @classmethod
    def __class_getitem__(cls, base):
        if cls.base == base:
            # makes normal use of class possible
            return cls
        else:
            # construct a new type using the given base class and also remember the attribute for future instantiations
            name = f'{cls.__name__}[{base.__name__}]'
            return type(name, (base, cls), {'base': base})


    @abc.abstractmethod
    def f(self):
        raise NotImplementedError()


class X1(X):
    def f(self):
        return 'X1'


class X2(X):
    def __init__(self):
        # use the attribute here to get the right base for the other subclass
        self.x1 = X1[self.base]()

    def f(self):
        return self.x1.f() + 'X2'


# just the wanted new functionality
class Y(X):
    def g(self):
        return self.f() + 'Y1'

用法是这样的:

# demonstration
y = X2[Y]()
print(y.g())  # X1X2Y1
print(y.x1.g())  # X1Y1
print(type(y))  # <class 'abc.X2[Y]'>
# little peeve here: why is it not '__main__.X2[Y]'?

# the existing functionality also still works
x = X2()
print(x.f())  # X1X2
print(x.x1.f())  # X1
print(type(x))  # <class '__main__.X2'>

2 个答案:

答案 0 :(得分:3)

由于您正在寻找一种自定义类的方法,所以最简单的方法就是这样做:

class X2(X):
    component_type = X1

    def __init__(self):
        self.x1 = self.component_type()

class Y2(X2, Y):
    component_type = Y1

由于component_type是一个类属性,因此可以专门化同一类的不同变体(阅读:子类)。


请注意,您当然可以使用其他代码来构造此类。可以使用类方法来创建新的派生类。

比如说您的班级能够选择正确的subclasses from their hierarchy

class X2(X):
    hierarchy = X

    @classmethod
    def specialise(cls, hierarchy_base):
        class Foo(cls, hierarchy_base):
            hierarchy = hierarchy_base
        return Foo

Y2 = X2.specialise(Y)

从Python3.7开始,可以使用__class_getitem__将以上内容写为Y2 = X2[Y],类似于将Tuple专门化为Tuple[int]的方式。

class X2(X):
    hierarchy = X

    def __class_getitem__(cls, hierarchy_base):
        class Foo(cls, hierarchy_base):
            hierarchy = hierarchy_base
        return Foo

Y2 = X2[Y]

类属性通常服务于元类字段的功能,因为它们精确地表达了这一点。从理论上讲,设置类属性等效于添加一个元类字段,然后将其设置为每个类的属性。这里利用的是Python允许实例具有属性,而无需类来定义其字段。可以将类属性视为鸭子式元类。

答案 1 :(得分:1)

您的问题是一个经常出现的问题,即您想更改现有类的行为。您可以通过继承它并添加新行为来实现这一方面。然后,您为此子类创建的所有实例都具有新行为。

但是您还希望其他人(在这种情况下为X2)创建此类的进一步的实例,而现在创建您自己的子类的实例而是添加了行为。

这可以被视为干预他人的事务。我的意思是,如果类X2要创建X1的实例,您是谁(X2的纯用户!)告诉它应该创建其他东西?也许不适用于X1类型的东西!

但是-当然。到过那里。做到了。我知道有时会出现这种需求。

实现此目的的直接方法是使类X2合作。这意味着,与其创建类X1的实例,不如创建一个作为参数传递的类的实例:

class X2(X):
    def __init__(self, x1_class=X1):
        self.x1 = x1_class()

这也可以使用方法重写而不是参数传递很好地嵌入:

class X2(X):
    @classmethod
    def my_x1(cls):
         return X1

    def __init__(self):
        self.x1 = self.my_x1()

,然后在另一个模块中:

class Y2(X2, Y):
    @classmethod
    def my_x1(cls):
        return Y1

但是,如果您可以更改X2,那么所有这些操作都将起作用,并且在某些情况下您将无法执行此操作(因为X的模块是第三方提供的,甚至是内置库,因此有效的只读)。

在这些情况下,您可以考虑进行猴子修补:

def patched_init(self):
    self.x1 = Y1()

X1.__init__ = patched_init

可以使用单元测试模块中已知的模拟方法来获得类似的解决方案。但是所有这些都具有共同点,即它们适用于所用类的当前实现的详细信息。一旦这些更改,代码就会中断。

因此,如果可以的话,最好为项目准备基类(X2),并使其更灵活地适合您的用例。