Python 来自外部作用域的函数本地名称绑定

Python 来自外部作用域的函数本地名称绑定,python,scope,decorator,Python,Scope,Decorator,我需要一种从外部代码块向函数“注入”名称的方法,这样它们就可以在本地访问,它们不需要由函数的代码专门处理(定义为函数参数,从*args等加载) 简化场景:提供一个框架,用户可以在其中定义(尽可能少的语法)自定义函数来操作框架的其他对象(不一定是全局) 理想情况下,用户定义 def user_func(): Mouse.eat(Cheese) if Cat.find(Mouse): Cat.happy += 1 这里的Cat、Mouse和Cheese都是框架对象,

我需要一种从外部代码块向函数“注入”名称的方法,这样它们就可以在本地访问它们不需要由函数的代码专门处理(定义为函数参数,从
*args
等加载)

简化场景:提供一个框架,用户可以在其中定义(尽可能少的语法)自定义函数来操作框架的其他对象(不一定是全局

理想情况下,用户定义

def user_func():
    Mouse.eat(Cheese)
    if Cat.find(Mouse):
        Cat.happy += 1
这里的
Cat
Mouse
Cheese
都是框架对象,出于充分的理由,它们不能绑定到全局名称空间

我想为该函数编写一个包装器,其行为如下:

def framework_wrap(user_func):
    # this is a framework internal and has name bindings to Cat, Mouse and Cheese
    def f():
        inject(user_func, {'Cat': Cat, 'Mouse': Mouse, 'Cheese': Cheese})
        user_func()
    return f
然后,这个包装器可以应用于所有用户定义的函数(作为装饰器,由用户自己或自动地,尽管我计划使用元类)

我知道Python 3的代码<代码>非本地< /Cord>关键字,但是我仍然认为丑陋(从框架的用户角度)添加一个附加的行:

nonlocal Cat, Mouse, Cheese
还要担心把他需要的所有东西都加到这条线上


非常感谢您的任何建议。

如果您的应用程序严格地说是Python 3,那么我不认为使用Python 3的
非本地
比编写修饰程序来操作函数的本地名称空间更难看。我建议您尝试一下非本地的解决方案,或者重新考虑一下这个策略。

听起来您可能想在dict
中使用
exec代码,其中
code
是用户的功能,
dict
是您提供的字典,可以

  • 预填充对用户代码应该能够使用的对象的引用
  • 存储用户代码声明的任何函数或变量,以供您的框架稍后使用
执行官的文件:

但是,我非常确定,只有当用户的代码作为字符串引入并且您需要执行它时,这才有效。如果函数已经编译,那么它将已经设置了全局绑定。因此,在框架dict中执行类似于
exec“user\u func(*args)”的操作将不起作用,因为
user\u func
的全局变量已设置为定义它的模块

由于
func\u globals
是只读的,我认为您必须执行类似的操作才能修改函数globals

我认为(除非您正在做一些前所未有的了不起的事情,或者我缺少一些关键的细节),您可能最好将框架对象放入一个模块中,然后让用户代码导入该模块。一旦模块被
import
ed,模块变量可以很容易地通过在该模块之外定义的代码重新分配、变异或访问

我认为这对代码的可读性也会更好,因为
user\u func
最终将为
Cat
Dog
等使用显式名称空间,而不是让不熟悉您的框架的读者怀疑它们来自何处。例如,
animal\u farm.Mouse.eat(animal\u farm.Cheese)
,或者类似的行

from animal_farm import Goat
cheese = make_cheese(Goat().milk())

如果您正在做一些前所未有的了不起的事情,我认为您需要使用C API将参数传递给代码对象。看起来该函数就是您想要的函数。

编辑的答案--在调用
user\u func()后恢复命名空间dict

使用Python 2.7.5和3.3.2进行测试

文件framework.py:

# framework objects
class Cat: pass
class Mouse: pass
class Cheese: pass

_namespace = {'Cat':Cat, 'Mouse':Mouse, 'Cheese':Cheese } # names to be injected

# framework decorator
from functools import wraps
def wrap(f):
    func_globals = f.func_globals if hasattr(f,'func_globals') else f.__globals__
    @wraps(f)
    def wrapped(*args, **kwargs):
        # determine which names in framework's _namespace collide and don't
        preexistent = set(name for name in _namespace if name in func_globals)
        nonexistent = set(name for name in _namespace if name not in preexistent)
        # save any preexistent name's values
        f.globals_save = {name: func_globals[name] for name in preexistent}
        # temporarily inject framework's _namespace
        func_globals.update(_namespace)

        retval = f(*args, **kwargs) # call function and save return value

        # clean up function's namespace
        for name in nonexistent:
             del func_globals[name] # remove those that didn't exist
        # restore the values of any names that collided
        func_globals.update(f.globals_save)
        return retval

    return wrapped
用法示例:

from __future__ import print_function
import framework

class Cat: pass  # name that collides with framework object

@framework.wrap
def user_func():
    print('in user_func():')
    print('  Cat:', Cat)
    print('  Mouse:', Mouse)
    print('  Cheese:', Cheese)

user_func()

print()
print('after user_func():')
for name in framework._namespace:
    if name in globals():
        print('  {} restored to {}'.format(name, globals()[name]))
    else:
        print('  {} not restored, does not exist'.format(name))
输出:

用户函数()中的
:
类别:
鼠标:
奶酪:
在user_func()之后:
奶酪没有恢复,就不存在
鼠标未还原,不存在
猫恢复健康

我越是乱动堆栈,就越希望我没有乱动。不要让globals做你想做的事。改为破解字节码。我可以想出两种方法来做到这一点

1) 添加单元格,将所需的引用包装到
f.func\u闭包中
。您必须重新组合函数的字节码以使用
LOAD\u DEREF
而不是
LOAD\u GLOBAL
,并为每个值生成一个单元格。然后将单元格的元组和新代码对象传递给
types.FunctionType
,并获得具有适当绑定的函数。函数的不同副本可以有不同的本地绑定,因此它应该是线程安全的

