且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

如何使用实现继承?

更新时间:2022-10-14 23:26:59

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

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

我在这里说实用是因为有一些方法可以某种程度上使它起作用,但是这条道路需要大量的工作来实施并且可能很难引入跟踪错误,并使调试和代码维护变得更加困难.忘记是否有可能",您需要担心为什么有人想要这样做".

我会试着解释原因.

我还会告诉您不切实际的方法是如何工作的.我实际上并不打算实施这些,因为太多的工作没有任何收获,而且我根本不想在这上面花任何时间.

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

隐私模型

首先,您不能将 C++ 的隐私模型应用于 Python,因为 Python 具有没有封装隐私.在所有.你需要彻底放弃这个想法.

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

引自 关于该主题的 Python 教程部分:

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

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

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

换句话说,它们用于避免与上述 namedtuple 问题非常相似的问题:取消对子类可以使用的名称的限制.如果您需要设计在框架中使用的基类,其中子类应该可以不受限制地***命名方法和属性,那么您可以使用 __name 类私有名称.当在 class 语句中以及在 中定义的任何函数中使用时,Python 编译器会将 __attribute_name 重写为 _ClassName__attribute_nameclass 语句.

请注意,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 类,使其更加真实:

类适配:def __init__(self, arg_foo=42):self.state = foo"self._bar = arg_foo % 17 + 2 * arg_foodef _ham_spam(self):如果 self._bar % 2 == 0:return f"ham: {self._bar:06d}"返回 f垃圾邮件:{self._bar:06d}"def specific_request(self):返回 self._ham_spam()

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

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

对于上面的例子,将变成:

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

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

>>>导入文件>>>dis.dis(Adaptee._ham_spam)8 0 LOAD_FAST 0 (自身)2 LOAD_ATTR 0 (_bar)4 LOAD_CONST 1 (2)6 BINARY_MODULO# .. 等余数省略..

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

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

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

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

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

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

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

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

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

  • 您可以通过创建一个 元类在类级别执行相同的操作 对于您的 private() 适配器将返回的任何类,并为那里的属性访问实现完全相同的钩子方法.您必须考虑到您的类可以有多个基类,因此您需要使用 他们的 MRO 订购.与 Adapter 类的属性交互(例如 Adapter._special_request 以自省从 Adaptee 继承的方法)将在此级别处理.

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

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

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

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

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

这更进一步.如果属性重写不实用,如何重写Adaptee的代码?

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

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

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

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

这些方法无法工作的其他原因

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

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

我也忽略了 isinstance(a, Adaptee) 仍然会返回 True 的事实,但是如果你通过重命名隐藏了它的公共 API,你就违反了那个契约.不管好坏,AdapterAdaptee 的子类.

TLDR

所以,总结一下:

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

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

class CompositionAdapter(Target):def __init__(self,adaptee):self._adaptee = 适应者定义请求(自己):返回 self._adaptee.state + self._adaptee.specific_request()类 SubclassingAdapter(Target, Adaptee):定义请求(自己):返回 self.state + self.specific_request()

How to use implementation inheritance in Python, that is to say public attributes x and protected attributes _x of the implementation inherited base classes becoming private attributes __x of the derived class?

In other words, in the derived class:

  • accessing the public attribute x or protected attribute _x should look up x or _x respectively like usual, except it should skip the implementation inherited base classes;
  • accessing the private attribute __x should look up __x like usual, except it should look up x and _x instead of __x for the implementation inherited base classes.

In C++, implementation inheritance is achieved by using the private access specifier in the base class declarations of a derived class, while the more common interface inheritance is achieved by using the public access specifier:

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

For instance, implementation inheritance is needed to implement the class Adapter design pattern which relies on class inheritance (not to be confused with the object Adapter design pattern which relies on object composition) and consists in converting the interface of an Adaptee class into the interface of a Target abstract class by using an Adapter class that inherits both the interface of the Target abstract class and the implementation of the Adaptee class (cf. the Design Patterns book by Erich Gamma et al.):

Here is a Python program specifying what is intended, based on the above class diagram:

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

You don't want to do this. Python is not C++, nor is C++ Python. How classes are implemented is completely different and so will lead to different design patterns. You do not need to use the class adapter pattern in Python, nor do you want to.

The only practical way to implement the adapter pattern in Python is either by using composition, or by subclassing the Adaptee without hiding that you did so.

I say practical here because there are ways to sort of make it work, but this path would take a lot of work to implement and is likely to introduce hard to track down bugs, and would make debugging and code maintenance much, much harder. Forget about 'is it possible', you need to worry about 'why would anyone ever want to do this'.

I'll try to explain why.

I'll also tell you how the impractical approaches might work. I'm not actually going to implement these, because that's way too much work for no gain, and I simply don't want to spend any time on that.

But first we have to clear several misconceptions here. There are some very fundamental gaps in your understanding of Python and how it's model differs from the C++ model: how privacy is handled, and compilation and execution philosophies, so lets start with those:

