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

2023-12-19

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

不幸的是,库维护者的修复速度有点慢,但我们不想分叉该库。在他们解决问题之前,我们也无法暂缓发布。

我们更愿意使用猴子修补来解决这个问题,因为这比修补源代码更容易跟踪。然而,重复一个非常大的函数,只需替换内部函数就足够了,这感觉有点过分,并且让其他人更难看到我们到底改变了什么。我们是否被库鸡蛋的静态补丁所困扰?

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

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

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

我们想要替换的只是实施innerfunction()。实际的外部函数要长得多。当然,我们会重用封闭变量并维护函数签名。


Yes,您可以替换内部函数,即使它使用闭包。不过,你必须克服一些困难。请考虑:

  1. 您还需要将替换函数创建为嵌套函数,以确保 Python 创建相同的闭包。如果原始函数对名称有闭包foo and 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: {}')

The co_consts序列是一个不可变的对象,一个元组,所以我们不能只交换内部代码对象。稍后我将展示如何生成一个新的函数对象just该代码对象被替换。

如何处理闭包

接下来,我们需要介绍闭包。在编译时,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)

# ... rest of disassembly omitted ...

The 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

The 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具有更新常量的对象,然后是具有更新代码对象的新函数对象;以下函数与 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__'):
        # 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 code object with the new constants
    try:
        # Python 3.8 added code.replace(), so much more convenient!
        ncode = ocode.replace(co_consts=new_consts)
    except AttributeError:
        # older Python versions, argument counts vary so we need to check
        # for specifics.
        args = [
            ocode.co_argcount, ocode.co_nlocals, ocode.co_stacksize,
            ocode.co_flags, ocode.co_code,
            new_consts,  # replacing the constants
            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'):
            # Python 3+, insert after co_argcount
            args.insert(1, ocode.co_kwonlyargcount)
        # Python 3.8 adds co_posonlyargcount, but also has code.replace(), used above
        ncode = code(*args)

    # and a new function object using the updated code object
    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获取代码对象。

现在我们可以修补原来的外部函数,换出just内部函数:

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

您甚至可以创建新的替换内部函数,使用fewer关闭:

>>> 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. 将您的猴子补丁内部函数创建为具有相同顺序的相同闭包的嵌套函数。
  2. 使用上面的replace_inner_function()生产一个new外函数。
  3. 对原始外部函数进行猴子修补,以使用步骤 2 中生成的新外部函数。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

你可以*仅仅*用闭包修补一个嵌套函数,还是必须重复整个外部函数? 的相关文章

随机推荐

  • GC规则:如何判断一个java对象是否适合垃圾回收

    任何人都可以向我指出一组已定义的规则 以找出 Java 对象垃圾收集的资格以及一个简单的示例 对象一旦不再存在就可以进行 GC可达的来自任何线程 如果满足以下任一条件 则可以从另一个对象 A 访问对象 O A 引用了 O 或者 A 具有对
  • 将临时字符数组转换为 D 中的字符串

    我正在学习 D 语言 我很了解 C 我想做一些 Windows 特定的东西 所以我写这个只是为了尝试 API import core sys windows windows import std stdio string name char
  • Cosmos DB:网关无法直接服务跨分区查询

    知道为什么当我使用其余 api 在 Cosmos DB 中执行下面的查询时 我收到以下错误 没有 order by 的相同查询工作正常 我已经设置了标题 x ms documentdb query enablecrosspartition
  • 如何将 pd.concat 与未启动的数据帧一起使用?

    我希望能够在数据帧结果通过一个函数时将它们连接到内存中 并最终得到一个只有结果的全新数据帧 在函数之前没有准备好数据帧的情况下 如何执行此操作 例如 import pandas as pd import numpy as np rand d
  • 当以双精度形式传递浮点值时,双精度

    我对双精度有疑问 当将浮点值传递给双精度时 我会得到一些不同的结果 例如 float f 54 23f double d1 f System out println d1 输出为 54 22999954223633 有人可以解释这种行为背后
  • ASP.NET Web API 2 文件上传

    我想知道如何最好地处理文件上传以及添加到要使用 ASP NET Web API 2 上传的文件而不使用 MVC 组件的附加信息 我用谷歌搜索了网络 我可以告诉你我比我想象的更困惑 附加信息将存储在数据库和磁盘上的文件中 到目前为止 我正在构
  • 在单独的进程中运行 python

    我正在寻找一个快速的 bash 脚本或程序 它允许我在单独的进程中启动 python 脚本 最好的方法是什么 我知道这非常简单 只是好奇是否有首选方法 只需使用与号 即可在后台启动 Python 进程 Python 已经在与 BASH 脚本
  • 如何使用 asp.net http 客户端使用 retry-after 标头来轮询 API

    我对在 net 中使用 http 客户端进行 RESTful 消费不太熟悉 并且在轮询外部 API 时无法理解如何使用 retry after 标头 这是我目前必须调查的 HttpResponseMessage result null va
  • cURL 不会提示我使用 GitHub API 输入密码

    我正在跟进GitHub 的教程 https developer github com guides getting started 关于使用他们的 API 在我的 Git Bash 命令提示符中 我输入以下内容 curl i https a
  • 裁剪后,fabricjs 将裁剪的对象设置为画布的背景

    在我的fabricjs中 我正在制作画布并向其添加图像并将图像设置为背景 然后我将卡瓦斯剪裁到一定的宽度和高度 裁剪画布后 我想要一个新的画布或以裁剪区域作为背景的相同画布 全部覆盖画布的宽度和高度 或者可以使用裁剪区域的高度和宽度制作新画
  • 如何为 Android API 级别 <11 实现 CursorLoader

    我有一个包含许多列表活动 其中 5 6 个 的应用程序 并且所有活动都有来自我自己的 ContentProvider 2 的自定义游标适配器 位于 Sqlite 数据库 2 上 现在的问题是 我想实现 CursorLoader 或类似 类来
  • 将多个对象放入 HttpParams

    我在表单控件中有一些类别 我将它们发送到一个字符串数组中 如下所示 1 4 6 这是我的实际代码 let categoryIds new Array
  • 重复播种随机数生成器是合理的哈希函数吗?

    我希望生成大量随机数据 这些数据对于给定的情况是可重现的key 包含一个数字列表 a b c d e 以下是让 RNG 进入生成随机数据的状态的好方法还是明智的方法 这样对于每个 n 元组 a b c n 该数据与 相邻 n 元组的输出不相
  • TFS2008到TFS2010迁移升级

    All 我目前正在尝试创建一个可重复的过程 将 TFS 2008 安装升级到新硬件 Microsoft 称为迁移升级 但在新硬件上构建 VS 2008 项目时遇到问题 我们的 TFS 2008 安装由两台机器组成 一个包含 SQL 数据库和
  • ChartJS - 具有多个环的圆环图

    是否可以使用 ChartJS 创建具有多个环的圆环图 如下所示 您可以在以下位置找到解决方案小提琴链接 https jsfiddle net zuupzpLL var ctx document getElementById chart ar
  • 如何知道用户何时将输出从 erlang shell 重定向到文件

    我有一个示例模块 module helloworld compile export all main gt io format s s s n e 31m Hello world e 0m 当我构建时 erlc helloworld erl
  • Django 模板对象类型

    好吧 这是我的情况 我有一组通用对象 我正在 django 模板中对其进行迭代 这些对象有许多子类 我想在模板中找出我正在处理的子类 这可能吗 可取吗 该代码可能看起来类似于 其中 if 语句包含一些虚构的语法 table tr th na
  • java.awt.Frame.setBackground() 在 OS X 中不起作用

    我正在尝试解决 OS X 中 java 小程序中的一些 UI 渲染错误 但我遇到了一个我无法解决的问题 我们打开的所有扩展 java awt Frame 的窗口似乎都忽略了 setBackground 调用 而是使用 OS X 默认值 拉丝
  • 根据日期精度设置日期格式

    我正在使用维基数据查询服务来获取某些地方的人口日期 输出给出以下格式的原始结果2010 01 01T00 00 00Z 举个例子 我还检索了每个日期的日期精度 这query https query wikidata org SELECT 2
  • 你可以*仅仅*用闭包修补一个嵌套函数,还是必须重复整个外部函数?

    我们使用的第三方库包含一个相当长的函数 该函数在其中使用了嵌套函数 我们对该库的使用触发了该函数中的错误 我们非常希望解决该错误 不幸的是 库维护者的修复速度有点慢 但我们不想分叉该库 在他们解决问题之前 我们也无法暂缓发布 我们更愿意使用