【问题标题】:Count how many arguments passed as positional计算有多少参数作为位置传递
【发布时间】:2021-08-10 19:41:41
【问题描述】:

如果我有一个功能

def foo(x, y):
    pass

如何从函数内部判断 y 是按位置传递还是通过其关键字传递的?

我想要类似的东西

def foo(x, y):
    if passed_positionally(y):
        print('y was passed positionally!')
    else:
        print('y was passed with its keyword')

让我得到

>>> foo(3, 4)
y was passed positionally
>>> foo(3, y=4)
y was passed with its keyword

我意识到我最初并没有指定这一点,但是否可以在保留类型注释的同时做到这一点?到目前为止,最佳答案建议使用装饰器 - 但是,它不会保留返回类型

【问题讨论】:

  • 有趣的问题,但是这样的功能的用例是什么?为什么函数会根据参数的传递方式对参数进行不同的处理?
  • 使这成为可能将允许函数的不同行为,无论您使用f(a, b) 还是使用f(x=a, y=b) 调用它,都打破了python 函数def 假定的“合同”:但是你调用它,如果参数是相同的,函数应该对 args 应用相同的操作。
  • 您的标题似乎与问题不符。标题询问位置参数的数量。这与确定y 是按位置传递还是通过关键字传递有什么关系?
  • 也许你真正想要的是def foo(*x, y=None):
  • 实际需要为库发出弃用警告

标签: python function parameter-passing


【解决方案1】:

你可以像这样创建一个装饰器:

def checkargs(func):
    def inner(*args, **kwargs):
        if 'y' in kwargs:
            print('y passed with its keyword!')
        else:
            print('y passed positionally.')
        result = func(*args, **kwargs)
        return result
    return inner

>>>  @checkargs
...: def foo(x, y):
...:     return x + y

>>> foo(2, 3)
y passed positionally.
5

>>> foo(2, y=3)
y passed with its keyword!
5

当然,您可以通过允许装饰器接受参数来改进这一点。因此,您可以传递要检查的参数。应该是这样的:

def checkargs(param_to_check):
    def inner(func):
        def wrapper(*args, **kwargs):
            if param_to_check in kwargs:
                print('y passed with its keyword!')
            else:
                print('y passed positionally.')
            result = func(*args, **kwargs)
            return result
        return wrapper
    return inner

>>>  @checkargs(param_to_check='y')
...: def foo(x, y):
...:     return x + y

>>> foo(2, y=3)
y passed with its keyword!
5

我认为添加functools.wraps 会保留注释,以下版本还允许对所有参数执行检查(使用inspect):

from functools import wraps
import inspect

def checkargs(func):
    @wraps(func)
    def inner(*args, **kwargs):
        for param in inspect.signature(func).parameters:
            if param in kwargs:
                print(param, 'passed with its keyword!')
            else:
                print(param, 'passed positionally.')
        result = func(*args, **kwargs)
        return result
    return inner

>>>  @checkargs
...: def foo(x, y, z) -> int:
...:     return x + y

>>> foo(2, 3, z=4)
x passed positionally.
y passed positionally.
z passed with its keyword!
9

>>> inspect.getfullargspec(foo)
FullArgSpec(args=[], varargs='args', varkw='kwargs', defaults=None, 
kwonlyargs=[], kwonlydefaults=None, annotations={'return': <class 'int'>})
                                             _____________HERE____________

【讨论】:

  • 谢谢!但是,这不会保留类型注释-例如如果我输入def foo(x, y) -&gt; int,然后输入reveal_type(foo(2, y=3)),我得到Any
  • 其实答案是否定的!反正也没什么好说的。你把这个函数变成了另一个函数,它需要 *args**kwargs 。顺便说一句,我是赞成票。
  • 谢谢@Cyttorak - 我试过了,但reveal_type(foo(2, 3)) 仍然显示Any,而如果我删除装饰器,它会显示builtins.int
【解决方案2】:

最后,如果你要做这样的事情:

def foo(x, y):
    if passed_positionally(y):
        raise Exception("You need to pass 'y' as a keyword argument")
    else:
        process(x, y)

你可以这样做:

def foo(x, *, y):
    pass