2) 在函数参数列表的末尾添加新局部变量的参数。将适当出现的
LOAD\u GLOBAL
替换为
LOAD\u FAST
。然后使用
types.FunctionType
构造一个新函数,并传入新的代码对象和一个您想要作为默认选项的绑定元组。这是有限的,因为python将函数参数限制为255,并且不能用于使用变量参数的函数。尽管如此,我还是觉得这两种方法中更具挑战性,所以这就是我实现的方法(另外还有其他方法可以用这个方法实现)。同样,您可以使用不同的绑定创建函数的不同副本,也可以从每个调用位置使用所需的绑定调用函数。因此,它也可以是线程安全的,因为你想使它

import types
import opcode

# Opcode constants used for comparison and replacecment
LOAD_FAST = opcode.opmap['LOAD_FAST']
LOAD_GLOBAL = opcode.opmap['LOAD_GLOBAL']
STORE_FAST = opcode.opmap['STORE_FAST']

DEBUGGING = True

def append_arguments(code_obj, new_locals):
    co_varnames = code_obj.co_varnames   # Old locals
    co_names = code_obj.co_names      # Old globals
    co_argcount = code_obj.co_argcount     # Argument count
    co_code = code_obj.co_code         # The actual bytecode as a string

    # Make one pass over the bytecode to identify names that should be
    # left in code_obj.co_names.
    not_removed = set(opcode.hasname) - set([LOAD_GLOBAL])
    saved_names = set()
    for inst in instructions(co_code):
        if inst[0] in not_removed:
            saved_names.add(co_names[inst[1]])

    # Build co_names for the new code object. This should consist of 
    # globals that were only accessed via LOAD_GLOBAL
    names = tuple(name for name in co_names
                  if name not in set(new_locals) - saved_names)

    # Build a dictionary that maps the indices of the entries in co_names
    # to their entry in the new co_names
    name_translations = dict((co_names.index(name), i)
                             for i, name in enumerate(names))

    # Build co_varnames for the new code object. This should consist of
    # the entirety of co_varnames with new_locals spliced in after the
    # arguments
    new_locals_len = len(new_locals)
    varnames = (co_varnames[:co_argcount] + new_locals +
                co_varnames[co_argcount:])

    # Build the dictionary that maps indices of entries in the old co_varnames
    # to their indices in the new co_varnames
    range1, range2 = xrange(co_argcount), xrange(co_argcount, len(co_varnames))
    varname_translations = dict((i, i) for i in range1)
    varname_translations.update((i, i + new_locals_len) for i in range2)

    # Build the dictionary that maps indices of deleted entries of co_names
    # to their indices in the new co_varnames
    names_to_varnames = dict((co_names.index(name), varnames.index(name))
                             for name in new_locals)

    if DEBUGGING:
        print "injecting: {0}".format(new_locals)
        print "names: {0} -> {1}".format(co_names, names)
        print "varnames: {0} -> {1}".format(co_varnames, varnames)
        print "names_to_varnames: {0}".format(names_to_varnames)
        print "varname_translations: {0}".format(varname_translations)
        print "name_translations: {0}".format(name_translations)


    # Now we modify the actual bytecode
    modified = []
    for inst in instructions(code_obj.co_code):
        # If the instruction is a LOAD_GLOBAL, we have to check to see if
        # it's one of the globals that we are replacing. Either way,
        # update its arg using the appropriate dict.
        if inst[0] == LOAD_GLOBAL:
            print "LOAD_GLOBAL: {0}".format(inst[1])
            if inst[1] in names_to_varnames:
                print "replacing with {0}: ".format(names_to_varnames[inst[1]])
                inst[0] = LOAD_FAST
                inst[1] = names_to_varnames[inst[1]]
            elif inst[1] in name_translations:    
                inst[1] = name_translations[inst[1]]
            else:
                raise ValueError("a name was lost in translation")
        # If it accesses co_varnames or co_names then update its argument.
        elif inst[0] in opcode.haslocal:
            inst[1] = varname_translations[inst[1]]
        elif inst[0] in opcode.hasname:
            inst[1] = name_translations[inst[1]]
        modified.extend(write_instruction(inst))

    code = ''.join(modified)
    # Done modifying codestring - make the code object

    return types.CodeType(co_argcount + new_locals_len,
                          code_obj.co_nlocals + new_locals_len,
                          code_obj.co_stacksize,
                          code_obj.co_flags,
                          code,
                          code_obj.co_consts,
                          names,
                          varnames,
                          code_obj.co_filename,
                          code_obj.co_name,
                          code_obj.co_firstlineno,
                          code_obj.co_lnotab)


