У меня есть функция original
с указанными аргументами, их типами и строкой документации.
Как я могу определить функцию new
, которую она будет вызывать original
, но переопределить a
аргумент некоторым A_DEFAULT
значением. Например. new(x) == original(A_DEFAULT, x)
.
Самое главное — это результат new
a
Цель состоит в том, чтобы создать такие частичные функции без явного копирования исходных строк документации и подписей.
Есть идеи, как это можно сделать? (даже если у вас есть какая-то хакерская идея)
import functools
A_DEFAULT = ...
def original(a: int, b: str):
"description"
return a + b
# `new` should have in IDE tooltips and type checks:
# - __doc__ = "description"
# - signature = (b: int)
# new(x) = original(A_DEFAULT, x)
new = ...
Вы спрашиваете, как скопировать подсказки типов original
в new
, за исключением первого аргумента?
Какую IDE вы используете?
@InSync VS Code (Pylance), но вопрос не зависит от IDE, поскольку более поздний код может использоваться в разных IDE.
@Бармар, да. А также скопируйте строку документа
@CharlesDuffy Я думаю, что functools.wraps
в этом случае не поможет, поскольку он предназначен для полного копирования подписи.
Зачем вам копировать строку документации? Ваша новая функция принимает на 1 аргумент меньше, поэтому строка документации будет неправильной.
Возможно, этот вопрос поможет: stackoverflow.com/questions/60387444/… но удалить аннотацию для a
может быть сложно. И я не уверен, что IDE поймет это статически.
@Barmar «Почему вы хотите скопировать строку документации?», - да, это немного спорная часть, но в моем случае аргументы либо не описаны в строке документации, либо их описание не так важно, как остальная часть документа- строка, которую я пытаюсь сохранить.
В любом случае, строка документации хранится в свойстве __doc__
функции, поэтому вы можете просто сделать new.__doc__ = original.__doc__
@Barmar, это тоже была моя идея, но она не работает статически (хотя будет работать динамически, для Jupyter, интерпретаторов и т. д.). Пробовал в pylance иpyright, см. astebin.com/qRv7Rq5E
Да, есть куча решений для дублирования сигнатуры другой функции, но как только вы начнете ее модифицировать, я думаю, вы превысите возможности статической проверки типов.
Приведенное ниже решение основано на этом примере здесь. Для статической типизации, включая строку документации, Concatenate
и ParamSpec
оказываются полезными:
from typing import Callable, Concatenate, ParamSpec, TypeVar
def original(a: int, b: str):
"description"
return str(a) + b
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
def set_first_arg_default(
f: Callable[Concatenate[T, P], R],
first_arg_default: T
) -> Callable[P, R]:
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
return f(first_arg_default, *args, **kwargs)
return inner
A_DEFAULT = 1
new = set_first_arg_default(original, A_DEFAULT)
print(new("x"))
Если вам также требуется обновление строки документации и подписи во время выполнения, для изменения inspect
можно использовать библиотеку __signature__
. Я выделил этот пример, поскольку он усложняет решение:
from typing import Callable, Concatenate, ParamSpec, TypeVar
import inspect
def original(a: int, b: str):
"description"
return str(a) + b
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
def set_first_arg_default(
f: Callable[Concatenate[T, P], R],
first_arg_default: T
) -> Callable[P, R]:
def get_new_signature(
f: Callable[Concatenate[T, P], R]
) -> inspect.Signature:
old_sig = inspect.signature(f)
params = list(dict(old_sig.parameters).values())
return old_sig.replace(parameters=params[1:])
def inner(*args: P.args, **kwargs: P.kwargs) -> R:
return f(first_arg_default, *args, **kwargs)
# This section is optional as far as IDEs are concerned
inner.__doc__ = f.__doc__
inner.__signature__ = get_new_signature(f) # type: ignore reportFunctionMemberAccess
return inner
A_DEFAULT = 1
new = set_first_arg_default(original, A_DEFAULT)
print(new("x"))
help(new)
Спасибо! Интересно, что использование одного и того же ParamSpec
P
как во входных, так и в возвращаемых вызываемых объектах заставляет IDE также сохранять строку документа, хотя new.__doc__
на самом деле является None
в первом примере. Хотя, если подробнее, есть идеи, можно ли пропустить в возвращаемой функции не первый аргумент из первых n аргументов, а какой-то n-й аргумент из последних n аргументов? Я думаю, если бы это было возможно, это нельзя было бы сделать с Concatenate
, поскольку он поддерживает только ParamSpec
в качестве последнего аргумента.
Я думаю, что с точки зрения статической типизации эти идеи все еще находятся в зачаточном состоянии - как вы говорите, нотация спецификации Param в настоящее время весьма ограничена в своих возможностях. Сигнатуры во время выполнения более гибкие — я создал эту библиотеку несколько лет назад, чтобы обеспечить возможность динамической модификации разными способами: github.com/dreamingspires/Mutate-Function
Смотрите
functools.wraps
.