Как объединить подсказку типа с использованием переменной связанного типа и статических типов для максимальной гибкости?

Я хотел бы добавить подсказки типа к простой функции. Поскольку внутри он использует только вызовы numpy, он очень гибок в своих входных данных. По сути, он принимает все объекты типа массива, для которых существует тип numpy.typing.ArrayLike.

Однако определить тип возвращаемого значения не так просто. Для некоторых типов ввода, таких как списки, функции numpy возвращают результат в массив numpy, а это значит, что я мог бы использовать -> np.NDArray.

Некоторые другие типы ввода, такие как pandas.DataFrames, которые я часто использую в своем коде. Обычно это хорошая причина для использования TypeVar, привязанного к типу ввода.

Как сохранить гибкость numpy, одновременно предоставляя осмысленные подсказки по типу, например, для mypy?

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

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

Они вызывают различные ошибки при проверке статических типов, демонстрируя ограничения каждого подхода.

Версия 1:

def get_decibels1(p2: npt.ArrayLike) -> npt.NDArray:
    return (10 * np.log10(np.divide(p2, 4e-10)))

df = pd.DataFrame([[4, 5, 6], [7, 8, 9]])
get_decibels1(df).columns
# --- Causes mypy Error:
# error: "ndarray[Any, dtype[Any]]" has no attribute "columns"  [attr-defined]

Версия 2:

T = TypeVar('T', bound=npt.ArrayLike)
def get_decibels2(p2: T) -> T:
    return (10 * np.log10(np.divide(p2, 4e-10)))

ls = [4.0, 5, 6]
get_decibels2(ls).shape
# --- Causes mypy error:
#  error: "list[float]" has no attribute "shape"  [attr-defined]

Как мне разумно объединить два подхода?


Обновлять:

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

T = TypeVar('T', bound=Union[pd.DataFrame, pd.Series])
@overload
def get_decibels(p2: T) -> T: ...

@overload
def get_decibels(p2: npt.ArrayLike) -> npt.NDArray: ...

def get_decibels(p2: npt.ArrayLike):
    return (10 * np.log10(np.divide(p2, 4e-10)))
# --- Causes mypy error:
#  error: Overloaded function signatures 1 and 2 overlap with incompatible return types  [overload-overlap]

У меня сложилось впечатление, что mypy просто выбирает первую совпадающую подпись, которая могла бы решить проблему. Есть идеи, как это решить?

Почему в 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
0
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Лично я бы предпочел, чтобы функция во всех случаях возвращала один тип (NDArray), применяя numpy.array или numpy.asarray. Более того, я считаю, что np.divide(a, b) и a/b эквивалентны, и, наконец, зачем перезаписывать pandas.DataFrame, если можно просто расширить таблицу? (если у вас проблемы с памятью, я понимаю, и вы можете проверить библиотеку Polars). А тебе не нужен clip_db?

Хотя это вопрос личного вкуса, вот мое предложение с функцией, которая всегда возвращает массив numpy:

import numpy as np
from numpy import typing as npt
import pandas as pd

def get_decibels(p2: npt.ArrayLike) -> npt.NDArray:
    p2 = np.asarray(p2)
    return 10 * np.log10(p2/4e-10)

def get_decibels_clip(p2: npt.ArrayLike, clip_db: float=0) -> npt.NDArray:
    p2 = np.asarray(p2)
    return np.maximum(10 * np.log10(p2/4e-10), clip_db)  # is this what was asked? 

df = pd.DataFrame([[4, 5, 6], [7, 8, 9]])
df[["db1", "db2", "db3"]] = get_decibels(df)
print(df) 

Спасибо за ответ. Я не хотел включать в пример clip_db и удалил его. np.divide() работает со списками и другими итерируемыми объектами, тогда как / ожидает пустые массивы. Я хотел бы сохранить структуру DataFrame и просто изменить значения. Конечно, я мог бы привести результат, но тогда индекс и столбцы были бы потеряны. На самом деле этот вопрос касается только подсказок типов.

ti-sch 19.07.2024 16:43

В этом случае я бы заменил np.asarray на pd.DataFrame. В противном случае, в зависимости от вашей версии Python, я бы набрал подсказку npt.ArrayLike | pd. DataFrame. Если это не ответ на ваш вопрос, то я вам ничем не смогу помочь, извините.

Axel Gib 19.07.2024 17:17
Ответ принят как подходящий

Оказывается, там обновление уже составило 95%: Mypy и другие средства проверки типов читают перегруженные функции в том порядке, в котором они определены, используя первую подходящую.

По умолчанию он будет указывать на совпадения, поскольку они считаются небезопасными (см. документ mypy). Однако:

Обратите внимание, что в случаях, когда вы игнорируете ошибку перекрывающейся перегрузки, mypy обычно по-прежнему определяет типы, которые вы ожидаете в местах вызовов.

Таким образом, использование type: ignore[overload-overlap] приводит к ожидаемому поведению.

T = TypeVar('T', bound=Union[pd.DataFrame, pd.Series])
@overload
def get_decibels(p2: T) -> T: ...  # type: ignore[overload-overlap]

@overload
def get_decibels(p2: npt.ArrayLike) -> npt.NDArray: ...

def get_decibels(p2):
    return (10 * np.log10(np.divide(p2, 4e-10)))

ls = [[4, 5, 6], [7, 8, 9]]
df = pd.DataFrame(ls)

reveal_type(get_decibels(ls))
>>>note: Revealed type is "numpy.array[Any, numpy.dtype[Any]]"

reveal_type(get_decibels(df))
>>>note: Revealed type is "pandas.core.frame.DataFrame"

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

df: np.ArrayLike = pd.DataFrame([[0,1,2],[3,4,5]])
get_decibels(df)  # mypy will wrongly deduce the return type to be `np.NDArray`

Поскольку я считаю, что это своего рода крайний случай, добавление подсказок типов к этому методу кажется хорошим компромиссом. Тем более, что pd.DataFrame в большинстве случаев в любом случае ведет себя как np.NDArray.

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

Переместить часть строки в фрейме данных в новую строку
Изменить цвет отображения на основе столбца
Как найти максимальную и минимальную метку времени, когда значение становится ниже минимального порога в pyspark?
Подсчет групп одинаковых значений в одном столбце
Как объединить несколько фреймов данных и суммировать общие значения в столбец
Как сопоставить прогнозы за несколько временных интервалов с фактическими значениями?
Суммирование перестановок в фрейме данных Pandas растет суперэкспоненциально
Найдите значение в столбце, который содержит список, возьмите другое значение из следующего столбца и поместите его в первую таблицу в новый столбец
Python pandas read_sas с параметром размера фрагмента завершается с ошибкой из-за несоответствия индекса
Как я могу заполнить значение на основе другого категориального столбца

Похожие вопросы