def instructions(code):
    code = map(ord, code)
    i, L = 0, len(code)
    extended_arg = 0
    while i < L:
        op = code[i]
        i+= 1
        if op < opcode.HAVE_ARGUMENT:
            yield [op, None]
            continue
        oparg = code[i] + (code[i+1] << 8) + extended_arg
        extended_arg = 0
        i += 2
        if op == opcode.EXTENDED_ARG:
            extended_arg = oparg << 16
            continue
        yield [op, oparg]

def write_instruction(inst):
    op, oparg = inst
    if oparg is None:
        return [chr(op)]
    elif oparg <= 65536L:
        return [chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)]
    elif oparg <= 4294967296L:
        return [chr(opcode.EXTENDED_ARG),
                chr((oparg >> 16) & 255),
                chr((oparg >> 24) & 255),
                chr(op),
                chr(oparg & 255),
                chr((oparg >> 8) & 255)]
    else:
        raise ValueError("Invalid oparg: {0} is too large".format(oparg))



if __name__=='__main__':
    import dis

    class Foo(object):
        y = 1

    z = 1
    def test(x):
        foo = Foo()
        foo.y = 1
        foo = x + y + z + foo.y
        print foo

    code_obj = append_arguments(test.func_code, ('y',))
    f = types.FunctionType(code_obj, test.func_globals, argdefs=(1,))
    if DEBUGGING:
        dis.dis(test)
        print '-'*20
        dis.dis(f)
    f(1)
导入类型
导入操作码
#用于比较和替换的操作码常量
LOAD\u FAST=opcode.opmap['LOAD\u FAST']
LOAD_GLOBAL=opcode.opmap['LOAD_GLOBAL']
STORE\u FAST=opcode.opmap['STORE\u FAST']
调试=真
def append_参数(代码_obj,新的_局部变量):
co_varnames=code_obj.co_varnames#旧本地人
co_名称=代码_obj.co_名称#旧全局
co_argcount=code_obj.co_argcount#参数计数
co_code=code_obj.co_code#作为字符串的实际字节码
#对字节码进行一次遍历,以标识应该使用的名称
#遗留代码_
import types
import opcode

# Opcode constants used for comparison and replacecment
LOAD_FAST = opcode.opmap['LOAD_FAST']
LOAD_GLOBAL = opcode.opmap['LOAD_GLOBAL']
STORE_FAST = opcode.opmap['STORE_FAST']

DEBUGGING = True

