如何使用实现继承?

How to use implementation inheritance?

如何在Python中使用实现继承,即public属性x和保护属性_x 的实现继承基 classes 成为私有属性 __x 派生的 class?

换句话说,在导出的class:

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

class A: public B, private C, private D, public E { /* class body */ };

例如,需要实现继承来实现依赖于class继承的class适配器设计模式(不要与依赖于对象组合对象适配器设计模式混淆)并且包括转换[=26的接口=] class 通过使用继承 接口 [=59= 的 Adapter class 进入 Target 抽象 class 的接口Target 摘要 class 的 ] 和 Adaptee class 的 实现 (参见 Design Patterns 书Erich Gamma 等人):

这是一个 Python 程序,根据上面的 class 图表指定了预期的内容:

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

Python 无法像您描述的那样定义私有成员 (docs)。

您可以使用 encapsulation instead of inheritance 并直接调用该方法,正如您在评论中指出的那样。这将是我的首选方法,感觉最“Pythonic”。

class Adapter(Target):
    def request(self):
        return Adaptee.specific_request(self)

一般来说,Python 对 类 的处理方式比 C++ 中的处理方式宽松得多。 Python支持duck-typing,所以不需要子类Adaptee,只要满足Target的接口即可。

如果你真的想使用继承,你可以覆盖你不想公开的接口来引发 AttributeError,并使用下划线约定来表示私有成员。

class Adaptee:
    def specific_request(self):
        return "foobar"
    
    # make "private" copy
    _specific_request = specific_request

class Adapter(Target, Adaptee):
    def request(self):
        # call "private" implementation
        return self._specific_request()
    
    def specific_request(self):
        raise AttributeError()

This question 如果您想要伪造私有方法的替代方案,有更多建议。

如果您真的 想要真正的私有方法,您可能会实现一个覆盖 object.__getattribute__ 的元类。但我不推荐它。

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

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

我在这里说实用是因为有一些方法可以某种让它工作,但是这条路需要很多工作实施并可能引入难以追踪的错误,并且会使调试和代码维护变得非常非常困难。忘掉 'is it possible',你需要担心 'why would anyone ever want to do this'.

我会试着解释原因。

我还会告诉您不切实际的方法可能会如何工作。我实际上并不打算实施这些,因为那是徒劳无益的,我根本不想花任何时间在这上面。

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

隐私模型

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

以单个下划线开头的名称实际上不是私有的,不是 C++ 隐私工作的方式。他们也不是 'protected'。使用下划线只是一种约定,Python并不强制执行访问控制。任何代码都可以访问实例或 classes 上的任何属性,无论使用何种命名约定。相反,当您看到以下划线开头的名称时,您可以假设该名称 不是 public 接口约定的一部分 ,也就是说,这些名称可以更改恕不另行通知或考虑向后兼容性。

引自 Python tutorial section on the subject:

“Private” instance variables that cannot be accessed except from inside an object don’t exist in Python. However, there is a convention that is followed by most Python code: a name prefixed with an underscore (e.g. _spam) should be treated as a non-public part of the API (whether it is a function, a method or a data member). It should be considered an implementation detail and subject to change without notice.

这是一个很好的约定,但甚至连您都不能始终如一地依赖它。例如。 collections.namedtuple() class generator 生成一个 class,它有 5 个不同的方法和属性,它们都以下划线开头,但都 意味着 是 public,因为替代方案将对您可以给包含的元素的属性名称施加任意限制,并且在不破坏大量代码的情况下很难在未来 Python 版本中添加其他方法。

以两个下划线开头(结尾是 none)的名称也不是私有的,不是像 C++ 模型那样的 class 封装意义上的。它们 class-private names, these names are re-written at compile time 产生一个 per-class 命名空间,以避免冲突。

换句话说,它们用于避免与上述 namedtuple 问题非常相似的问题:取消对 subclass 可以使用的名称的限制。如果您需要设计基础 classes 以在框架中使用,其中子 classes 应该可以不受限制地自由命名方法和属性,这就是您使用 __name class-private 名字。 Python 编译器将 __attribute_name 重写为 _ClassName__attribute_name 当在 class 语句内以及在 class 语句内定义的任何函数中使用时。

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

