简体   繁体   English

计算有多少 arguments 作为位置传递

[英]Count how many arguments passed as positional

If I have a function如果我有一个 function

def foo(x, y):
    pass

how can I tell, from inside the function, whether y was passed positionally or with its keyword?从 function 内部,我如何判断y是按位置传递还是通过其关键字传递?

I'd like to have something like我想要类似的东西

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

so that I get所以我得到

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

I realise I didn't originally specify this, but is it possible to do this whilst preserving type annotations?我意识到我最初并没有指定这一点,但是否可以在保留类型注释的同时做到这一点? The top answer so far suggests using a decorator - however, that does not preserve the return type到目前为止,最佳答案建议使用装饰器 - 但是,它不会保留返回类型

You can create a decorator, like this:您可以创建一个装饰器,如下所示:

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

Of course you can improve this by allowing the decorator to accept arguments.当然,您可以通过允许装饰器接受 arguments 来改进这一点。 Thus you can pass the parameter you want to check for.因此,您可以传递要检查的参数。 Which would be something like this:这将是这样的:

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

I think adding functools.wraps would preserve the annotations, following version also allows to perform the check over all arguments (using inspect ):我认为添加functools.wraps将保留注释,以下版本还允许对所有 arguments 执行检查(使用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____________

Update Python 3.10更新Python 3.10

In Python 3.10+ new ParamSpec type annotation was introduced ( PEP 612 ), for better specifying parameter types in higher-order functions.在 Python 3.10+ 中引入了新的ParamSpec类型注释 ( PEP 612 ),以便更好地指定高阶函数中的参数类型。 As of now, the correct way to annotate this decorator would be like this:到目前为止,注释这个装饰器的正确方法是这样的:

import inspect

from functools import wraps
from typing import Callable, ParamSpec, TypeVar, TYPE_CHECKING

T = TypeVar("T")
P = ParamSpec("P")


def check_args(func: Callable[P, T]) -> Callable[P, T]:
    """
    Decorator to monitor whether an argument is passed
    positionally or with its keyword, during function call.
    """

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

    return inner

Which correctly preserves type annotation:正确保留类型注释:

if TYPE_CHECKING:
    reveal_type(foo(2, 3))

# ─❯ mypy check_kwd.py
# check_kwd.py:34: note: Revealed type is "builtins.int"
# Success: no issues found in 1 source file

At the end, if you are going to do something like this:最后,如果您要执行以下操作:

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

You can do this:你可以这样做:

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

Or only allow them to be passed positionally:或者只允许它们按位置传递:

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

See PEP 570 and PEP 3102 for more.有关更多信息,请参阅PEP 570PEP 3102

Adapted from @Cyttorak 's answer, here's a way to do it which maintains the types:改编自@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)

Output is: Output 是:

$ 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!

It is not ordinarily possible.这通常是不可能的。 In a sense: the language is not designed to allow you to distinguish both ways.从某种意义上说:该语言并非旨在让您区分两种方式。

You can design your function to take different parameters - positional, and named, and check which one was passed, in a thing like:您可以设计您的 function 以采用不同的参数 - 位置和命名,并检查通过了哪个参数,如下所示:

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

The problem is that, although by using positional only parameters as abov, you could get y both ways, it would be shown not once for a user (or IDE) inspecting the function signature.问题是,尽管通过使用上述仅位置参数,您可以通过两种方式获得y ,但对于检查 function 签名的用户(或 IDE)来说,它不会显示一次。

I hav a feeling you just want to know this for the sake of knowing - if you really intend this for design of an API, I'd suggest you'd rethink your API - there should be no difference in the behavior, unless both are un-ambiguously different parameters from the user point of view.我有一种感觉,您只是想知道这一点 - 如果您真的打算将此用于设计 API,我建议您重新考虑您的 API - 行为应该没有区别,除非两者都是从用户的角度来看,明确不同的参数。

That said, the way to go would be to inspect the caller frame, and check the bytecode around the place the function is called:也就是说,通往 go 的方法是检查调用者帧,并检查调用 function 的位置周围的字节码:


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

So, as you can see, when y is called as named parameter, the opcode for the call is CALL_FUNCTION_KW , and the name of the parameter is loaded into the stack imediately before it.因此,如您所见,当y被称为命名参数时,调用的操作码是CALL_FUNCTION_KW ,并且参数的名称在它之前立即加载到堆栈中。

You can trick the user and add another argument to the function like this:您可以欺骗用户并向 function 添加另一个参数,如下所示:

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')

I don't recommend doing it but it does work我不建议这样做,但它确实有效

In foo , you can pass the call stack from traceback to positionally , which will then parse the lines, find the line where foo itself is called, and then parse the line with ast to locate positional parameter specifications (if any):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)

Output: Output:

y was passed with its keyword

Notes:笔记:

  1. This solution does not require any wrapping of foo .此解决方案不需要任何包装foo Only the traceback needs to be captured.只需要捕获回溯。
  2. To get the full foo call as a string in the traceback, this solution must be run in a file, not the shell.要在回溯中以字符串形式获取完整的foo调用,此解决方案必须在文件中运行,而不是在 shell 中运行。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM