【问题标题】:Correctly replace a function's code object正确替换函数的代码对象
【发布时间】:2019-02-09 01:26:57
【问题描述】:

我正在尝试获取函数的源代码,向其中添加代码,然后将其放回原始函数中。

基本上是这样的:

new_code = change_code(original_code)
throwaway_module = ModuleType('m')
exec(new_code, throwaway_module.__dict__)
func.__code__ = getattr(throwaway_module, func.__name__).__code__

new_code 不包含任何不在原始函数中的名称时,这非常有效。

但是,当 new_code 包含原始 func 中不存在的变量名称时,在最后一行我收到以下错误:

ValueError: func() requires a code object with 1 free vars, not 0

有什么想法吗?

编辑:

似乎我已经找到了在 CPython 源代码中引发此异常的位置(文件 funcobject.c)。为清楚起见,省略了一些行:

static int
func_set_code(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
{
    Py_ssize_t nfree, nclosure;

    // ... lines omitted

    nfree = PyCode_GetNumFree((PyCodeObject *)value);
    nclosure = (op->func_closure == NULL ? 0 :
            PyTuple_GET_SIZE(op->func_closure));
    if (nclosure != nfree) {
        PyErr_Format(PyExc_ValueError,
                     "%U() requires a code object with %zd free vars,"
                     " not %zd",
                     op->func_name,
                     nclosure, nfree);
        return -1;
    }
    Py_INCREF(value);
    Py_XSETREF(op->func_code, value);
    return 0;
}

这对你有帮助吗? :)

【问题讨论】:

    标签: python metaprogramming cpython


    【解决方案1】:

    此异常是由于尝试将代码对象分配给一个函数,该函数关闭的变量数量与其来自的函数不同。如果这句话听起来像胡言乱语,那么你应该看看this answer

    避免此问题的最简单方法是以显而易见的方式重新分配现有名称,即f = g 而不是f.__code__ = g.__code__。通过这种方式,代码对象总是与匹配的闭包保持一致(稍后会详细介绍)。在你的情况下,这看起来像func = getattr(throwaway_module, func.__name__)。有什么原因你不能这样做,而是在搞内部实现细节吗?

    为了更好地说明这里发生了什么,假设我们有一些愚蠢的函数。

    def dog():
        return "woof"
    
    def cat():
        return "meow"
    
    def do_stuff(seq):
        t1 = sum(seq)
        seq2 = [e + t1 for e in seq]
        t2 = sum(seq2)
        return t1 + t2
    
    def pair(animal):
        def ret():
            return animal() + animal()
        return ret
    
    cats = pair(cat)
    
    print(dog()) # woof
    print(cat()) # meow
    print(cats()) # meowmeow
    print(do_stuff([1,2,3])) # 30
    

    即使do_stuff 的局部变量数量与dog 不同,我们仍然可以成功地在它们之间重新分配代码对象。

    do_stuff.__code__ = dog.__code__
    print(do_stuff()) # woof
    

    但是,我们不能在 catsdog 之间重新分配,因为 cats 关闭了参数 animal

    print(cats.__code__.co_freevars) # ('animal',)
    dog.__code__ = cats.__code__
    

    ValueError: dog() requires a code object with 0 free vars, not 1

    只需将名称重新分配给所需的函数对象即可避免此问题。

    dog = cats
    print(dog()) # meowmeow
    

    事实上,如果您成功地为带有闭包的函数重新分配代码对象,那么如果函数被执行,事情很可能不会按预期进行。这是因为封闭变量与编译后的代码是分开保存的,所以它们不会匹配。

    def get_sum_func(numbers):
        def ret():
            return sum(numbers)
        return ret
    
    sum_func = get_sum_func([2,2,2]) # sum_func closes over the provided arg
    
    # swap code objects
    # quite possibly the most disturbing single line of python I've ever written
    sum_func.__code__, cats.__code__ = (cats.__code__, sum_func.__code__)
    
    print(sum_func()) # this will attempt to execute numbers() + numbers(), which will throw
    print(cats()) # this will attempt to execute sum(animal), which will throw
    

    事实证明,我们不能轻易替换__closure__ 属性,因为它是只读的。如果你真的下定决心,你大概可以work around it,但这几乎可以肯定是个糟糕的主意。

    # swap closures
    # this results in "AttributeError: readonly attribute"
    sum_func.__closure__, cats.__closure__ = (cats.__closure__, sum_func.__closure__)
    

    有关函数对象属性的更多详细信息,请参阅this answerthe docs

    【讨论】:

      猜你喜欢
      • 2011-10-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多