如何使用实现继承?

Mag*_*ero 3 python inheritance adapter private-members

如何在Python中使用实现继承,即实现继承的基类的public属性x和protected属性_x变成__x派生类的私有属性?

换句话说,在派生类中:

  • 访问公共属性x或受保护属性_x应该像往常一样分别查找x_x分别,除了它应该跳过实现继承的基类;
  • 访问私有属性__x应该__x像往常一样查找,除了它应该查找x_x不是__x实现继承的基类。

在 C++ 中,实现继承是通过private在派生类的基类声明中使用访问说明符来实现的,而更常见的接口继承是通过使用public访问说明符来实现的:

class A: public B, private C, private D, public E { /* class body */ };
Run Code Online (Sandbox Code Playgroud)

例如,需要实现继承来实现依赖于类继承适配器设计模式(不要与依赖于对象组合对象适配器设计模式混淆)并包括将类的接口转换为类的接口。通过使用继承抽象类的接口和类的实现的类来创建抽象类(参见Erich Gamma等人设计模式一书):AdapteeTargetAdapterTargetAdaptee

类适配器

下面是一个 Python 程序,根据上面的类图指定了意图:

import abc

class Target(abc.ABC):
    @abc.abstractmethod
    def request(self):
        raise NotImplementedError

class Adaptee:
    def __init__(self):
        self.state = "foo"
    def specific_request(self):
        return "bar"

class Adapter(Target, private(Adaptee)):
    def request(self):
        # Should access self.__state and Adaptee.specific_request(self)
        return self.__state + self.__specific_request()  

a = Adapter()

# Test 1: the implementation of Adaptee should be inherited
try:
    assert a.request() == "foobar"
except AttributeError:
    assert False

# Test 2: the interface of Adaptee should NOT be inherited
try:
    a.specific_request()
except AttributeError:
    pass
else:
    assert False
Run Code Online (Sandbox Code Playgroud)

Mar*_*ers 8

你不想这样做。Python 不是 C++,C++ 也不是 Python。类的实现方式完全不同,因此会导致不同的设计模式。你不是需要使用类适配器模式在Python,你也不希望。

在 Python 中实现适配器模式的唯一实用方法是使用组合,或者通过子类化 Adaptee而不隐藏你这样做了。

我说,实际这里是因为有办法排序使其工作,但是这条道路将采取大量的工作来实现,并有可能引入很难追踪bug,并会作出调试和代码维护多,更难。忘记“是否有可能”,您需要担心“为什么有人想要这样做”。

我会试着解释原因。

我还会告诉你不切实际的方法是如何工作的。我实际上并不打算实施这些,因为那是徒劳无功的工作,而且我只是不想在这上面花任何时间。

但首先我们必须在这里澄清几个误解。您对 Python 及其模型与 C++ 模型的不同之处的理解存在一些非常基本的差距:如何处理隐私,以及编译和执行哲学,所以让我们从这些开始:

隐私模型

首先,你不能把C++的隐私模型应用到Python上,因为Python没有封装隐私。在所有。你需要完全放弃这个想法。

以单个下划线开头的名称实际上不是 private,不是 C++ 隐私的工作方式。他们也不受“保护”。使用下划线只是一种约定,Python 不强制执行访问控制。任何代码都可以访问实例或类的任何属性,无论使用什么命名约定。相反,当您看到以下划线开头的名称时,您可以假设该名称不是公共接口约定的一部分,也就是说,这些名称可以在没有通知或考虑向后兼容性的情况下更改。

引用有关该主题Python 教程部分

Python 中不存在只能从对象内部访问的“私有”实例变量。但是,大多数 Python 代码都遵循一个约定:带有下划线前缀的名称(例如_spam)应该被视为 API 的非公开部分(无论是函数、方法还是数据成员)。它应被视为实施细节,如有更改,恕不另行通知。

这是一个很好的约定,但甚至不是您可以始终依赖的东西。例如,collections.namedtuple()类生成器生成一个具有 5 个不同方法和属性的类,这些方法和属性都以下划线开头,但都意味着是公共的,因为另一种方法是对可以为包含的元素指定的属性名称进行任意限制,并使其成为在不破坏大量代码的情况下,在未来的 Python 版本中添加其他方法非常困难。

以两个下划线开头(结尾没有)的名称也不是私有的,不是在类封装意义上,例如 C++ 模型。它们是类私有名称,这些名称在编译时重写以生成每个类的命名空间,以避免冲突。

换句话说,它们用于避免与namedtuple上述问题非常相似的问题:取消对子类可以使用的名称的限制。如果您需要设计在框架中使用的基类,其中子类应该可以无限制地自由命名方法和属性,那么您就可以使用__name类私有名称。Python的编译器将重写__attribute_name_ClassName__attribute_name一个内使用时class在正在被一个内部定义的任何功能的语句以及class语句。

请注意,C++不使用名称来表示隐私。相反,隐私是每个标识符的属性,在给定的命名空间内,由编译器处理。编译器强制执行访问控制;私有名称不可访问,会导致编译错误。