如果没有隐私模型,您的要求“public 实现的属性 x 和受保护的属性 _x 继承基础 class 成为私有属性 __x 派生的 class" 无法达到

编译和执行模型

C++

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

处理跨库边界的继承

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

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

Python

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

这些对象是完全可自省的,使用 common model of attributes, sequences and mappings。您可以子 class classes 而无需访问其他头文件。

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

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

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

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

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

这在更改适配器 class 如何控制实例生命周期的实现可能不切实际甚至不可能时特别有用。同时,您不希望剥夺编译器通过私有和受保护属性访问提供的优化机会。公开 Target 和 Adaptee 接口的 class 提供更少的优化选项。

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

接下来,大多数 Python API 不需要特定的 class 或子 class。他们只关心正确的协议,即是否实现了正确的方法和属性。只要您的 Adapter 具有正确的方法和属性,它就可以正常工作。参见Duck Typing;如果您的适配器走路像鸭子,说话像鸭子,那么它肯定 鸭子。同一只鸭子是否也能像狗一样吠叫并不重要。

在 Python

中您不这样做的 实用 原因

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

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()

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

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

上面的例子会变成:

  • state -> [=31=
  • _bar -> __bar
  • _ham_spam -> __ham_spam
  • specific_request -> __specific_request

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

>>> 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 ..

上面 Python 字节码反汇编摘录中的 LOAD_ATTR 操作码只有在局部变量 self 具有名为 _bar.[=92= 的属性时才能正常工作]

请注意,self 可以绑定到 Adaptee 以及 Adapter 的实例,如果您想更改此方式,则必须考虑这一点代码运行。

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

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

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

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

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

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

  • 您可以通过实现 Customizing attribute access section 中的所有方法来拦截 instance-level 属性访问(对于这种情况,您不需要 __getattr__)。您必须非常小心,因为您需要访问实例的各种属性,同时控制对这些属性的访问。您需要处理设置和删除以及获取。这使您可以控制对 Adapter().

    实例的大多数属性访问
  • 您可以通过创建 metaclass for whatever class your private() adapter would return, and implementing the exact same hook methods for attribute access there. You'll have to take into account that your class can have multiple base classes, so you'd need to handle these as layered namespaces, using their MRO ordering 在 class 级别执行相同的操作。与适配器 class 的属性交互(例如 Adapter._special_request 内省从 Adaptee 继承的方法)将在此级别处理。

听起来很简单,对吧?除了 Python 解释器有许多优化以确保它不会 完全 对于实际工作来说太慢了。如果你开始拦截实例上的 every 属性访问,你将杀死很多这些优化(例如 method call optimisations introduced in Python 3.7). Worse, Python ignores the attribute access hooks for special method lookups.

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

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

侵入式 all-out 属性拦截很脆弱,很难做到正确。

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

这更深入了兔子洞。如果属性重写不可行,重写Adaptee的代码如何?

是的,原则上你可以这样做。有一些工具可以直接重写字节码,例如 codetransformer. Or you could use the inspect.getsource() function to read the on-disk Python source code for a given function, then use the ast module 重写所有属性和方法访问,然后将生成的更新后的 AST 编译为字节码。您必须对 Adaptee MRO 中的所有方法执行此操作,并动态生成替换 class 以实现您想要的效果。

这又是不容易pytest 项目做了类似的事情,他们 rewrite test assertions to provide much more detailed failure information than otherwise possible. This simple feature requires a 1000+ line module to achieve, paired with a 1600-line test suite 确保它正确地做到了。

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

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

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

我忽略了上述两种方法都无法解决的进一步问题。因为 Python 没有隐私模型,所以有很多代码与 class 状态 直接 .

交互的项目

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

我也忽略了 isinstance(a, Adaptee) 仍然会 return True 的事实,但是如果你通过重命名隐藏它是 public API,你有打破了那个合同。无论好坏,AdapterAdaptee.

的子class

TLDR

所以,总结一下:

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

您应该改为使用组合,或者接受您的适配器既是 Target 又是 Adaptee,因此使用 subclassing 来实现新方法所需的方法接口 没有 隐藏适配器接口:

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()