Статически переопределять аргумент функции и сохранять правильные подсказки типов и строки документации

У меня есть функция original с указанными аргументами, их типами и строкой документации.

Как я могу определить функцию new, которую она будет вызывать original, но переопределить a аргумент некоторым A_DEFAULT значением. Например. new(x) == original(A_DEFAULT, x).

Самое главное — это результат new

  • должна быть оригинальная подпись, но без аргумента a
  • сохранить исходную строку документа
  • и подпись, и строка документа должны быть видны из IDE.

Цель состоит в том, чтобы создать такие частичные функции без явного копирования исходных строк документации и подписей.

Есть идеи, как это можно сделать? (даже если у вас есть какая-то хакерская идея)

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 = ...

Смотрите functools.wraps.

Charles Duffy 06.05.2024 18:42

Вы спрашиваете, как скопировать подсказки типов original в new, за исключением первого аргумента?

Barmar 06.05.2024 18:44

Какую IDE вы используете?

InSync 06.05.2024 18:54

@InSync VS Code (Pylance), но вопрос не зависит от IDE, поскольку более поздний код может использоваться в разных IDE.

FamousSnake 06.05.2024 19:12

@Бармар, да. А также скопируйте строку документа

FamousSnake 06.05.2024 19:13

@CharlesDuffy Я думаю, что functools.wraps в этом случае не поможет, поскольку он предназначен для полного копирования подписи.

FamousSnake 06.05.2024 19:15

Зачем вам копировать строку документации? Ваша новая функция принимает на 1 аргумент меньше, поэтому строка документации будет неправильной.

Barmar 06.05.2024 19:15

Возможно, этот вопрос поможет: stackoverflow.com/questions/60387444/… но удалить аннотацию для a может быть сложно. И я не уверен, что IDE поймет это статически.

Barmar 06.05.2024 19:19

@Barmar «Почему вы хотите скопировать строку документации?», - да, это немного спорная часть, но в моем случае аргументы либо не описаны в строке документации, либо их описание не так важно, как остальная часть документа- строка, которую я пытаюсь сохранить.

FamousSnake 06.05.2024 20:56

В любом случае, строка документации хранится в свойстве __doc__ функции, поэтому вы можете просто сделать new.__doc__ = original.__doc__

Barmar 06.05.2024 21:27

@Barmar, это тоже была моя идея, но она не работает статически (хотя будет работать динамически, для Jupyter, интерпретаторов и т. д.). Пробовал в pylance иpyright, см. astebin.com/qRv7Rq5E

FamousSnake 06.05.2024 22:38

Да, есть куча решений для дублирования сигнатуры другой функции, но как только вы начнете ее модифицировать, я думаю, вы превысите возможности статической проверки типов.

Barmar 06.05.2024 23:12
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
12
54
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Статическая типизация

Приведенное ниже решение основано на этом примере здесь. Для статической типизации, включая строку документации, 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)

Спасибо! Интересно, что использование одного и того же ParamSpecP как во входных, так и в возвращаемых вызываемых объектах заставляет IDE также сохранять строку документа, хотя new.__doc__ на самом деле является None в первом примере. Хотя, если подробнее, есть идеи, можно ли пропустить в возвращаемой функции не первый аргумент из первых n аргументов, а какой-то n-й аргумент из последних n аргументов? Я думаю, если бы это было возможно, это нельзя было бы сделать с Concatenate, поскольку он поддерживает только ParamSpec в качестве последнего аргумента.

FamousSnake 07.05.2024 16:58

Я думаю, что с точки зрения статической типизации эти идеи все еще находятся в зачаточном состоянии - как вы говорите, нотация спецификации Param в настоящее время весьма ограничена в своих возможностях. Сигнатуры во время выполнения более гибкие — я создал эту библиотеку несколько лет назад, чтобы обеспечить возможность динамической модификации разными способами: github.com/dreamingspires/Mutate-Function

Mark 07.05.2024 17:03

Другие вопросы по теме