如果没有隐私模型,“实现继承的基类的公共属性x和受保护属性_x成为__x派生类的私有属性”的要求将无法实现

编译和执行模型

C++

C++ 编译生成二进制机器代码,旨在由您的 CPU 直接执行。如果你想从另一个项目扩展一个类,你只能在你可以访问头文件形式的附加信息来描述可用的 API 的情况下这样做。编译器将头文件中的信息与存储在机器代码和源代码中的表结合起来,以构建更多的机器代码;例如,跨库边界的继承是通过虚拟化表处理的。

实际上,用于构建程序的对象所剩无几。您通常不会创建对类或方法或函数对象的引用,编译器已将这些抽象概念作为输入,但生成的输出是机器代码,不再需要大多数这些概念存在。变量(状态、方法中的局部变量等)要么存储在堆上,要么存储在堆栈上,机器代码直接访问这些位置。

隐私用于指导编译器优化,因为编译器在任何时候都可以准确地知道什么代码可以改变什么状态。隐私还使虚拟化表和从 3rd 方库继承变得实用,因为只需要公开公共接口。隐私主要是一种效率措施

Python

另一方面,Python 使用专用的解释器运行时运行 Python 代码,它本身是一段从 C 代码编译的机器代码,它有一个中央评估循环,可以使用Python 特定的操作码来执行您的代码。Python 源代码大致在模块和函数级别被编译成字节码,存储为对象的嵌套树。

这些对象是完全可内省的,使用属性、序列和映射通用模型。您可以对类进行子类化,而无需访问其他头文件。

在这个模型中,类是一个对象,它包含对基类的引用,以及属性的映射(包括通过访问实例成为绑定方法的任何函数)。在实例上调用方法时要执行的任何代码都封装在附加到存储在类属性映射中的函数对象的代码对象中。代码对象已经编译为字节码,并且与 Python 对象模型中的其他对象的交互是通过引用的运行时查找,如果源代码使用固定名称,则用于这些查找的属性名称作为常量存储在编译的字节码中。

从执行 Python 代码的角度来看,变量(状态和局部变量)存在于字典中(Python 类型,忽略作为哈希映射的内部实现),或者对于函数中的局部变量,存在于附加到堆栈帧对象的数组中. Python 解释器将对这些的访问转换为对存储在堆上的值的访问。

这使得 Python 变慢,但在执行. 您不仅可以内省对象树,树的大部分都是可写的,让您可以随意替换对象,从而以几乎无限的方式改变程序的行为方式。同样,没有强制执行隐私控制

为什么在 C++ 中使用类适配器,而不是在 Python 中

我的理解是,有经验的 C++ 编码人员将在对象适配器(使用组合)上使用类适配器(使用子类化),因为他们需要通过编译器强制类型检查(他们需要将实例传递给需要Target类或其子类),并且他们需要对对象生命周期和内存占用进行精细控制。因此,在使用组合时不必担心封装实例的生命周期或内存占用,子类使您可以更完整地控制适配器的实例生命周期。

当更改适配类如何控制实例生命周期的实现可能不切实际或什至不可能时,这尤其有用。同时,您不想剥夺编译器由私有和受保护属性访问提供的优化机会。同时公开 Target 和 Adaptee 接口的类提供的优化选项较少。

在 Python 中,您几乎不必处理此类问题。Python 的对象生命周期处理是直接的、可预测的,并且无论如何对每个对象都一样。如果生命周期管理或内存占用成为一个问题,您可能已经将实现移至 C++ 或 C 等扩展语言。

其次,大多数 Python API 不需要特定的类或子类。他们只关心正确的协议,即是否实现了正确的方法和属性。只要你Adapter有正确的方法和属性,它就会做得很好。见鸭打字;如果您的适配器走路像鸭子,说话像鸭子,那肯定鸭子。如果同一只鸭子也能像狗一样吠叫,那也没关系。

不在 Python 中执行此操作的实际原因

让我们转向实用性。我们需要更新您的示例Adaptee类以使其更逼真:

class Adaptee:
    def __init__(self, arg_foo=42):
        self.state = "foo"
        self._bar = arg_foo % 17 + 2 * arg_foo

    def _ham_spam(self):
        if self._bar % 2 == 0:
            return f"ham: {self._bar:06d}"
        return f"spam: {self._bar:06d}"

    def specific_request(self):
        return self._ham_spam()
Run Code Online (Sandbox Code Playgroud)

这个对象不仅有一个state属性,它还有一个_bar属性和一个私有方法_ham_spam

现在,从现在开始,我将忽略您的基本前提存在缺陷这一事实,因为 Python 中没有隐私模型,而是将您的问题重新解释为重命名属性的请求。

对于上面的例子,它会变成:

  • state -> __state
  • _bar -> __bar
  • _ham_spam -> __ham_spam
  • specific_request -> __specific_request

您现在有一个问题,因为在代码_ham_spamspecific_request已编译的。这些方法的实现期望在调用时在传入的对象上查找_bar_ham_spam属性self。这些名称是其编译字节码中的常量:

>>> import dis
>>> dis.dis(Adaptee._ham_spam)
  8           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (_bar)
              4 LOAD_CONST               1 (2)
              6 BINARY_MODULO
