是的,即使使用闭包,也可以替换内部函数。但是,您将不得不跳过几圈。请考虑:
您还需要将替换函数创建为嵌套函数,以确保Python创建相同的闭包。如果原始函数在名称
foo
和上有一个闭包,则bar
需要将替换定义为闭有相同名称的嵌套函数。更重要的是,您需要以 相同的顺序 使用这些名称;闭包由索引引用。猴子修补程序始终很脆弱,并且可能随着实现的更改而中断。这也不例外。每当您更改修补程序库的版本时,请重新测试您的猴子修补程序。
为了理解它是如何工作的,我将首先解释Python如何处理嵌套函数。Python使用 代码
对象根据需要生成函数对象。每个代码对象都有一个关联的常量序列,嵌套函数的代码对象按该序列存储:
>>> def outerfunction(*args):... def innerfunction(val):... return someformat.format(val)... someformat = 'Foo: {}'... for arg in args:... yield innerfunction(arg)... >>> outerfunction.__pre__<pre object outerfunction at 0x105b27ab0, file "<stdin>", line 1>>>> outerfunction.__pre__.co_consts(None, <pre object innerfunction at 0x10f136ed0, file "<stdin>", line 2>, 'outerfunction.<locals>.innerfunction', 'Foo: {}')
该
co_consts序列是一个不变的对象,一个元组,因此我们不能只交换内部代码对象。再告诉我们如何将产生一个新的函数对象与 刚才 的代码替换对象。
接下来,我们需要介绍闭包。在编译时,Python确定a)
someformat不是in的本地名称,
innerfunction并且b)它在中的相同名称之上
outerfunction。Python不仅会生成字节码以产生正确的名称查找,而且还将对嵌套函数和外部函数的代码对象进行注释,以记录
someformat将要关闭的代码:
>>> outerfunction.__pre__.co_cellvars('someformat',)>>> outerfunction.__pre__.co_consts[1].co_freevars('someformat',)
您要确保替换内部代码对象仅将那些相同的名称列出为自由变量,并且这样做的顺序相同。
关闭是在运行时创建的;产生它们的字节码是外部函数的一部分:
>>> import dis>>> dis.dis(outerfunction) 20 LOAD_CLOSURE 0 (someformat) 2 BUILD_TUPLE 1 4 LOAD_ConST 1 (<pre 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)# ... rest of disassembly omitted ...
LOAD_CLOSURE那里的字节码为
someformat变量创建了一个闭包。Python按照 在内部函数中首次使用的顺序
创建与该函数使用的闭合一样多的闭合。这是以后要记住的重要事实。函数本身按位置查找这些闭包:
>>> dis.dis(outerfunction.__pre__.co_consts[1]) 30 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如果使用相同的名称,您可以比较元组。
现在是交换技巧。函数是对象,就像Python中的其他对象一样,是特定类型的实例。该类型通常不会公开,但是
type()调用仍会返回它。这同样适用于代码对象,并且两种类型甚至都有文档:
>>> type(outerfunction)<type 'function'>>>> print(type(outerfunction).__doc__)Create a function object. pre a pre object globals the globals dictionary name a string that overrides the name from the pre object argdefs a tuple that specifies the default argument values closure a tuple that supplies the bindings for free variables>>> type(outerfunction.__pre__)<type 'pre'>>>> print(type(outerfunction.__pre__).__doc__)pre(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, prestring, constants, names, varnames, filename, name, firstlineno, lnotab[, freevars[, cellvars]])Create a pre object. Not for the faint of heart.
(确切的参数计数和文档字符串在不同的Python版本之间有所不同; Python 3.0添加了该
kwonlyargcount参数,从Python
3.8开始,添加了posonlyargcount)。
我们将使用这些类型对象来生成
pre具有更新的常量的新对象,然后生成具有更新的代码对象的新功能对象;以下函数与Python 2.7至3.8版本兼容。
def replace_inner_function(outer, new_inner): """Replace a nested function pre 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, '__pre__'): # support both functions and pre objects new_inner = new_inner.__pre__ # find original pre object so we can validate the closures match opre = outer.__pre__ function, pre = type(outer), type(opre) iname = new_inner.co_name orig_inner = next( const for const in opre.co_consts if isinstance(const, pre) and const.co_name == iname) # you can ignore later closures, but since they are matched by position # the new sequence must match the start of the old. assert (orig_inner.co_freevars[:len(new_inner.co_freevars)] == new_inner.co_freevars), 'New closures must match originals' # replace the pre object for the inner function new_consts = tuple( new_inner if const is orig_inner else const for const in outer.__pre__.co_consts) # create a new pre object with the new constants try: # Python 3.8 added pre.replace(), so much more convenient! npre = opre.replace(co_consts=new_consts) except AttributeError: # older Python versions, argument counts vary so we need to check # for specifics. args = [ opre.co_argcount, opre.co_nlocals, opre.co_stacksize, opre.co_flags, opre.co_pre, new_consts, # replacing the constants opre.co_names, opre.co_varnames, opre.co_filename, opre.co_name, opre.co_firstlineno, opre.co_lnotab, opre.co_freevars, opre.co_cellvars, ] if hasattr(opre, 'co_kwonlyargcount'): # Python 3+, insert after co_argcount args.insert(1, opre.co_kwonlyargcount) # Python 3.8 adds co_posonlyargcount, but also has pre.replace(), used above npre = pre(*args) # and a new function object using the updated pre object return function( npre, 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.__pre__.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.__pre__.co_consts[1])()foo
因此,要完成图片:
- 使用相同的闭包将猴子补丁内部函数创建为嵌套函数
- 使用
replace_inner_function()
产生一个 新的 外部函数 - Monkey修补了原始外部函数,以使用在步骤2中生成的新外部函数。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)