在闭包作用域内捕获函数

4
Python3新增了一个__prepare__,这样你就可以替换用于从类声明中收集项目的字典类型(参见这里)。使用__prepare__,我可以设置类以允许多个定义相同的成员函数。
class MultiType(type):

    @classmethod
    def __prepare__(metacls, name, bases, **kwds):
        return collections.defaultdict(list)


class Foo(metaclass=MultiType):
    def bar(self, x):
        return x - 1

    def bar(self, x):
        return x

在模块级别,我可以使用装饰器来进行一些技巧操作:

def capture(f):
    module = importlib.import_module(f.__module__)
    if not hasattr(module, 'my_cache'):
        setattr(module, 'my_cache', [])
    module.my_cache.append(f)

    @functools.wraps(f)
    def _wrapper(*args, **kwargs):
        return (func(*args, **kwargs) for func in module.my_cache)
    return _wrapper

@capture
def foo(x):
    return x - 1

@capture
def foo(x):
    return 42

然而,如果我在闭包内执行此操作,则可能会添加作用域为模块级别的内容,这是不应该的。

def foo(x):
    @some_awesome_deco
    def bar(y):
        return y

    @some_awesome_deco
    def bar(y):
        return 24

    return bar(x+1)

有没有一种方法可以识别和捕获闭包范围内声明的函数,以便我可以将这些函数与模块范围内声明的函数区分开,而无需引用封闭的函数(即@some_awesome_deco(foo))?
1个回答

4
如果你只需要支持CPython,那么你的装饰器可以查看sys._getframe(1)帧对象,该对象代表执行装饰器代码的执行帧。如果frame.f_locals字典是与frame.f_globals相同的对象,则您在模块级别。否则,您处于嵌套作用域中。
但是,您必须生成某种范围键; 您可以使用在f_locals中存储某些内容(这实际上不会影响实际局部变量)。只需记住,当函数退出时,局部变量(以及帧)将被清除。我会返回一个特殊的可调用对象,因此您可以在后续的装饰器调用中引用它。例如,您可以使用frame.f_locals[decorated_function.__name__]检索该对象。
请参阅inspect模块文档以获取有关帧对象上可以找到哪些属性的概述。
演示:
>>> import sys
>>> def nested_scope_detector(func):
...     frame = sys._getframe(1)
...     nested_scope = frame.f_globals is not frame.f_locals
...     redefinition = func.__name__ in frame.f_locals
...     if nested_scope: print('{!r} is located in a nested scope.'.format(func))
...     if redefinition: print('Redefining {!r}, previously bound to {!r}'.format(
...         func.__name__, frame.f_locals[func.__name__]))
...     return func
... 
>>> @nested_scope_detector
... def foo(): pass
... 
>>> @nested_scope_detector
... def foo(): pass
... 
Redefining 'foo', previously bound to <function foo at 0x10e931d08>
>>> def bar():
...     @nested_scope_detector
...     def foo(): pass
...     @nested_scope_detector
...     def foo(): pass
... 
>>> bar()
<function bar.<locals>.foo at 0x10eb4ef28> is located in a nested scope.
<function bar.<locals>.foo at 0x10eb4eea0> is located in a nested scope.
Redefining 'foo', previously bound to <function bar.<locals>.foo at 0x10eb4ef28>

因此,您可以在返回的包装函数上使用函数属性来存储您的函数:
def capture(f):
    locals = sys._getframe(1).f_locals
    preexisting = locals.get(f.__name__)
    if preexisting is not None and hasattr(preexisting, 'functions'):
        preexisting.functions.append(f)
        return preexisting

    @functools.wraps(f)
    def _wrapper(*args, **kwargs):
        return (func(*args, **kwargs) for func in _wrapper.functions)
    _wrapper.functions = [f]
    return _wrapper

而且它将在任何作用域中工作:

>>> @capture
... def foo(): return 'bar'
... 
>>> @capture
... def foo(): return 'baz'
... 
>>> foo()
<generator object <genexpr> at 0x10eb45ee8>
>>> list(foo())
['bar', 'baz']
>>> def bar():
...     @capture
...     def foo(): return 'bar'
...     @capture
...     def foo(): return 'baz'
...     return foo
... 
>>> list(bar()())
['bar', 'baz']

这会发生在导入时还是始终在运行时?我实际上没有使用过 sys._getframe 函数。 - wheaties
@wheaties:装饰器只是在装饰对象被加载时执行的函数。 - Martijn Pieters
@eryksun:这实际上比我们有权期望的支持还要多! - Martijn Pieters
@wheaties:它也可以在PyPy上运行,但是会破坏JIT - Martijn Pieters
2
@eryksun:我认为OP不应该关心范围,真的。接下来我们会看到有人在字典理解中使用装饰器来修饰lambda函数,并想知道他们走进了什么奇怪的范围.. - Martijn Pieters
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接