Privacy models

First of all, you can't apply C++'s privacy model to Python, because Python has no encapsulation privacy. At all. You need to let go of this idea, entirely.

Names starting with a single underscore are not actually private, not in the way C++ privacy works. Nor are they 'protected'. Using an underscore is just a convention, Python does not enforce access control. Any code can access any attribute on instances or classes, whatever naming convention was used. Instead, when you see a name that start with an underscore you can assume that the name is not part of the conventions of a public interface, that is, that these names can be changed without notice or consideration for backwards compatibility.

Quoting from the 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.

It's a good convention, but not even something you can rely on, consistently. E.g. the collections.namedtuple() class generator generates a class with 5 different methods and attributes that all start with an underscore but are all meant to be public, because the alternative would be to place arbitrary restrictions on what attribute names you can give the contained elements, and making it incredibly hard to add additional methods in future Python versions without breaking a lot of code.

Names starting with two underscores (and none at the end), are not private either, not in a class encapsulation sense such as the C++ model. They are class-private names, these names are re-written at compile time to produce a per-class namespace, to avoid collisions.

In other words, they are used to avoid a problem very similar to the namedtuple issue described above: to remove limits on what names a subclass can use. If you ever need to design base classes for use in a framework, where subclasses should have the freedom to name methods and attributes without limit, that's where you use __name class-private names. The Python compiler will rewrite __attribute_name to _ClassName__attribute_name when used inside a class statement as well as in any functions that are being defined inside a class statement.

Note that C++ doesn't use names to indicate privacy. Instead, privacy is a property of each identifier, within a given namespace, as processed by the compiler. The compiler enforces access control; private names are not accessible and will lead to compilation errors.

Without a privacy model, your requirement where "public attributes x and protected attributes _x of the implementation inherited base classes becoming private attributes __x of the derived class" are not attainable.

Compilation and execution models

C++

C++ compilation produces binary machine code aimed at execution directly by your CPU. If you want to extend a class from another project, you can only do so if you have access to additional information, in the form of header files, to describe what API is available. The compiler combines information in the header files with tables stored with the machine code and your source code to build more machine code; e.g. inheritance across library boundaries is handled through virtualisation tables.

Effectively, there is very little left of the objects used to construct the program with. You generally don't create references to class or method or function objects, the compiler has taken those abstract ideas as inputs but the output produced is machine code that doesn't need most of those concepts to exist any more. Variables (state, local variables in methods, etc.) are stored either on the heap or on the stack, and the machine code accesses these locations directly.

Privacy is used to direct compiler optimisations, because the compiler can, at all times, know exactly what code can change what state. Privacy also makes virtualisation tables and inheritance from 3rd-party libraries practical, as only the public interface needs to be exposed. Privacy is an efficiency measure, primarily.

Python

Python, on the other hand, runs Python code using a dedicated interpreter runtime, itself a piece of machine code compiled from C code, which has a central evaluation loop that takes Python-specific op-codes to execute your code. Python source code is compiled into bytecode roughly at the module and function levels, stored as a nested tree of objects.

These objects are fully introspectable, using a common model of attributes, sequences and mappings. You can subclass classes without having to have access to additional header files.

In this model, a class is an object with references to base classes, as well as a mapping of attributes (which includes any functions which become bound methods through access on instances). Any code to be executed when a method is called on an instance is encapsulated in code objects attached to function objects stored in the class attribute mapping. The code objects are already compiled to bytecode, and interaction with other objects in the Python object model is through runtime lookups of references, with the attribute names used for those lookups stored as constants within the compiled bytecode if the source code used fixed names.

From the point of view of executing Python code, variables (state and local variables) live in dictionaries (the Python kind, ignoring the internal implementation as hash maps) or, for local variables in functions, in an array attached to the stack frame object. The Python interpreter translates access to these to access to values stored on the heap.

This makes Python slow, but also much more flexible when executing. You can not only introspect the object tree, most of the tree is writeable letting you replace objects at will and so change how the program behaves in nearly limitless ways. And again, there are no privacy controls enforced.

Why use class adapters in C++, and not in Python

My understanding is that experienced C++ coders will use a class adapter (using subclassing) over an object adapter (using composition), because they need to pass compiler-enforced type checks (they need to pass the instances to something that requires the Target class or a subclass thereof), and they need to have fine control over object lifetimes and memory footprints. So, rather than have to worry about the lifetime or memory footprint of an encapsulated instance when using composition, subclassing gives you more complete control over the instance lifetime of your adapter.

This is especially helpful when it might not be practical or even possible to alter the implementation of how the adaptee class would control instance lifetime. At the same time, you wouldn't want to deprive the compiler from optimisation opportunities offered by private and protected attribute access. A class that exposes both the Target and Adaptee interfaces offers fewer options for optimisation.

In Python you almost never have to deal with such issues. Python's object lifetime handling is straightforward, predictable and works the same for every object anyway. If lifetime management or memory footprints were to become an issue you'd probably already be moving the implementation to an extension language like C++ or C.

