Подсчитайте, сколько аргументов передано как позиционное

Если у меня есть функция

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

Я понимаю, что изначально не указывал это, но можно ли сделать это, сохранив аннотации типов? В верхнем ответе до сих пор предлагается использовать декоратор, однако он не сохраняет возвращаемый тип.

Ответов (6)

Решение

Вы можете создать декоратор, например:

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,y1=None,y=None):
  if y1 is not None:
    print('y was passed positionally!')
  else:
    print('y was passed with its keyword')

Я не рекомендую это делать, но это работает

В конце, если вы собираетесь сделать что-то вроде этого:

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 570 и PEP 3102 для получения дополнительной информации.

В 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]
    print(f_name, co)
    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вызов в виде строки в трассировке, это решение необходимо запустить в файле, а не в оболочке.

Обычно это невозможно. В некотором смысле: язык не предназначен для того, чтобы вы могли различать оба пути.

Вы можете разработать свою функцию так, чтобы она принимала разные параметры - позиционные и именованные, и проверяла, какой из них был передан, например:

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

Проблема в том, что, хотя, используя только позиционные параметры в качестве abov, вы можете получить y оба пути, он будет показан не один раз для пользователя (или IDE), проверяющего подпись функции.

У меня такое чувство, что вы просто хотите знать это ради знания - если вы действительно намереваетесь это сделать для разработки 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, а имя параметра загружается в стек сразу перед ним.

Адаптировано из ответа @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!