Python 装饰器处理装饰函数的默认参数
Python decorator handles default arguments of the decorated function
我想为 class 方法创建一个 'cache' 装饰器,它在内部注册 class 属性 方法的结果以避免多次计算次(而且我不想使用一个简单的 属性,在 __init__
中计算,因为我不确定是否一直计算一次)。
第一个想法是创建一个类似这样的装饰器'cache':
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj):
if not hasattr(obj, name) or getattr(obj, name) is None:
print "Computing..."
setattr(obj, name, func(obj))
else:
print "Already computed!"
return getattr(obj, name)
return wrapped
class Test:
@cache
def hello(self):
return 1000 ** 5
一切正常:
In [121]: t = Test()
In [122]: hasattr(t, '_hello')
Out[122]: False
In [123]: t.hello()
Computing...
Out[123]: 1000000000000000
In [124]: t.hello()
Already computed!
Out[124]: 1000000000000000
In [125]: hasattr(t, '_hello')
Out[125]: True
现在让我们说我想做同样的事情,但是当方法可以用参数调用时(关键字 and/or 不行)。
当然,现在我们不会将结果存储在不同的属性中(名称是什么?...),而是存储在字典中,其键由 *args 和 **kwargs 组成。让我们用元组来做:
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj, *args, **kwargs):
if not hasattr(obj, name) or getattr(obj, name) is None:
setattr(obj, name, {})
o = getattr(obj, name)
a = args + tuple(kwargs.items())
if not a in o:
print "Computing..."
o[a] = func(obj, *args, **kwargs)
else:
print "Already computed!"
return o[a]
return wrapped
class Test:
@cache
def hello(self, *args, **kwargs):
return 1000 * sum(args) * sum(kwargs.values())
In [137]: t = Test()
In [138]: hasattr(t, '_hello')
Out[138]: False
In [139]: t.hello()
Computing...
Out[139]: 0
In [140]: hasattr(t, '_hello')
Out[140]: True
In [141]: t.hello(3)
Computing...
Out[141]: 0
In [142]: t.hello(p=3)
Computing...
Out[142]: 0
In [143]: t.hello(4, y=23)
Computing...
Out[143]: 92000
In [144]: t._hello
Out[144]: {(): 0, (3,): 0, (4, ('y', 23)): 92000, (('p', 3),): 0}
由于 items
方法在不考虑字典中的顺序的情况下将字典转换为元组,如果关键字参数没有以相同的顺序调用,它可以完美地工作:
In [146]: t.hello(2, a=23,b=34)
Computing...
Out[146]: 114000
In [147]: t.hello(2, b=34, a=23)
Already computed!
Out[147]: 114000
这是我的问题:如果该方法有默认参数,那么它就不再起作用了:
class Test:
@cache
def hello(self, a=5):
return 1000 * a
现在不能用了:
In [155]: t = Test()
In [156]: t.hello()
Computing...
Out[156]: 5000
In [157]: t.hello(a=5)
Computing...
Out[157]: 5000
In [158]: t.hello(5)
Computing...
Out[158]: 5000
In [159]: t._hello
Out[159]: {(): 5000, (5,): 5000, (('a', 5),): 5000}
结果计算了 3 次,因为参数的给出方式不同(即使它们是 "same" 参数!)。
有人知道我如何在装饰器中捕获赋予函数的 "default" 值吗?
谢谢
根据参数的函数结构的复杂程度,可以有多种解决方案。我更喜欢的解决方案是将内部函数添加到 hello
中。如果您不想更改缓存的名称,请给它一个与您的外部函数相同的名称:
class Test:
def hello(self, a=5):
@cache
def hello(self, a):
return 1000 * a
return hello(self, a)
t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello
Out[111]: Computing...
Already computed!
Already computed!
{(5,): 5000}
另一种方法是在装饰器中添加对默认变量的检查,例如:
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj, *args, **kwargs):
if not hasattr(obj, name) or getattr(obj, name) is None:
setattr(obj, name, {})
o = getattr(obj, name)
a = args + tuple(kwargs.items())
if func.func_defaults: # checking if func have default variable
for k in kwargs.keys():
if k in func.func_code.co_varnames and kwargs[k] == func.func_defaults[0]:
a = ()
if args:
if args[0] == func.func_defaults[0]:
a = ()
if not a in o:
print "Computing..."
o[a] = func(obj, *args, **kwargs)
else:
print "Already computed!"
return o[a]
return wrapped
class Test:
@cache
def hello(self, a=5):
return 1000 * a
t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello
Out[112]: Computing...
Already computed!
Already computed!
{(): 5000}
如果你有,例如2 个默认变量,第一个代码(带有内部函数)仍然可以工作,而第二个代码需要在 "default variable check rules".
中进行更改
如果您使用的是足够新的 Python 版本,您可以使用 inspect.signature
获取一个 Signature
对象,该对象完全封装了有关函数参数的信息。然后你可以用你的包装器传递的参数调用它的 bind
方法,以获得一个 BoundArguments
对象。调用 BoundArguments
上的 apply_defaults
方法以填充任何缺少的具有默认值的参数,并检查 arguments
有序字典以查看函数参数及其值的明确列表对于此电话:
import inspect
def cache(func):
name = "_{:s}".format(func.__name__)
sig = inspect.signature(func)
def wrapped(obj, *args, **kwargs):
cache_dict = getattr(obj, name, None)
if cache_dict is None:
cache_dict = {}
setattr(obj, name, cache_dict)
bound_args = sig.bind(obj, *args, **kwargs)
bound_args.apply_defaults()
cache_key = tuple(bound_args.arguments.values())
if not cache_key in cache_dict:
print("Computing...")
cache_dict[cache_key] = func(obj, *args, **kwargs)
else:
print("Already computed!")
return cache_dict[cache_key]
return wrapped
请注意,我已将您的 a
和 o
变量重命名为更有意义的名称。我还改变了在对象上设置缓存字典的方式。更少 getattr
和 setattr
调用这种方式!
inspect.signature
函数和关联类型是在 Python 3.3 中添加的,但是 BoundArguments
对象上的 apply_defaults
方法是 Python 3.5 中的新方法。旧 Python 版本 on PyPi, but it doesn't include apply_defaults
yet, it seems. I'm going to report that as an issue on the backport's github tracker.
的基本功能有一个反向移植
我想为 class 方法创建一个 'cache' 装饰器,它在内部注册 class 属性 方法的结果以避免多次计算次(而且我不想使用一个简单的 属性,在 __init__
中计算,因为我不确定是否一直计算一次)。
第一个想法是创建一个类似这样的装饰器'cache':
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj):
if not hasattr(obj, name) or getattr(obj, name) is None:
print "Computing..."
setattr(obj, name, func(obj))
else:
print "Already computed!"
return getattr(obj, name)
return wrapped
class Test:
@cache
def hello(self):
return 1000 ** 5
一切正常:
In [121]: t = Test()
In [122]: hasattr(t, '_hello')
Out[122]: False
In [123]: t.hello()
Computing...
Out[123]: 1000000000000000
In [124]: t.hello()
Already computed!
Out[124]: 1000000000000000
In [125]: hasattr(t, '_hello')
Out[125]: True
现在让我们说我想做同样的事情,但是当方法可以用参数调用时(关键字 and/or 不行)。 当然,现在我们不会将结果存储在不同的属性中(名称是什么?...),而是存储在字典中,其键由 *args 和 **kwargs 组成。让我们用元组来做:
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj, *args, **kwargs):
if not hasattr(obj, name) or getattr(obj, name) is None:
setattr(obj, name, {})
o = getattr(obj, name)
a = args + tuple(kwargs.items())
if not a in o:
print "Computing..."
o[a] = func(obj, *args, **kwargs)
else:
print "Already computed!"
return o[a]
return wrapped
class Test:
@cache
def hello(self, *args, **kwargs):
return 1000 * sum(args) * sum(kwargs.values())
In [137]: t = Test()
In [138]: hasattr(t, '_hello')
Out[138]: False
In [139]: t.hello()
Computing...
Out[139]: 0
In [140]: hasattr(t, '_hello')
Out[140]: True
In [141]: t.hello(3)
Computing...
Out[141]: 0
In [142]: t.hello(p=3)
Computing...
Out[142]: 0
In [143]: t.hello(4, y=23)
Computing...
Out[143]: 92000
In [144]: t._hello
Out[144]: {(): 0, (3,): 0, (4, ('y', 23)): 92000, (('p', 3),): 0}
由于 items
方法在不考虑字典中的顺序的情况下将字典转换为元组,如果关键字参数没有以相同的顺序调用,它可以完美地工作:
In [146]: t.hello(2, a=23,b=34)
Computing...
Out[146]: 114000
In [147]: t.hello(2, b=34, a=23)
Already computed!
Out[147]: 114000
这是我的问题:如果该方法有默认参数,那么它就不再起作用了:
class Test:
@cache
def hello(self, a=5):
return 1000 * a
现在不能用了:
In [155]: t = Test()
In [156]: t.hello()
Computing...
Out[156]: 5000
In [157]: t.hello(a=5)
Computing...
Out[157]: 5000
In [158]: t.hello(5)
Computing...
Out[158]: 5000
In [159]: t._hello
Out[159]: {(): 5000, (5,): 5000, (('a', 5),): 5000}
结果计算了 3 次,因为参数的给出方式不同(即使它们是 "same" 参数!)。
有人知道我如何在装饰器中捕获赋予函数的 "default" 值吗?
谢谢
根据参数的函数结构的复杂程度,可以有多种解决方案。我更喜欢的解决方案是将内部函数添加到 hello
中。如果您不想更改缓存的名称,请给它一个与您的外部函数相同的名称:
class Test:
def hello(self, a=5):
@cache
def hello(self, a):
return 1000 * a
return hello(self, a)
t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello
Out[111]: Computing...
Already computed!
Already computed!
{(5,): 5000}
另一种方法是在装饰器中添加对默认变量的检查,例如:
def cache(func):
name = "_{:s}".format(func.__name__)
def wrapped(obj, *args, **kwargs):
if not hasattr(obj, name) or getattr(obj, name) is None:
setattr(obj, name, {})
o = getattr(obj, name)
a = args + tuple(kwargs.items())
if func.func_defaults: # checking if func have default variable
for k in kwargs.keys():
if k in func.func_code.co_varnames and kwargs[k] == func.func_defaults[0]:
a = ()
if args:
if args[0] == func.func_defaults[0]:
a = ()
if not a in o:
print "Computing..."
o[a] = func(obj, *args, **kwargs)
else:
print "Already computed!"
return o[a]
return wrapped
class Test:
@cache
def hello(self, a=5):
return 1000 * a
t = Test()
t.hello()
t.hello(a=5)
t.hello(5)
t._hello
Out[112]: Computing...
Already computed!
Already computed!
{(): 5000}
如果你有,例如2 个默认变量,第一个代码(带有内部函数)仍然可以工作,而第二个代码需要在 "default variable check rules".
中进行更改如果您使用的是足够新的 Python 版本,您可以使用 inspect.signature
获取一个 Signature
对象,该对象完全封装了有关函数参数的信息。然后你可以用你的包装器传递的参数调用它的 bind
方法,以获得一个 BoundArguments
对象。调用 BoundArguments
上的 apply_defaults
方法以填充任何缺少的具有默认值的参数,并检查 arguments
有序字典以查看函数参数及其值的明确列表对于此电话:
import inspect
def cache(func):
name = "_{:s}".format(func.__name__)
sig = inspect.signature(func)
def wrapped(obj, *args, **kwargs):
cache_dict = getattr(obj, name, None)
if cache_dict is None:
cache_dict = {}
setattr(obj, name, cache_dict)
bound_args = sig.bind(obj, *args, **kwargs)
bound_args.apply_defaults()
cache_key = tuple(bound_args.arguments.values())
if not cache_key in cache_dict:
print("Computing...")
cache_dict[cache_key] = func(obj, *args, **kwargs)
else:
print("Already computed!")
return cache_dict[cache_key]
return wrapped
请注意,我已将您的 a
和 o
变量重命名为更有意义的名称。我还改变了在对象上设置缓存字典的方式。更少 getattr
和 setattr
调用这种方式!
inspect.signature
函数和关联类型是在 Python 3.3 中添加的,但是 BoundArguments
对象上的 apply_defaults
方法是 Python 3.5 中的新方法。旧 Python 版本 on PyPi, but it doesn't include apply_defaults
yet, it seems. I'm going to report that as an issue on the backport's github tracker.