在修饰函数中强制使用仅关键字的参数



我有一个类,它有几个方法,需要存在某个参数,但原因不同。

通常,参数将作为属性附加到实例,在这种情况下,不需要传递参数。但是,如果属性丢失(或None),则可以选择将此参数作为仅关键字的参数传递:

import functools
class Foo:
def __init__(self, this_kwarg_default=None):
self.default = this_kwarg_default

@staticmethod
def require_this_kwarg(reason):
def enforced(func):
@functools.wraps(func)
def wrapped(self, *args, this_kwarg=None, **kwargs):
if this_kwarg is None:
this_kwarg = self.default
if this_kwarg is None:
raise TypeError(f'You need to pass this kwarg, {reason}!')
return func(self, *args, this_kwarg=this_kwarg, **kwargs)

return wrapped
return enforced
require_this_kwarg = require_this_kwarg.__func__
@require_this_kwarg('because I said so')
def foo(self, this_kwarg=None):
print(f'This kwarg is {str(this_kwarg)}')

大多数情况下,这会产生所需的行为。

>>> myfoo = Foo(42)
>>> myfoo.foo()
This kwarg is 42
>>> myfoo.foo(this_kwarg=4)
This kwarg is 4
>>> yourfoo = Foo()
>>> yourfoo.foo()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "dec.py", line 15, in wrapped
raise TypeError(f'You need to pass this kwarg, {reason}!')
TypeError: You need to pass this kwarg, because I said so!

但如果传递了任何位置参数,我会得到一些意想不到的行为:

>>> myfoo.foo(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "dec.py", line 16, in wrapped
return func(self, *args, this_kwarg=this_kwarg, **kwargs)
TypeError: foo() got multiple values for argument 'this_kwarg'

那么,将Foo.foo定义为仅将this_kwarg作为关键字参数是有意义的:

@require_this_kwarg('because I said so')
def foo(self, *, this_kwarg=None):
print(f'This kwarg is {str(this_kwarg)}')

然而。。。

>>> myfoo.foo(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "dec.py", line 16, in wrapped
return func(self, *args, this_kwarg=this_kwarg, **kwargs)
TypeError: foo() takes 1 positional argument but 2 positional arguments (and 1 keyword-only argument) were given

在这种情况下,所需的行为将是引发TypeError: foo() takes 0 positional arguments but 1 was given,就像在不使用decorator的情况下所期望的那样。

我希望functools.wraps能够强制执行修饰函数的调用签名。显然,wraps并不是这样做的。有什么办法可以做到这一点吗?

哇,这比我预期的要棘手得多。我很想看看是否有人能想出一个更简单、更清洁的解决方案,但我认为这正是你所需要的?

from inspect import getfullargspec
import functools

class Foo:
def __init__(self, x_default):
self.default = x_default
@staticmethod
def require_x(reason):
def enforced(func):
@functools.wraps(func)
def wrapped(self, *args, **kwargs):
argspec = getfullargspec(func)
while True:
if 'x' in kwargs:
# it's explicitly there, so it will have a value
if kwargs['x'] is None:
kwargs['x'] = self.default
break
elif argspec.varargs is None:
# there are no varargs to eat up positional arguments
if 'x' in argspec.args[:len(args)+1]:
# x will get a value from args, offset by one for self
if args[argspec.args.index('x') - 1] is None:
args = tuple(a if n != argspec.args.index('x') - 1 else self.default
for n, a in enumerate(args))
break
elif argspec.defaults is not None and 'x' in argspec.args[-len(argspec.defaults):]:
# x will get a value from a default
if argspec.defaults[argspec.args[-len(argspec.defaults):].index('x')] is None:
kwargs['x'] = self.default
break
elif 'x' in argspec.kwonlydefaults:
if argspec.kwonlydefaults['x'] is None:
kwargs['x'] = self.default
break
raise TypeError(f'{func.__name__} needs a value for x, {reason}.')
func(self, *args, **kwargs)
return wrapped
return enforced
require_x = require_x.__func__

我不喜欢需要inspect才能工作的生产代码,所以我仍然怀疑你是否真的需要这样做的代码——在这里更广泛的设计中可能有点反模式。但我想,任何事情都可以做。

这并不能真正回答问题;但这是一个可行的解决方案,说明了所需的行为,而且无论如何都太长了,无法发表评论。

也许这是个坏主意。我不确定。

我想要的基本上是一个可选的关键字参数:

def foo(self, this_kwarg=None):
if this_kwarg is None:
this_kwarg = self.default
...

但是如果self.default is None呢?一般来说,这是允许的。然而,某些方法(如Foo.foo)要求this_kwarg不是None,即如果是,它们就会失败。因此,基本上,我正在寻找一种很好的"Python"方式的描述性/信息性错误处理。

一种解决方法是像这样实现Foo.foo

def foo(self, this_kwarg=None):
if this_kwarg is None:
this_kwarg = self.default
if this_kwarg is None:
raise TypeError('This kwarg cannot be `None`, because I said so!')
...

但是,我必须将这些代码添加到每个有此要求的方法中。(假设我有其他几种方法Foo.barFoo.baz等,如果参数是None,它们都无法工作。)

我认为一个装饰师应该是一个优雅的&在没有大量重复代码的情况下实现这一点的Python方式。问题是Foo.fooFoo.barFoo.baz等都具有不同的呼叫签名。除了this_kwarg,这些方法中的一些方法可能有1个或多个(可能是任意数量)位置和/或关键字参数。

也许,最好的解决方案是:

class Foo:
def __init__(self, default=None):
self.default = default

def require_this_kwarg(self, this_kwarg, reason):
if this_kwarg is None:
this_kwarg = self.default
if this_kwarg is None:
raise TypeError(f'This kwarg cannot be `None`, because {reason}!')
return this_kwarg
def foo(self, *args, this_kwarg=None, **kwargs):
this_kwarg = self.require_this_kwarg(this_kwarg, 'I said so')
...

这实际上是我最初的解决方案。然后我试着用一个装饰师&遇到了我描述的问题。我认为函数调用签名是否可以在装饰器中强制执行是一个有趣的问题。

在阅读了您的问题和评论后,我对您的问题的理解是您正在按以下顺序搜索值。

  1. 如果存在this_kwarg,请使用它
  2. 如果#1失败,请使用self.default
  3. 如果#2失败,则引发错误

此代码应在位置或关键字参数中工作。

import functools
class Foo:
def __init__(self, this_kwarg_default=None):
self.default = this_kwarg_default

@staticmethod
def require_this_kwarg(reason):
def enforced(func):
@functools.wraps(func)
def wrapped(self, this_kwarg=None):
def _process(k=self.default):
if k is None:
raise TypeError(f'You need to pass this kwarg, {reason}!')
return func(self, k)
if this_kwarg is None:
return _process()
return _process(this_kwarg)
return wrapped
return enforced
require_this_kwarg = require_this_kwarg.__func__
@require_this_kwarg('because I said so')
def foo(self, this_kwarg=None):
print(f'This kwarg is {str(this_kwarg)}')

核心逻辑在下面的代码中

...
12                 def _process(k=self.default):
13                     if k is None:
14                         raise TypeError(f'You need to pass this kwarg, {reason}!')
15                     return func(self, k)
16                 if this_kwarg is None:
17                     return _process()
18                 return _process(this_kwarg)
...

代码逻辑匹配上面提到的搜索顺序:

  1. 如果this_kwarg不是None,则返回func(this_kwarg)。(第18行)
  2. 如果this_kwarg为None,请尝试返回func(self.default)。(第17行)
  3. 如果this_kwargself.default均为None,则引发错误。(第14行)

测试

import pytest
@pytest.mark.parametrize("default_val", [None, 1, "this_kwarg_default=1"])
@pytest.mark.parametrize("call_val", [None, 3, "this_kwarg=3"])
def test_foo(default_val, call_val):
print("parametr are: ", default_val, call_val)
f = eval(f"Foo({default_val})")
eval(f"f.foo({call_val})")

输出:

============================================================================== test session starts ===============================================================================
platform darwin -- Python 3.8.5, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /Users/kz2249/tmp/st/tests
collected 9 items                                                                                                                                                                
test_example.py parametr are:  None None
Fparametr are:  1 None
This kwarg is 1
.parametr are:  this_kwarg_default=1 None
This kwarg is 1
.parametr are:  None 3
This kwarg is 3
.parametr are:  1 3
This kwarg is 3
.parametr are:  this_kwarg_default=1 3
This kwarg is 3
.parametr are:  None this_kwarg=3
This kwarg is 3
.parametr are:  1 this_kwarg=3
This kwarg is 3
.parametr are:  this_kwarg_default=1 this_kwarg=3
This kwarg is 3
.
==================================================================================== FAILURES ====================================================================================
______________________________________________________________________________ test_foo[None-None] _______________________________________________________________________________
default_val = None, call_val = None
@pytest.mark.parametrize("default_val", [None, 1, "this_kwarg_default=1"])
@pytest.mark.parametrize("call_val", [None, 3, "this_kwarg=3"])
def test_foo(default_val, call_val):
print("parametr are: ", default_val, call_val)
f = eval(f"Foo({default_val})")
>       eval(f"f.foo({call_val})")
test_example.py:36: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
<string>:1: in <module>
???
test_example.py:19: in wrapped
return wrap()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
k = None
def wrap(k=self.default):
if k is None:
>           raise TypeError(f'You need to pass this kwarg, {reason}!')
E           TypeError: You need to pass this kwarg, because I said so!
test_example.py:16: TypeError
============================================================================ short test summary info =============================================================================
FAILED test_example.py::test_foo[None-None] - TypeError: You need to pass this kwarg, because I said so!
========================================================================== 1 failed, 8 passed in 0.10s ===========================================================================

相关内容

  • 没有找到相关文章

最新更新