>>> foo(1, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() takes 1 positional argument but 2 were given

>>> foo(1, y=2) # works

或者只允许它们按位置传递:

def foo(x, y, /):
    pass

>>> foo(x=1, y=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got some positional-only arguments passed as keyword arguments: 'x, y'

>>> foo(1, 2) # works

请参阅PEP 570PEP 3102 了解更多信息。

【讨论】:

    【解决方案3】:

    改编自@Cyttorak 的回答,这是一种维护类型的方法:

    from typing import TypeVar, Callable, Any, TYPE_CHECKING
    
    T = TypeVar("T", bound=Callable[..., Any])
    
    from functools import wraps
    import inspect
    
    def checkargs() -> Callable[[T], T]:
        def decorate(func):
            @wraps(func)
            def inner(*args, **kwargs):
                for param in inspect.signature(func).parameters:
                    if param in kwargs:
                        print(param, 'passed with its keyword!')
                    else:
                        print(param, 'passed positionally.')
                result = func(*args, **kwargs)
                return result
            return inner
        return decorate
    
    @checkargs()
    def foo(x, y) -> int:
        return x+y
    
    if TYPE_CHECKING:
        reveal_type(foo(2, 3))
    foo(2, 3)
    foo(2, y=3)
    

    输出是:

    $ mypy t.py 
    t.py:27: note: Revealed type is 'builtins.int'
    
    $ python t.py 
    x passed positionally.
    y passed positionally.
    x passed positionally.
    y passed with its keyword!
    

    【讨论】:

      【解决方案4】:

      这通常是不可能的。从某种意义上说:该语言并非旨在让您区分两种方式。

      您可以设计您的函数以采用不同的参数 - 位置和命名,并检查传递了哪个参数,如下所示:

      def foo(x, y=None, /, **kwargs):
       
          if y is None: 
              y = kwargs.pop(y)
              received_as_positional = False
          else:
              received_as_positional = True
      
      

      问题在于,尽管通过使用上述仅位置参数,你可以得到y两种方式, 对于检查 函数签名。

      我有一种感觉,你只是想知道这个是为了知道 - 如果 您真的打算将其用于设计 API,我建议您重新考虑 您的 API - 行为应该没有区别,除非两者都有 从用户的角度来看,它们是明确不同的参数。

      也就是说,要走的路是检查调用者框架,并检查 函数调用位置周围的字节码:

      
      In [24]: import sys, dis
      
      In [25]: def foo(x, y=None):
          ...:     f = sys._getframe().f_back
          ...:     print(dis.dis(f.f_code))
          ...: 
      
      In [26]: foo(1, 2)
        1           0 LOAD_NAME                0 (foo)
                    2 LOAD_CONST               0 (1)
                    4 LOAD_CONST               1 (2)
                    6 CALL_FUNCTION            2
                    8 PRINT_EXPR
                   10 LOAD_CONST               2 (None)
                   12 RETURN_VALUE
      None
      
      In [27]: foo(1, y=2)
        1           0 LOAD_NAME                0 (foo)
                    2 LOAD_CONST               0 (1)
                    4 LOAD_CONST               1 (2)
                    6 LOAD_CONST               2 (('y',))
                    8 CALL_FUNCTION_KW         2
                   10 PRINT_EXPR
                   12 LOAD_CONST               3 (None)
                   14 RETURN_VALUE
      

      因此,如您所见,当 y 作为命名参数调用时,调用的操作码是 CALL_FUNCTION_KW ,并且参数的名称会立即加载到堆栈中。

      【讨论】:

      • 我的用例是放置一个弃用警告,这样我就可以在未来的版本中只使用关键字
      • 我想这里的第一种方法对他们有用。如果您更喜欢使用装饰器的方式,我有一种方法可以创建一个可以保留类型提示和签名的装饰器(我可以将其分解到开源包中) - 请告诉我。
      • 那太棒了!
      【解决方案5】:

      您可以像这样欺骗用户并向函数添加另一个参数:

      def foo(x,y1=None,y=None):
        if y1 is not None:
          print('y was passed positionally!')
        else:
          print('y was passed with its keyword')
      

      我不建议这样做,但它确实有效

      【讨论】:

      • 我认为这行不通。我可以运行foo(None, None, None)foo(None, y=None)
      • 这就是为什么我说你可以欺骗用户,如果只传递 2 个参数就可以了。只要您不将 None 传递给 y1 并且如果您想保留一个选项,您就可以使用一个永远不会使用的虚拟值
      • 你可以使用哨兵(stackoverflow.com/questions/39313943/…)而不是None来防范foo(None, None)
      【解决方案6】:

      foo中,你可以将调用栈从traceback传递给positionally,然后它会解析行,找到调用foo本身的行,然后解析带有ast的行定位位置参数规范(如果有):

      import traceback, ast, re
      def get_fun(name, ast_obj):
          if isinstance(ast_obj, ast.Call) and ast_obj.func.id == name:
              yield from [i.arg for i in getattr(ast_obj, 'keywords', [])]
          for a, b in getattr(ast_obj, '__dict__', {}).items():
              yield from (get_fun(name, b) if not isinstance(b, list) else \
                              [i for k in b for i in get_fun(name, k)])
      
      def passed_positionally(stack):
          *_, [_, co], [trace, _] = [re.split('\n\s+', i.strip()) for i in stack] 
          f_name = re.findall('(?:line \d+, in )(\w+)', trace)[0]
          return list(get_fun(f_name, ast.parse(co)))
      
      def foo(x, y):
          if 'y' in passed_positionally(traceback.format_stack()):
              print('y was passed with its keyword')
          else:
              print('y was passed positionally')
      
      foo(1, y=2)
      

      输出:

      y was passed with its keyword
      

      注意事项:

      1. 此解决方案不需要对foo 进行任何包装。只需要捕获回溯。
      2. 要在回溯中以字符串形式获取完整的 foo 调用,此解决方案必须在文件中运行,而不是在 shell 中。

      【讨论】:

      猜你喜欢
      • 2017-01-13
      • 2011-02-25
      • 1970-01-01
      • 2012-10-24
      • 2010-12-01
      • 2014-03-29
      • 1970-01-01
      • 2015-04-18
      相关资源
      最近更新 更多