# .. etc. remainder elided ..
Run Code Online (Sandbox Code Playgroud)

LOAD_ATTR上述 Python 字节码反汇编摘录中的操作码只有在局部变量self具有名为_bar.

需要注意的是self可以绑定到的实例Adaptee以及的Adapter,有些东西你不得不如果你想改变这个怎么操作的代码考虑到。

因此,仅仅重命名方法和属性名称是不够的

克服这个问题需要以下两种方法之一:

  • 拦截类和实例级别上的所有属性访问以在两个模型之间进行转换。
  • 重写所有方法的实现

这两个都不是一个好主意。当然,与创建组合适配器相比,它们都不会更有效或更实用。

不切实际的方法#1:重写所有属性访问

Python动态的,您可以在类和实例级别拦截所有属性访问。您需要两者,因为您混合了类属性(_ham_spamspecific_request)和实例属性(state_bar)。

  • 您可以通过实现自定义属性访问部分中的所有方法来拦截实例级属性访问(__getattr__在这种情况下不需要)。您必须非常小心,因为您需要访问实例的各种属性,同时控制对这些属性的访问。您需要处理设置和删除以及获取。这使您可以控制对 的实例的大多数属性访问Adapter()

  • 通过为适配器将返回的任何类创建private(),并在那里实现完全相同的属性访问挂钩方法,您可以在类级别执行相同的操作。您必须考虑到您的类可以有多个基类,因此您需要使用它们的 MRO ordering它们作为分层命名空间处理。与 Adapter 类的属性交互(例如Adapter._special_request内省从 继承的方法Adaptee)将在此级别处理。

听起来很容易,对吧?除了 Python 解释器有许多优化之外,以确保它在实际工作中不会完全太慢。如果您开始拦截实例上的每个属性访问,您将杀死很多这些优化(例如Python 3.7 中引入方法调用优化)。更糟糕的是,Python忽略了特殊方法查找的属性访问挂钩

现在您已经注入了一个用 Python 实现的翻译层,每次与对象的交互都会调用多次。这是一个性能瓶颈。

最后但并非最不重要的一点是,要以通用的方式执行此操作,您可以期望private(Adaptee)在大多数情况下都可以工作,这很困难。Adaptee可能有其他原因来实现相同的钩子。Adapter或者层次结构中的同级类也可以实现相同的钩子,并以一种意味着private(...)版本被简单绕过的方式实现它们。

侵入式全属性拦截是脆弱的,很难做到正确。

不切实际的方法#2:重写字节码

这会进一步深入兔子洞。如果属性重写不切实际,那么重写 的代码如何Adaptee

是的,原则上你可以这样做。有一些工具可以直接重写字节码,例如codetransformer. 或者,您可以使用该inspect.getsource()函数读取给定函数的磁盘 Python 源代码,然后使用该ast模块重写所有属性和方法访问,然后将生成的更新后的 AST 编译为字节码。您必须对AdapteeMRO 中的所有方法都这样做,并动态生成一个替换类来实现您想要的。

也不容易。该pytest项目做这样的事情,他们重写测试断言以提供比其他方式更详细的失败信息。这个简单的功能需要一个1000+ 行的模块来实现,搭配一个1600 行的测试套件来确保它正确地做到这一点。

然后你实现的是与原始源代码不匹配的字节码,因此任何必须调试此代码的人都必须处理调试器看到的源代码与 Python 正在执行的不匹配的事实.

您还将失去与原始基类的动态连接。无需重写代码的直接继承让您可以动态更新Adaptee类,重写代码会强制断开连接。

这些方法不起作用的其他原因

我忽略了上述方法都无法解决的另一个问题。因为 Python 没有隐私模型,所以有很多项目代码直接与类状态交互。

例如,如果您的Adaptee()实现依赖于将尝试访问state_bar直接访问的实用程序函数怎么办?它是同一个库的一部分,该库的作者完全有权假设访问Adaptee()._bar是安全和正常的。属性拦截和代码重写都不能解决这个问题。

我也忽略了isinstance(a, Adaptee)仍然会返回的事实True,但是如果您通过重命名隐藏了它的公共 API,那么您就违反了该合同。无论好坏,Adapter都是Adaptee.

TLDR

所以,总结一下:

  • Python 没有隐私模型。在这里尝试强制执行是没有意义的。
  • 在 C++ 中需要类适配器模式的实际原因在 Python 中不存在
  • 在这种情况下,动态属性代理和代码转换都不实用,并且引入的问题比这里解决的问题要多。

您应该改为使用组合,或者只接受您的适配器既是 aTarget又是 an Adaptee,因此使用子类化来实现新接口所需的方法而不隐藏适配器接口:

class CompositionAdapter(Target):
    def __init__(self, adaptee):
        self._adaptee = adaptee

    def request(self):
        return self._adaptee.state + self._adaptee.specific_request()


class SubclassingAdapter(Target, Adaptee):
    def request(self):
        return self.state + self.specific_request()
Run Code Online (Sandbox Code Playgroud)