是的,即使内部函数使用了闭包,您也可以替换它。但是您需要跳过一些障碍。请注意以下几点:
1. 您需要将替换函数创建为嵌套函数,以确保Python创建相同的闭包。如果原始函数对名称foo和bar进行闭包,则需要定义具有相同名称的闭合的替换函数。更重要的是,您需要按照相同的顺序使用这些名称;闭包按索引引用。
2. 猴子补丁始终很脆弱,并且可能会因实现更改而中断。这也不例外。每当更改已修补程序库的版本时,请重新测试猴子补丁。
代码对象
为了理解这将如何工作,我首先将解释Python如何处理嵌套函数。Python使用代码对象根据需要生成函数对象。每个代码对象都有一个关联的常量序列,并且嵌套函数的代码对象存储在该序列中:
>>> def outerfunction(*args):
... def innerfunction(val):
... return someformat.format(val)
... someformat = 'Foo: {}'
... for arg in args:
... yield innerfunction(arg)
...
>>> outerfunction.__code__
<code object outerfunction at 0x105b27ab0, file "<stdin>", line 1>
>>> outerfunction.__code__.co_consts
(None, <code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>, 'outerfunction.<locals>.innerfunction', 'Foo: {}')
co_consts
序列是一个不可变对象,即一个元组,因此我们不能仅仅交换内部代码对象。稍后我将展示如何生成一个新的函数对象,只替换那个代码对象。
如何处理闭包
接下来,我们需要涉及闭包。在编译时,Python 确定了以下内容:
a)
someformat
不是
innerfunction
中的局部名称,而
b) 它与
outerfunction
中的相同名称有关。
Python 不仅生成字节码以产生正确的名称查找,而且嵌套和外部函数的代码对象都被注释为记录要关闭
someformat
。
>>> outerfunction.__code__.co_cellvars
('someformat',)
>>> outerfunction.__code__.co_consts[1].co_freevars
('someformat',)
你需要确保替换的内部代码对象只列出相同名称的自由变量,并按照相同顺序进行列出。
闭包是在运行时创建的;生成它们的字节码是外部函数的一部分:
>>> import dis
>>> dis.dis(outerfunction)
2 0 LOAD_CLOSURE 0 (someformat)
2 BUILD_TUPLE 1
4 LOAD_CONST 1 (<code object innerfunction at 0x10f136ed0, file "<stdin>", line 2>)
6 LOAD_CONST 2 ('outerfunction.<locals>.innerfunction')
8 MAKE_FUNCTION 8 (closure)
10 STORE_FAST 1 (innerfunction)
LOAD_CLOSURE
字节码在这里为 someformat
变量创建了一个闭包;Python 会按照内部函数中首次使用的顺序创建所有使用的闭包。这是一个重要的事实,以后需要记住。函数本身通过位置查找这些闭包:
>>> dis.dis(outerfunction.__code__.co_consts[1])
3 0 LOAD_DEREF 0 (someformat)
2 LOAD_METHOD 0 (format)
4 LOAD_FAST 0 (val)
6 CALL_METHOD 1
8 RETURN_VALUE
LOAD_DEREF
操作码在这里选择了位置为
0
的闭包,以便访问
someformat
闭包。
理论上,您可以在内部函数中使用完全不同的闭包名称,但为了调试方便,最好使用相同的名称。如果使用相同的名称,则验证替换函数是否正确插入变得更加容易,因为您可以比较
co_freevars
元组。
replace_inner_function()
现在是交换技巧时间了。函数与 Python 中的任何其他对象一样,是特定类型的实例。该类型通常不公开,但是 type()
调用仍然会返回它。代码对象也适用于此,并且两种类型都有文档说明。
>>> type(outerfunction)
<type 'function'>
>>> print(type(outerfunction).__doc__)
Create a function object.
code
a code object
globals
the globals dictionary
name
a string that overrides the name from the code object
argdefs
a tuple that specifies the default argument values
closure
a tuple that supplies the bindings for free variables
>>> type(outerfunction.__code__)
<type 'code'>
>>> print(type(outerfunction.__code__).__doc__)
code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize,
flags, codestring, constants, names, varnames, filename, name,
firstlineno, lnotab[, freevars[, cellvars]])
Create a code object. Not for the faint of heart.
(Python的确切参数计数和文档字符串因各个版本而异; Python 3.0添加了kwonlyargcount参数,截至Python 3.8,posonlyargcount已被添加。)
我们将使用这些类型对象来生成一个新的code对象并更新常量,然后使用更新后的code对象生成一个新的函数对象;以下功能与Python版本2.7到3.8兼容。
def replace_inner_function(outer, new_inner):
"""Replace a nested function code object used by outer with new_inner
The replacement new_inner must use the same name and must at most use the
same closures as the original.
"""
if hasattr(new_inner, '__code__'):
new_inner = new_inner.__code__
ocode = outer.__code__
function, code = type(outer), type(ocode)
iname = new_inner.co_name
orig_inner = next(
const for const in ocode.co_consts
if isinstance(const, code) and const.co_name == iname)
assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] ==
new_inner.co_freevars), 'New closures must match originals'
new_consts = tuple(
new_inner if const is orig_inner else const
for const in outer.__code__.co_consts)
try:
ncode = ocode.replace(co_consts=new_consts)
except AttributeError:
args = [
ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize,
ocode.co_flags, ocode.co_code,
new_consts,
ocode.co_names, ocode.co_varnames, ocode.co_filename,
ocode.co_name, ocode.co_firstlineno, ocode.co_lnotab,
ocode.co_freevars, ocode.co_cellvars,
]
if hasattr(ocode, 'co_kwonlyargcount'):
args.insert(1, ocode.co_kwonlyargcount)
ncode = code(*args)
return function(
ncode, outer.__globals__, outer.__name__,
outer.__defaults__, outer.__closure__
)
上述函数验证了新创建的内部函数(可以被传入作为代码对象或者函数)确实会使用和原始函数相同的闭包。它接着会创建新的代码和函数对象,以匹配旧的
outer
函数对象,但是将嵌套的函数(通过名称定位)替换为您的猴子补丁。
让我们试一下吧
为了证明上述内容可行,我们将
innerfunction
替换为一个使每个格式化值增加2的函数:
>>> def create_inner():
... someformat = None # the actual value doesn't matter
... def innerfunction(val):
... return someformat.format(val + 2)
... return innerfunction
...
>>> new_inner = create_inner()
新的内部函数也被创建为嵌套函数;这很重要,因为它确保Python将使用正确的字节码来查找someformat闭包。我使用了一个return语句来提取函数对象,但你也可以查看create_inner.__code__.co_consts来获取代码对象。
现在我们可以修补原始外部函数,仅交换内部函数。
>>> new_outer = replace_inner_function(outerfunction, new_inner)
>>> list(outerfunction(6, 7, 8))
['Foo: 6', 'Foo: 7', 'Foo: 8']
>>> list(new_outer(6, 7, 8))
['Foo: 8', 'Foo: 9', 'Foo: 10']
原始函数回显了原始值,而新返回的值增加了2。
您甚至可以创建使用更少闭包的新替换内部函数:
>>> def demo_outer():
... closure1 = 'foo'
... closure2 = 'bar'
... def demo_inner():
... print(closure1, closure2)
... demo_inner()
...
>>> def create_demo_inner():
... closure1 = None
... def demo_inner():
... print(closure1)
...
>>> replace_inner_function(demo_outer, create_demo_inner.__code__.co_consts[1])()
foo
简而言之
所以,为了完整地说明:
- 创建一个猴子补丁内部函数,作为一个嵌套函数,闭包的顺序与原来相同。
- 使用上述的
replace_inner_function()
函数来生成一个新的外部函数。
- 对原始的外部函数进行猴子补丁,使用步骤2中生成的新外部函数。