Аннотации функций: возвращаемые значения
Основы аннотации возвращаемого типа
Как указать, что функция возвращает значение определённого типа?
Начиная с Python 3.5, в язык введена возможность аннотировать возвращаемый тип функции с помощью стрелки -> после списка параметров. Основной синтаксис выглядит так:
def add(a: int, b: int) -> int:
return a + bPython возвращаемый тип функции (возвращаемый тип функции в python)
Здесь -> int сообщает, что функция add должна вернуть целое число. Это не влияет на выполнение программы, но статические анализаторы (например, mypy) могут проверять корректность типов.
Типичные ошибки и проблемы:
- Пропуск аннотации возвращаемого типа приводит к тому, что mypy считает его
Any, что снижает строгость проверки. - Несоответствие фактического возвращаемого значения и аннотации вызывает ошибку mypy.
- В Python 3.5–3.9 для сложных типов (например, список чисел) требуется импорт из модуля
typing.
Как аннотировать функцию, которая может не возвращать значение (None)?
Для функций, которые завершаются без явного return или возвращают None, обычно указывают -> None.
def print_message(msg: str) -> None:
print(msg)
Если функция иногда возвращает значение, а иногда None, используют Optional[Тип] или эквивалент Union[Тип, None]. Начиная с Python 3.10, можно писать Тип | None.
from typing import Optional, Union
def find_user(user_id: int) -> Optional[str]:
# ... возвращает имя пользователя или None
if user_id in database:
return database[user_id]
return None
# Эквивалентные записи:
# def find_user(user_id: int) -> Union[str, None]:
# def find_user(user_id: int) -> str | None:
Проблемы: Частая ошибка - использование Optional для типов, которые не могут быть None. Например, def func() -> Optional[int]: подразумевает, что может быть None, хотя функция всегда возвращает целое.
Как указать, что функция возвращает значение одного из нескольких типов (Union)?
Когда функция может возвращать значения разных типов, применяют Union.
from typing import Union
def parse_number(value: str) -> Union[int, float]:
try:
return int(value)
except ValueError:
return float(value)
С Python 3.10 используется синтаксис int | float.
Ошибки: При большом количестве альтернатив Union становится громоздким. В таких случаях лучше пересмотреть архитектуру или использовать Any, но это снижает безопасность типов.
Когда стоит использовать Any для возврата?
Если возвращаемое значение действительно может быть любого типа (например, десериализация JSON), допустимо написать -> Any. Однако это отключает проверку типов, поэтому применяется редко.
from typing import Any
def load_json(data: str) -> Any:
import json
return json.loads(data)
Злоупотребление Any лишает преимуществ статической типизации. Лучше максимально конкретизировать тип (например, dict[str, Any]).
Как аннотировать возврат функции, возвращающей другую функцию (Callable)?
Используется конструкция Callable[[аргументы], возвращаемый_тип].
from typing import Callable
def create_multiplier(factor: int) -> Callable[[float], float]:
def multiply(x: float) -> float:
return x * factor
return multiply
Здесь Callable[[float], float] означает функция, принимающая float и возвращающая float.
Проблемы: сложные сигнатуры (например, с переменным числом параметров) трудно выразить через Callable. Для этого используют Protocol или TypedDict.
Как обобщить возвращаемый тип с помощью TypeVar (дженерики)?
Когда функция возвращает значение, тип которого зависит от входных данных, применяют TypeVar.
from typing import TypeVar, List
T = TypeVar('T')
def first_element(lst: List[T]) -> T:
return lst[0]
Теперь first_element([1,2,3]) выведет int, а first_element(['a','b']) - str.
Ошибки: неправильная привязка TypeVar (например, без указания ограничений) может привести к неопределённому поведению. Также для нескольких разных типов входных данных нужно определять отдельные TypeVar.
Как задать разные возвращаемые типы для разных сценариев вызова (overload)?
Декоратор @typing.overload позволяет определить несколько сигнатур для одной функции. Полезно, когда тип возвращаемого значения зависит от типа аргумента или его присутствия.
from typing import overload, Union
@overload
def to_str(value: int) -> str: ...
@overload
def to_str(value: float) -> str: ...
@overload
def to_str(value: None) -> str: ...
def to_str(value: Union[int, float, None]) -> str:
if value is None:
return ""
return str(value)
Реализация должна покрывать все перегруженные варианты. mypy использует перегрузки для точного вывода типов.
Ошибки: забытая реализация или несоответствие между перегрузками и телом функции. Также перегрузки не проверяются во время выполнения, только статически.
Как ограничить возвращаемое значение конкретными литералами (Literal)?
Если функция возвращает только фиксированный набор строк или чисел, применяют Literal.
from typing import Literal
def get_status() -> Literal['ok', 'error', 'pending']:
# ...
return 'ok'
Теперь mypy проверит, что возвращается только одно из этих значений.
Проблемы: Literal требует точного совпадения. Нельзя вернуть переменную, содержащую такую строку, если она не объявлена как Literal.
Как определить собственный возвращаемый тип с помощью Protocol?
Protocol позволяет определить структурный тип (утиная типизация) для возвращаемого объекта. Полезно, если нужно описать интерфейс без наследования.
from typing import Protocol
class Named(Protocol):
name: str
def greet(self) -> str: ...
def create_person(name: str) -> Named:
class Person:
def __init__(self, name: str):
self.name = name
def greet(self) -> str:
return f"Hello, {self.name}"
return Person(name)
Функция гарантирует, что возвращаемый объект имеет атрибут name и метод greet.
Ошибки: непроверяемые атрибуты, если они не реализованы в возвращаемом объекте, mypy выдаст ошибку. Также Protocol не может быть инстанцирован напрямую.
Расширенные примеры использования возвращаемых типов
Ниже приведены примеры, иллюстрирующие одновременное использование нескольких механизмов типизации.
Пример 1. Перегрузка с дженериками для функции объединения списков
Функция, которая принимает список и возвращает его первый элемент, но если список пустой – возвращает None. Используем overload и TypeVar для точного вывода типа.
from typing import TypeVar, overload, Sequence, Optional
T = TypeVar('T')
@overload
def head(lst: Sequence[T]) -> T: ...
@overload
def head(lst: Sequence[T]) -> None: ...
# На самом деле корректно: если список пуст, вернется None, иначе T.
# Но mypy требует неоднозначности, поэтому используем Union
# Лучше:
# @overload
# def head(lst: Sequence[T]) -> T: ...
# @overload
# def head(lst: Sequence[T]) -> None: ...
# Реализация:
def head(lst: Sequence[T]) -> Optional[T]:
return lst[0] if lst else None
Проверка mypy для вызовов:
# вызовы:
result1 = head([1, 2, 3]) # ожидается int (из первой перегрузки)
result2 = head([]) # ожидается None (из второй перегрузки)
reveal_type(result1) # mypy: int
reveal_type(result2) # mypy: None
# Результат mypy (при запуске с --strict): # main.py:19: note: Revealed type is "builtins.int" # main.py:20: note: Revealed type is "None"
Объяснение: Благодаря overload mypy выбирает правильную сигнатуру в зависимости от аргумента. Если список содержит элементы, возвращается T; если пуст – None. В реализации используется Optional[T].
Пример 2. Использование Protocol для возврата конфигурации
Создадим протокол для объекта конфигурации с полями server и port. Функция load_config возвращает объект, соответствующий этому протоколу.
from typing import Protocol, Optional
class Config(Protocol):
server: str
port: int
def load_config(file_path: str) -> Optional[Config]:
# Имитация загрузки из файла
if file_path.endswith('.json'):
# возвращаем dict, который соответствует протоколу (утиная типизация)
return {'server': 'localhost', 'port': 8080}
else:
return None
config = load_config('settings.json')
if config:
print(config.server, config.port)
# Результат выполнения: localhost 8080
Объяснение: Protocol задаёт интерфейс. Возвращаемый словарь имеет атрибуты server и port, поэтому mypy считает его удовлетворяющим протоколу. При возврате None используется Optional.
Пример 3. Рекурсивный тип для древовидной структуры
Определим тип для узла дерева, который может содержать вложенные списки узлов. Используем самоссылающийся тип.
from typing import List, Union, TypeAlias
# В Python 3.12+ можно использовать TypeAlias
TreeNode: TypeAlias = Union[int, List['TreeNode']]
def make_tree(data: list) -> TreeNode:
# упрощённая реализация
if not data:
return 0
return [make_tree(x) for x in data]
Такой тип сложно аннотировать полностью, поэтому часто прибегают к Any или object, но при необходимости указывают Union с рекурсивной ссылкой.
Проблема: mypy может не справиться с глубокой рекурсией или циклическими зависимостями. В таких случаях используют отложенную аннотацию (строка с именем типа).
Пример 4. Использование TypeVar с ограничением
Ограничим TypeVar только числовыми типами (int, float).
from typing import TypeVar, Union
Number = TypeVar('Number', int, float)
def add_numbers(a: Number, b: Number) -> Number:
return a + b
result = add_numbers(1, 2) # OK
result2 = add_numbers(1.0, 2) # OK
# result3 = add_numbers(1, '2') # mypy ошибка: str не подходит
Объяснение: TypeVar с ограничениями гарантирует, что оба аргумента и возврат будут одного из указанных типов. Это строже, чем Union.