Подсчитайте, сколько аргументов передано как позиционное
Если у меня есть функция
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)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, 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
Примечания:
- Это решение не требует обертывания
foo
. Нужно только записать трассировку. - Чтобы получить полный
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!