Я хотел бы добавить подсказки типа к простой функции. Поскольку внутри он использует только вызовы 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 просто выбирает первую совпадающую подпись, которая могла бы решить проблему. Есть идеи, как это решить?






Лично я бы предпочел, чтобы функция во всех случаях возвращала один тип (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)
В этом случае я бы заменил np.asarray на pd.DataFrame. В противном случае, в зависимости от вашей версии Python, я бы набрал подсказку npt.ArrayLike | pd. DataFrame. Если это не ответ на ваш вопрос, то я вам ничем не смогу помочь, извините.
Оказывается, там обновление уже составило 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.
Спасибо за ответ. Я не хотел включать в пример clip_db и удалил его.
np.divide()работает со списками и другими итерируемыми объектами, тогда как/ожидает пустые массивы. Я хотел бы сохранить структуру DataFrame и просто изменить значения. Конечно, я мог бы привести результат, но тогда индекс и столбцы были бы потеряны. На самом деле этот вопрос касается только подсказок типов.