你可以修补*只是一个带闭包的嵌套函数,还是必须重复整个外部函数?

时间:2014-12-18 15:35:43

标签: python closures monkeypatching

我们使用的第三方库包含一个使用嵌套函数的相当长的函数。我们对该库的使用会触发该函数中的错误,我们非常希望解决该错误。

不幸的是,图书馆维护者在修复方面有些慢,但我们不想要分叉库。在修复问题之前,我们也无法保留我们的版本。

我们更喜欢使用monkey-patching来修复此问题,因为这比修补源更容易跟踪。然而,要重复一个非常大的功能,只需更换内部功能就足够了,并且让其他人更难看到我们究竟改变了什么。我们是否坚持使用静态补丁到库蛋?

内部函数依赖于关闭变量;一个人为的例子是:

def outerfunction(*args):
    def innerfunction(val):
        return someformat.format(val)

    someformat = 'Foo: {}'
    for arg in args:
        yield innerfunction(arg)

我们希望只替换innerfunction()的实现。实际的外部功能远远更长。当然,我们重复使用已关闭的变量并维护函数签名。

3 个答案:

答案 0 :(得分:42)

是的,您可以替换内部函数,即使它正在使用闭包。你必须跳过一些箍。请考虑到:

  1. 您还需要将替换函数创建为嵌套函数,以确保Python创建相同的闭包。如果原始函数对名称foobar具有闭包,则需要将替换定义为具有相同名称的嵌套函数。更重要的是,您需要以相同的顺序使用这些名称​​ ;闭包由索引引用。

  2. Monkey修补程序总是很脆弱,可能会随着实现的变化而中断。这也不例外。每当您更改修补库的版本时,请重新测试您的猴子补丁。

  3. 为了理解这是如何工作的,我将首先解释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
                 10 STORE_FAST               1 (innerfunction)
    
    # ... rest of disassembly omitted ...
    

    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元组。

    现在进行交换技巧。函数是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, kwonlyargcount, nlocals, stacksize, flags, codestring,
          constants, names, varnames, filename, name, firstlineno,
          lnotab[, freevars[, cellvars]])
    
    Create a code object.  Not for the faint of heart.
    

    我们将使用这些类型对象生成具有更新常量的新code对象,然后使用更新的代码对象生成新的函数对象:

    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__'):
            # support both functions and code objects
            new_inner = new_inner.__code__
    
        # find original code object so we can validate the closures match
        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)
        # 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 code object for the inner function
        new_consts = tuple(
            new_inner if const is orig_inner else const
            for const in outer.__code__.co_consts)
    
        # create a new function object with the new constants
        return function(
            code(ocode.co_argcount, ocode.co_kwonlyargcount, 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),
            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
    

    所以,要完成图片:

    1. 使用相同的闭包创建您的monkey-patch内部函数作为嵌套函数
    2. 使用replace_inner_function()生成外部功能
    3. Monkey修补原始外部函数以使用步骤2中生成的新外部函数。

答案 1 :(得分:18)

Martijn的答案很好,但有一个缺点是很好的删除:

  

您希望确保替换内部代码对象只列出与自由变量相同的名称,并按相同的顺序进行。

这对于正常情况来说并不是一个特别困难的约束,但依赖于名称排序等未定义的行为并且当出现问题时可能存在非常令人讨厌的错误甚至可能是硬崩溃,这并不令人愉快。

我的方法有其自身的缺点,但在大多数情况下,我认为上述缺点会促使其使用它。据我所知,它也应该更便携。

基本方法是使用inspect.getsource加载源,更改它然后进行评估。这是在AST级别完成的,以便保持秩序。

以下是代码:

import ast
import inspect
import sys

class AstReplaceInner(ast.NodeTransformer):
    def __init__(self, replacement):
        self.replacement = replacement

    def visit_FunctionDef(self, node):
        if node.name == self.replacement.name:
            # Prevent the replacement AST from messing
            # with the outer AST's line numbers
            return ast.copy_location(self.replacement, node)

        self.generic_visit(node)
        return node

def ast_replace_inner(outer, inner, name=None):
    if name is None:
        name = inner.__name__

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

    # Fix the source lines for the outer AST
    outer_ast = ast.increment_lineno(outer_ast, inspect.getsourcelines(outer)[1] - 1)

    # outer_ast should be a module so it can be evaluated;
    # inner_ast should be a function so we strip the module node
    inner_ast = inner_ast.body[0]

    # Replace the function
    inner_ast.name = name
    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

    # Evaluate the modified AST in the original module's scope
    compiled = compile(modified_ast, inspect.getsourcefile(outer), "exec")
    outer_globals = outer.__globals__ if sys.version_info >= (3,) else outer.func_globals
    exec_scope = {}

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

快速演练。 AstReplaceInnerast.NodeTransformer,它只允许您通过将某些节点映射到某些其他节点来修改AST。在这种情况下,只要名称匹配,就需要replacement个节点来替换ast.FunctionDef节点。

ast_replace_inner是我们真正关心的功能,它需要两个函数和一个名称。该名称用于允许用另一个不同名称的函数替换内部函数。

解析AST:

    outer_ast = ast.parse(inspect.getsource(outer))
    inner_ast = ast.parse(inspect.getsource(inner))

进行转型:

    modified_ast = AstReplaceInner(inner_ast).visit(outer_ast)

评估代码并提取函数:

    exec(compiled, outer_globals, exec_scope)
    return exec_scope.popitem()[1]

这是一个使用示例。假设此旧代码位于buggy.py

def outerfunction():
    numerator = 10.0

    def innerfunction(denominator):
        return denominator / numerator

    return innerfunction

您想要将innerfunction替换为

def innerfunction(denominator):
    return numerator / denominator

你写道:

import buggy

def innerfunction(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, innerfunction)

或者,你可以写:

def divide(denominator):
    return numerator / denominator

buggy.outerfunction = ast_replace_inner(buggy.outerfunction, divide, "innerfunction")

这种技术的主要缺点是需要inspect.getsource来处理目标和替换。如果目标是“内置”(用C语言编写)或在分发之前编译为字节码,则会失败。请注意,如果它是内置的,Martijn的技术也不会起作用。

另一个主要缺点是来自内部功能的行号完全是螺旋式的。如果内部函数很小,这不是一个大问题,但如果你有一个很大的内部函数,这是值得考虑的。

如果未以相同方式指定函数对象,则会产生其他缺点。例如,你无法修补

def outerfunction():
    numerator = 10.0

    innerfunction = lambda denominator: denominator / numerator

    return innerfunction
同样的方式;需要不同的AST转换。

你应该决定哪种权衡对你的特定环境最有意义。

答案 2 :(得分:2)

我需要这个,但是在一个类和python2 / 3中。所以我将@ MartijnPieters的解决方案扩展了一些

import pandas as pd
df1 = pd.DataFrame(columns=['post_id','post_likes'], data={'post_id': range(6), 'post_likes': range(1,7)})
df2 = pd.DataFrame(columns=['post_id','post_shares'], data={'post_id': range(6), 'post_shares': range(11,17)})
pd.merge(df1,df2,on='post_id')

Out[12]:
   post_id  post_likes  post_shares
0        0           1           11
1        1           2           12
2        2           3           13
3        3           4           14
4        4           5           15
5        5           6           16

这应该适用于函数,绑定的类方法和未绑定的类方法。 (对于未绑定的方法,只有python3才需要class_class参数)。感谢@MartijnPieters完成大部分工作!我从来没有想过这个;)