def append_arguments(code_obj, new_locals):
    co_varnames = code_obj.co_varnames   # Old locals
    co_names = code_obj.co_names      # Old globals
    co_argcount = code_obj.co_argcount     # Argument count
    co_code = code_obj.co_code         # The actual bytecode as a string

    # Make one pass over the bytecode to identify names that should be
    # left in code_obj.co_names.
    not_removed = set(opcode.hasname) - set([LOAD_GLOBAL])
    saved_names = set()
    for inst in instructions(co_code):
        if inst[0] in not_removed:
            saved_names.add(co_names[inst[1]])

    # Build co_names for the new code object. This should consist of 
    # globals that were only accessed via LOAD_GLOBAL
    names = tuple(name for name in co_names
                  if name not in set(new_locals) - saved_names)

    # Build a dictionary that maps the indices of the entries in co_names
    # to their entry in the new co_names
    name_translations = dict((co_names.index(name), i)
                             for i, name in enumerate(names))

    # Build co_varnames for the new code object. This should consist of
    # the entirety of co_varnames with new_locals spliced in after the
    # arguments
    new_locals_len = len(new_locals)
    varnames = (co_varnames[:co_argcount] + new_locals +
                co_varnames[co_argcount:])

    # Build the dictionary that maps indices of entries in the old co_varnames
    # to their indices in the new co_varnames
    range1, range2 = xrange(co_argcount), xrange(co_argcount, len(co_varnames))
    varname_translations = dict((i, i) for i in range1)
    varname_translations.update((i, i + new_locals_len) for i in range2)

    # Build the dictionary that maps indices of deleted entries of co_names
    # to their indices in the new co_varnames
    names_to_varnames = dict((co_names.index(name), varnames.index(name))
                             for name in new_locals)

    if DEBUGGING:
        print "injecting: {0}".format(new_locals)
        print "names: {0} -> {1}".format(co_names, names)
        print "varnames: {0} -> {1}".format(co_varnames, varnames)
        print "names_to_varnames: {0}".format(names_to_varnames)
        print "varname_translations: {0}".format(varname_translations)
        print "name_translations: {0}".format(name_translations)


    # Now we modify the actual bytecode
    modified = []
    for inst in instructions(code_obj.co_code):
        # If the instruction is a LOAD_GLOBAL, we have to check to see if
        # it's one of the globals that we are replacing. Either way,
        # update its arg using the appropriate dict.
        if inst[0] == LOAD_GLOBAL:
            print "LOAD_GLOBAL: {0}".format(inst[1])
            if inst[1] in names_to_varnames:
                print "replacing with {0}: ".format(names_to_varnames[inst[1]])
                inst[0] = LOAD_FAST
                inst[1] = names_to_varnames[inst[1]]
            elif inst[1] in name_translations:    
                inst[1] = name_translations[inst[1]]
            else:
                raise ValueError("a name was lost in translation")
        # If it accesses co_varnames or co_names then update its argument.
        elif inst[0] in opcode.haslocal:
            inst[1] = varname_translations[inst[1]]
        elif inst[0] in opcode.hasname:
            inst[1] = name_translations[inst[1]]
        modified.extend(write_instruction(inst))

    code = ''.join(modified)
    # Done modifying codestring - make the code object

    return types.CodeType(co_argcount + new_locals_len,
                          code_obj.co_nlocals + new_locals_len,
                          code_obj.co_stacksize,
                          code_obj.co_flags,
                          code,
                          code_obj.co_consts,
                          names,
                          varnames,
                          code_obj.co_filename,
                          code_obj.co_name,
                          code_obj.co_firstlineno,
                          code_obj.co_lnotab)


def instructions(code):
    code = map(ord, code)
    i, L = 0, len(code)
    extended_arg = 0
    while i < L:
        op = code[i]
        i+= 1
        if op < opcode.HAVE_ARGUMENT:
            yield [op, None]
            continue
        oparg = code[i] + (code[i+1] << 8) + extended_arg
        extended_arg = 0
        i += 2
        if op == opcode.EXTENDED_ARG:
            extended_arg = oparg << 16
            continue
        yield [op, oparg]

def write_instruction(inst):
    op, oparg = inst
    if oparg is None:
        return [chr(op)]
    elif oparg <= 65536L:
        return [chr(op), chr(oparg & 255), chr((oparg >> 8) & 255)]
    elif oparg <= 4294967296L:
        return [chr(opcode.EXTENDED_ARG),
                chr((oparg >> 16) & 255),
                chr((oparg >> 24) & 255),
                chr(op),
                chr(oparg & 255),
                chr((oparg >> 8) & 255)]
    else:
        raise ValueError("Invalid oparg: {0} is too large".format(oparg))



if __name__=='__main__':
    import dis

    class Foo(object):
        y = 1

    z = 1
    def test(x):
        foo = Foo()
        foo.y = 1
        foo = x + y + z + foo.y
        print foo

    code_obj = append_arguments(test.func_code, ('y',))
    f = types.FunctionType(code_obj, test.func_globals, argdefs=(1,))
    if DEBUGGING:
        dis.dis(test)
        print '-'*20
        dis.dis(f)
    f(1)