如何在动态创建 class 时使用 super() 传递函数?

How to pass function with super() when creating class dynamically?

假设我有这个代码:

class StaticParent:
  def _print(self):
    print("I'm StaticParent")

class StaticChild(StaticParent):
  def _print(self):
    print('StaticChild saying: ')
    super()._print()

def _parent_print_proto(self):
  print("I'm DynamicParent")

def _child_print_proto(self):
  print('DynamicChild saying: ')
  super()._print()

DynamicParent = type('DynamicParent', tuple([]), {"_print": _parent_print_proto})

DynamicChild = type('DynamicChild', (DynamicParent,), {"_print": _child_print_proto})

sc = StaticChild()
sc._print()

dc = DynamicChild()
dc._print()

它的输出是:

StaticChild saying:
I'm StaticParent
DynamicChild saying:
Traceback (most recent call last):
  File "/tmp/example.py", line 28, in <module>
    dc._print()
  File "/tmp/example.py", line 17, in _child_print_proto
    super()._print()
RuntimeError: super(): __class__ cell not found

所以问题是如何为调用 super() 的许多 类 创建原型方法?

PS 我尝试用 lambda 实现方法,但它也不起作用:
DynamicChildWithLambda = type('DynamicChild', (DynamicParent,), {"_print": lambda self : print('Lambda saying: ', super()._print())})
Traceback (most recent call last):
  File "/tmp/example.py", line 30, in <module>
    dcwl._print()
  File "/tmp/example.py", line 23, in <lambda>
    DynamicChildWithLambda = type('DynamicChild', (DynamicParent,), {"_print": lambda self : print('Lambda saying: ', super()._print())})
RuntimeError: super(): __class__ cell not found
PS2 我也这样试过:
class StaticParent:

  def _print(self):
    print("I'm StaticParent")

def _child_print_proto(self):
  print('DynamicChild saying: ')
  super(StaticParent, self)._print()

DynamicChild = type('DynamicChild', (StaticParent,), {"_print": _child_print_proto})

dc = DynamicChild()
dc._print()
DynamicChild saying:
Traceback (most recent call last):
  File "/tmp/example.py", line 13, in <module>
    dc._print()
  File "/tmp/example.py", line 8, in _child_print_proto
    super(StaticParent, self)._print()
AttributeError: 'super' object has no attribute '_print'

在 class 中定义的方法会得到一个伪造的闭包作用域,该作用域会自动提供它在 no-arg super() 中定义的 class。当在 class 之外定义时,它不能执行此操作(因为在您定义方法时显然没有定义 class)。但是您仍然可以通过 old-fashioned 方式进行闭包,方法是实际编写一个您手动定义 __class__ 的闭包函数:

class StaticParent:
  def _print(self):
    print("I'm StaticParent")

class StaticChild(StaticParent):
  def _print(self):
    print('StaticChild saying: ')
    super()._print()

def _parent_print_proto(self):
    print("I'm DynamicParent")

# Nesting allows us to make the inner function have an appropriate __class__
# defined for use by no-arg super
def _make_child_print_proto(cls):
  __class__ = cls
  def _child_print_proto(self):
    print('DynamicChild saying: ')
    super()._print()
  return _child_print_proto

DynamicParent = type('DynamicParent', tuple([]), {"_print": _parent_print_proto})

DynamicChild = type('DynamicChild', (DynamicParent,), {})
# Need DynamicChild to exist to use it as __class__, bind after creation
DynamicChild._print = _make_child_print_proto(DynamicChild)

sc = StaticChild()
sc._print()

dc = DynamicChild()
dc._print()

Try it online!

是的,它很老套而且很糟糕。在实际代码中,我只使用不太常见的显式 two-arg super:

def _child_print_proto(self):
  print('DynamicChild saying: ')
  super(DynamicChild, self)._print()

这就是 super() 所做的一切; Python 隐藏了它在闭包作用域中定义为 __class__ 的 class,super() 拉出它和第一个位置参数,假定为 self,并且隐含地two-arg 表单明确地做同样的事情。

明确一点:不能手动将self.__class__作为第一个参数传递给two-argsuper(),到模拟在闭包作用域中绑定的 __class__ 它似乎有效,并且确实有效,直到您使用该方法实际创建 class 的子项并尝试调用它的方法(super() 的全部要点是你可能有一个任意复杂的 class 层次结构来导航;你不能只说“哦,但我的 class 足够特别再也不会被 subclassed").如果你做一些像添加这样简单的事情:

class DynamicGrandChild(DynamicChild):
    pass

dgc = DynamicGrandChild()
dgc._print()

self.__class__-使用来自 Epsi95's answer 的代码,您将看到:

StaticChild saying: 
I'm StaticParent
DynamicChild saying: 
I'm DynamicParent
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
DynamicChild saying: 
... repeats a thousand times or so ...
DynamicChild saying: 
DynamicChild saying: 
Traceback (most recent call last):
  File ".code.tio", line 31, in <module>
    dgc._print()
  File ".code.tio", line 15, in _child_print_proto
    super(self.__class__, self)._print()
  File ".code.tio", line 15, in _child_print_proto
    super(self.__class__, self)._print()
  File ".code.tio", line 15, in _child_print_proto
    super(self.__class__, self)._print()
  [Previous line repeated 994 more times]
  File ".code.tio", line 14, in _child_print_proto
    print('DynamicChild saying: ')
RecursionError: maximum recursion depth exceeded while calling a Python object

super() 在设计继承时使用。 super(self.__class__, self) 仅在您 破坏 继承时使用。唯一安全的方法是对 class 进行某种静态链接(该方法将附加到 而不是 运行 时间类型调用它的任何实例),我上面的两个解决方案是执行此操作的两种合理方法(实际上只有一个其他选项,它正在关闭,并且仍然传递 class 和 self 明确地,例如,而不是定义 __class__,只需使用 super(cls, self) 来明确地使用闭包变量;不够明显,并且有点两全其美)。


所以,我说“有三种方法”,但实际上有一种 稍微 更好(但也不太便携,因为 API 用于 types.FunctionType 随着时间的推移发生了变化,即使通常定义的闭包没有)解决方案,它使您可以创建一个实用函数来将任意函数绑定到任意 classes,而不是要求您将每个这样的函数包装在一个 closure-maker。那就是直接将函数重建为闭包:

import types

# Define function normally, with super(). The function can't actually be used as is though
def _child_print_proto(self):
  print('DynamicChild saying: ')
  super()._print()

# Utility function binding arbitrary function to arbitrary class
def bind_to_class(cls, func):
  # This translates as:
  # Make a new function using the same code object as the passed function,
  # but tell it it has one closure scoped variable named __class__, and
  # provide the class as the value to associate with it.
  # This requires Python 3.8 or later (code objects didn't have a replace method
  # until then, so doing this would be even uglier than it already is)
  return types.FunctionType(func.__code__.replace(co_freevars=('__class__',)), func.__globals__, closure=(types.CellType(cls),))

DynamicParent = type('DynamicParent', tuple([]), {"_print": _parent_print_proto})

DynamicChild = type('DynamicChild', (DynamicParent,), {})
# Rebind the function to a new function that believes it was defined in DynamicChild
DynamicChild._print = bind_to_class(DynamicChild, _child_print_proto)

就像我说的,它是 super-ugly 并且需要在 3.8 之前重写,但它确实有一个轻微的优势,允许您使用 bind_to_class 和任意 super() 使用函数来将它们绑定到任意 classes。我仍然认为手动调用 two-arg super 是 safe/obvious 的方法,但现在您已经有了所有选项。