【问题标题】:Decorator to define function-local statics - fine details of AST-munging定义函数局部静态的装饰器 - AST-munging 的详细信息
【发布时间】:2015-12-07 20:05:07
【问题描述】:

我正在尝试为常见问题“如何在 Python 中处理函数局部静态变量?”提供更好的答案。 (1, 2, 3, ...) “更好”意味着完全封装在装饰器中,可以在可能出现函数定义的任何上下文中使用。特别是当应用于方法和嵌套函数时,它必须 DTRT;它必须与应用于同一功能的其他装饰器配合得很好(以任何顺序);它必须接受静态变量的任意初始值设定项,并且不得修改修饰函数的形参列表。基本上,如果这被提议包含在标准库中,那么没有人应该能够以实施质量为由提出反对。

理想的表面语法应该是

@static_vars(a=0, b=[])
def test():
    b.append(a)
    a += 1
    sys.stdout.write(repr(b) + "\n")

我也会接受

@static_vars(a=0, b=[])
def test():
    static.b.append(static.a)
    static.a += 1
    sys.stdout.write(repr(static.b) + "\n")

或类似的,只要静态变量的命名空间是而不是函数的名称! (我打算在名称可能很长的函数中使用它。)

一个稍微更有动机的例子涉及仅与一个函数相关的预编译正则表达式:

@static_vars(encode_re = re.compile(
        br'[\x00-\x20\x7F-\xFF]|'
        br'%(?!(?:[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))')
def encode_nonascii_and_percents(segment):
    segment = segment.encode("utf-8", "surrogateescape")
    return encode_re.sub(
        lambda m: "%{:02X}".format(ord(m.group(0))).encode("ascii"),
        segment).decode("ascii")

现在,我已经有了一个主要工作的实现。装饰器重写每个函数定义,就好像它是这样读取的(使用第一个示例):

def _wrap_test_():
    a = 0
    b = 1
    def test():
        nonlocal a, b
        b.append(a)
        a += 1
        sys.stdout.write(repr(b) + "\n")
test = _wrap_test_()
del _wrap_test_

似乎实现此目的的唯一方法是使用 AST。我有适用于简单情况的代码(见下文),但我强烈怀疑它在更复杂的情况下是错误的。例如,如果应用于方法定义,我认为它会中断,当然它也会在 inspect.getsource() 失败的任何情况下中断。

所以 问题 是,首先,我应该怎么做才能使其在 more 情况下工作,其次,是否有更好的方法来定义装饰器相同的黑盒效果?

注 1:我只关心 Python 3。

注意 2:请假设我已阅读所有链接问题中的所有建议解决方案,但发现所有问题都不合适。

#! /usr/bin/python3

import ast
import functools
import inspect
import textwrap

def function_skeleton(name, args):
    """Return the AST of a function definition for a function named NAME,
       which takes keyword-only args ARGS, and does nothing.  Its
       .body field is guaranteed to be an empty array.
    """

    fn = ast.parse("def foo(*, {}): pass".format(",".join(args)))

    # The return value of ast.parse, as used here, is a Module object.
    # We want the function definition that should be the Module's
    # sole descendant.
    assert isinstance(fn, ast.Module)
    assert len(fn.body) == 1
    assert isinstance(fn.body[0], ast.FunctionDef)
    fn = fn.body[0]

    # Remove the 'pass' statement.
    assert len(fn.body) == 1
    assert isinstance(fn.body[0], ast.Pass)
    fn.body.clear()

    fn.name = name
    return fn

class static_vars:
    """Decorator which provides functions with static variables.
       Usage:

           @static_vars(foo=1, bar=2, ...)
           def fun():
               foo += 1
               return foo + bar

       The variables are implemented as upvalues defined by a wrapper
       function.

       Uses introspection to recompile the decorated function with its
       context changed, and therefore may not work in all cases.
    """

    def __init__(self, **variables):
        self._variables = variables

    def __call__(self, func):
        if func.__name__ in self._variables:
            raise ValueError(
                "function name {} may not be the same as a "
                "static variable name".format(func.__name__))

        fname = inspect.getsourcefile(func)
        lines, first_lineno = inspect.getsourcelines(func)

        mod = ast.parse(textwrap.dedent("".join(lines)), filename=fname)

        # The return value of ast.parse, as used here, is a Module
        # object.  Save that Module for use later and extract the
        # function definition that should be its sole descendant.
        assert isinstance(mod, ast.Module)
        assert len(mod.body) == 1
        assert isinstance(mod.body[0], ast.FunctionDef)
        inner_fn = mod.body[0]
        mod.body.clear()

        # Don't apply decorators twice.
        inner_fn.decorator_list.clear()

        # Fix up line numbers.  (Why the hell doesn't ast.parse take a
        # starting-line-number argument?)
        ast.increment_lineno(inner_fn, first_lineno - inner_fn.lineno)

        # Inject a 'nonlocal' statement declaring the static variables.
        svars = sorted(self._variables.keys())
        inner_fn.body.insert(0, ast.Nonlocal(svars))

        # Synthesize the wrapper function, which will take the static
        # variableas as arguments.
        outer_fn_name = ("_static_vars_wrapper_" +
                         inner_fn.name + "_" +
                         hex(id(self))[2:])
        outer_fn = function_skeleton(outer_fn_name, svars)
        outer_fn.body.append(inner_fn)
        outer_fn.body.append(
            ast.Return(value=ast.Name(id=inner_fn.name, ctx=ast.Load())))

        mod.body.append(outer_fn)
        ast.fix_missing_locations(mod)

        # The new function definition must be evaluated in the same context
        # as the original one.  FIXME: supply locals if appropriate.
        context = func.__globals__
        exec(compile(mod, filename="<static-vars>", mode="exec"),
             context)

        # extract the function we just defined
        outer_fn = context[outer_fn_name]
        del context[outer_fn_name]

        # and call it, supplying the static vars' initial values; this
        # returns the adjusted inner function
        adjusted_fn = outer_fn(**self._variables)
        functools.update_wrapper(adjusted_fn, func)
        return adjusted_fn

if __name__ == "__main__":
    import sys

    @static_vars(a=0, b=[])
    def test():
        b.append(a)
        a += 1
        sys.stdout.write(repr(b) + "\n")

    test()
    test()
    test()
    test()

【问题讨论】:

    标签: python python-3.x abstract-syntax-tree introspection


    【解决方案1】:

    这不就是类的用途吗?

    import sys
    
    class test_class:
        a=0
        b=[]
    
        def test(self):
            test_class.b.append(test_class.a)
            test_class.a += 1
            sys.stdout.write(repr(test_class.b) + "\n")
    
    t = test_class()
    t.test()
    t.test()
    

    [0] [0, 1]

    这是您的正则表达式编码器的一个版本:

    import re
    
    class encode:
        encode_re = re.compile(
            br'[\x00-\x20\x7F-\xFF]|'
            br'%(?!(?:[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}))')
    
        def encode_nonascii_and_percents(self, segment):
            segment = segment.encode("utf-8", "surrogateescape")
            return encode.encode_re.sub(
                lambda m: "%{:02X}".format(ord(m.group(0))).encode("ascii"),
                segment).decode("ascii")
    
    e = encode()
    print(e.encode_nonascii_and_percents('foo bar'))
    

    foo%20bar

    总是有singleton class

    Is there a simple, elegant way to define Singletons in Python?

    【讨论】:

    • 不满足硬性设计约束,在问题中说明:“更好”意味着:您不必限定静态变量的每次使用。
    • 我遵循 Python 之禅 python.org/dev/peps/pep-0020,其中指出“显式优于隐式”(不是黑盒效果)。但“更好”的解释完全取决于你。
    • 我对此进行了更多思考,并且我有更好的理由不喜欢这个:它不能在任意上下文中按原样使用。例如,您必须跳过几个额外的环节才能将此处理应用于方法。 (是的,我确实希望这种方法用于方法 - 再次,认为正则表达式仅用于一种方法。)只要表面语法是可以应用的装饰器,我会接受在后台使用单例类的答案到任何函数定义,不管上下文。
    • 我已经修改了这个问题,以便更清楚地了解我在寻找什么。
    • 我认为您应该发布您的修订作为答案!
    猜你喜欢
    • 2011-09-06
    • 2012-05-21
    • 1970-01-01
    • 2012-10-27
    • 2019-03-01
    • 2011-03-24
    • 1970-01-01
    • 2022-10-17
    • 1970-01-01
    相关资源
    最近更新 更多