正确替换函数的代码对象

时间:2019-02-09 01:26:57

标签: python metaprogramming cpython

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

基本上像这样:

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;
}

这对您有帮助吗? :)

1 个答案:

答案 0 :(得分:0)

此异常是由于尝试将代码对象分配给一个函数而导致的,该函数关闭的变量数量与其来源的函数不同。如果该句子听起来像胡言乱语,那么您应该看看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_stuffdog的局部变量数量不同,我们仍然可以在它们之间成功地重新分配代码对象。

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

实际上,如果您成功地为带有闭包的函数完成了代码对象的重新分配,则如果执行该函数,事情很可能不会按预期进行。这是因为close结束变量与已编译代码分开保存,因此它们不匹配。

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