是否应该添加额外的层来支持 __repr__?

Should extra layers be added just to support __repr__?

我正在从事一个 Python 项目,该项目主要 returns 闭包而不是传统的基于 class 的方法。例如:

def term(token):
    def fn(text):
        return (...)
    return fn

如您所想,调试和测试闭包是一场噩梦,尤其是当我在整个代码中对闭包使用相同的名称时。例如:

>>> term('t')
<function fn at 0x...>

所以我尝试将其包装在 class 中以从 namedtuple 获得特殊的 __repr__ 处理:

def rr(cls, attrs):
    T = namedtuple(cls, attrs)

    class G(object):
        __slots__ = ()

        def __init__(self, repr_message, callable):
            self.callable = callable
            self.repr_message = repr_message

        def __call__(self, *args, **kwargs):
            return self.callable(*args, **kwargs)

        def __repr__(self):
            return self.repr_message

    K = type(cls, (G,), {})

    def wrapper(fn):
        def init(*args, **kwargs):
            t = T(*args, **kwargs)
            return K(
                repr(t),
                fn(*args, **kwargs),
            )
        return init
    return wrapper

这样:

>>> rr('Term', ['token'])(term)('$')
Term(token='$')

如您所想,这可能会影响性能。我的问题是这种包装是否更可取,是否比返回 "ugly" 闭包更 Pythonic?

一种更简单的方法是简单地修改闭包的 func_name 属性。结果不如您的代码生成的结果漂亮,但它对 RAM 和性能的影响很小。

def term(token):
    def fn(text):
        return text.split(token)
    fn.func_name = "term(token={0!r})".format(token)
    return fn

s = 'splitthistestup'
f = term('t')
g = term('i')
print(f, f(s))
print(g, g(s))

典型输出

<function term(token='t') at 0xb74878b4> ['spli', '', 'his', 'es', 'up']
<function term(token='i') at 0xb74878ec> ['spl', 'tth', 'stestup']

对于 Python 3 你需要做一些稍微不同的事情。

fn.func_name = ...

变成:

fn.__qualname__ = ...

根据你的问题我了解到你还在使用Python 2.

在 Python 3(我们使用的是 3.3+ inspect.getclosurevars)这里可以做一些真正的魔术来做一个几乎没有开销的装饰器,它将(对于这种情况)产生期望的输出;这在 Python 2 中更难做到,因为 Python 2 函数不知道它们的完全限定名称:

import inspect

class reprcorate(object):
    __slots__ = ('__call__',)

    def __init__(self, callable):
        self.__call__ = callable

    def __repr__(self):
        func = self.__call__
        funcname = func.__qualname__
        funcname = funcname.replace('.<locals>', '')

        try:
            closure_vars = inspect.getclosurevars(func)
            args = '(%s)' % ', '.join('%s=%r' % i for i in closure_vars.nonlocals.items())
            last_part = '.' + func.__name__

            if funcname.endswith(last_part):
                funcname = funcname[:-len(last_part)]

        except:
            args = '(...)'

        return funcname + args

def term(token):
    @reprcorate
    def fn(text):
        @reprcorate
        def fn2(fn2arg):
            print(token, text, fn2arg)

        return fn2

    return fn

print(term('foo'))
print(term('foo')('bar'))

打印出来

term(token='foo')
term.fn(token='foo', text='bar')

它的工作原理是 __call__ 可以分配给 实例 ;所以我们不需要虚拟 __call__ 蹦床;我们还使用 __slots__ 来减少运行时开销。

真正的魔力在于__repr__方法,其中仔细剖析了装饰的function/closure。可以在 inspect.getclosurevars 的帮助下检查函数从外部作用域使用的自由变量,其中 returns 一个命名元组;我们对 nonlocals 特别感兴趣,因为这些是外部范围使用的值;这是一个简单的变量名到值的字典。我们把它变成一个漂亮的 variable=value, variable=value 字符串。这些不是严格的函数参数,而是在内部闭包中看到和使用的值。

我们也清理了一下函数名; in Python 3 函数名在__name__,全限定名在__qualname__;闭包的 __qualname__ 看起来像 foo.<locals>.bar.<locals>.baz,所以我们删除所有 .<locals>,得到 foo.bar.baz;然后,如果最后一个虚线部分与包装函数的 __funcname__ 匹配,我们将其删除,并在末尾附加格式良好的参数,以获得真正好的结果。