是否应该添加额外的层来支持 __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__
匹配,我们将其删除,并在末尾附加格式良好的参数,以获得真正好的结果。
我正在从事一个 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__
匹配,我们将其删除,并在末尾附加格式良好的参数,以获得真正好的结果。