Next, most Python APIs do not require a specific class or subclass. They only care about the right protocols, that is, if the right methods and attributes are implemented. As long as your Adapter has the right methods and attributes, it'll do fine. See Duck Typing; if your adapter walks like a duck, and talks like a duck, it surely must be a duck. It doesn't matter if that same duck can also bark like a dog.

The practical reasons why you don't do this in Python

Let's move to practicalities. We'll need to update your example Adaptee class to make it a bit more realistic:

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

This object not only has a state attribute, it also has a _bar attribute and a private method _ham_spam.

Now, from here on out I'm going to ignore the fact that your basic premise is flawed because there is no privacy model in Python, and instead re-interpret your question as a request to rename the attributes.

For the above example that would become:

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

You now have a problem, because the code in _ham_spam and specific_request has already been compiled. The implementation for these methods expects to find _bar and _ham_spam attributes on the self object passed in when called. Those names are constants in their compiled bytecode:

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

The LOAD_ATTR opcode in the above Python bytecode disassembly excerpt will only work correctly if the local variable self has an attribute named _bar.

Note that self can be bound to an instance of Adaptee as well as of Adapter, something you'd have to take into account if you wanted to change how this code operates.

So, it is not enough to simply rename method and attribute names.

Overcoming this problem would require one of two approaches:

  • intercept all attribute access on both the class and instance levels to translate between the two models.
  • rewriting the implementations of all methods

Neither of these is a good idea. Certainly neither of them are going to be more efficient or practical, compared to creating a composition adapter.

Impractical approach #1: rewrite all attribute access

Python is dynamic, and you could intercept all attribute access on both the class and the instance levels. You need both, because you have a mix of class attributes (_ham_spam and specific_request), and instance attributes (state and _bar).

  • You can intercept instance-level attribute access by implementing all methods in the Customizing attribute access section (you don't need __getattr__ for this case). You'll have to be very careful, because you'll need access to various attributes of your instances while controlling access to those very attributes. You'll need to handle setting and deleting as well as getting. This lets you control most attribute access on instances of Adapter().

  • You would do the same at the class level by creating a 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. Attribute interactions with the Adapter class (such as Adapter._special_request to introspect the inherited method from Adaptee) will be handled at this level.

Sounds easy enough, right? Except than the Python interpreter has many optimisations to ensure it isn't completely too slow for practical work. If you start intercepting every attribute access on instances, you will kill a lot of these optimisations (such as the method call optimisations introduced in Python 3.7). Worse, Python ignores the attribute access hooks for special method lookups.

And you have now injected a translation layer, implemented in Python, invoked multiple times for every interaction with the object. This will be a performance bottleneck.

Last but not least, to do this in a generic way, where you can expect private(Adaptee) to work in most circumstances is hard. Adaptee could have other reasons to implement the same hooks. Adapter or a sibling class in the hierarchy could also be implementing the same hooks, and implement them in a way that means the private(...) version is simply bypassed.

Invasive all-out attribute interception is fragile and hard to get right.

Impractical approach #2: rewriting the bytecode

This goes down the rabbit hole quite a bit further. If attribute rewriting isn't practical, how about rewriting the code of Adaptee?

Yes, you could, in principle, do this. There are tools available to directly rewrite bytecode, such as 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 to rewrite all attribute and method access, then compile the resulting updated AST to bytecode. You'd have to do so for all methods in the Adaptee MRO, and produce a replacement class dynamically that'll achieve what you want.

This, again, is not easy. The pytest project does something like this, they 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 to ensure that it does this correctly.

And what you've then achieved is bytecode that doesn't match the original source code, so anyone having to debug this code will have to deal with the fact that the source code the debugger sees doesn't match up with what Python is executing.

You'll also lose the dynamic connection with the original base class. Direct inheritance without code rewriting lets you dynamically update the Adaptee class, rewriting the code forces a disconnect.

Other reason these approaches can't work

I've ignored a further issue that neither of the above approaches can solve. Because Python doesn't have a privacy model, there are plenty of projects out there where code interacts with class state directly.

E.g., what if your Adaptee() implementation relies on a utility function that will try to access state or _bar directly? It's part of the same library, the author of that library would be well within their rights to assume that accessing Adaptee()._bar is safe and normal. Neither attribute intercepting nor code rewriting will fix this issue.

I also ignored the fact that isinstance(a, Adaptee) will still return True, but if you have hidden it's public API by renaming, you have broken that contract. For better or worse, Adapter is a subclass of Adaptee.

TLDR

So, in summary:

  • Python has no privacy model. There is no point in trying to enforce one here.
  • The practical reasons that necessitate the class adapter pattern in C++, don't exist in Python
  • Neither dynamic attribute proxying nor code tranformation is going to be practical in this case and introduce more problems than are being solved here.

You should instead use composition, or just accept that your adapter is both a Target and an Adaptee and so use subclassing to implement the methods required by the new interface without hiding the adaptee interface:

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