Binobinos - Python
11 subscribers
1 photo
6 links
Python, Программирование, алгоритмы, типизация
Download Telegram
Channel created
Здравствуйте👋 !
Я Михаил Binobinos и в этом канале я буду публиковать полезные статьи в области программирования, алгоритмов, IT-сферы.

Вот мои соц-сети:
GitHub - https://github.com/Binobinos
telegram - @binobinos
telegram channel - https://t.me/binobinosdev
Почта для обращений - binobinos.dev@gmail.com

Если есть вопросы - пишите в личные сообщения - всегда готов ответить!

Binobinos - Python
1👍1
🔥 Аннотации типов в Python. Что это и почему важно указывать. PEP 484
Давай разберём аннотации на примере этой функции:
def add(a: int, b: int) -> int:
msg: str = f"Сумма чисел {a} и {b} = {a + b}"
print(msg)
return a + b

В данном примере мы видим функцию которая принимает два целых числа, и возвращает число.
В самой функции мы создаем переменную msg и указываем что это строка.

Для аннотации типа значений аргумента используется двоеточие и после идёт указываемый тип. Для указания возвращаемого значения используется `->`и после возвращаемый тип.
❗️Заметим что Python не проверяет типы при вызове функции. Аннотации просто подсказывают тип.
Если мы вызовем add("Hello ", "World") то получим вывод "Сумма чисел Hello и World = Hello World".
Но зачем они нужны если они не влияют на код?
1. Это помогает другим разработчикам понимать ваш код.
Пример ситуации:
Вы создали функцию get_user которая принимает id пользователя и возвращает его объект:
users = {123: "User"}
def get_user(user_id):
return users.get(user_id)

Если другой разработчик попытается передать ключ "123", функция вернет None, так как в словаря наш пользователь записан ключом 123 типа int. Если мы укажем в аннотации все данные:
users: dict[int, str] = {123: "User"}
def get_user(user_id: int) -> str | None:
return users.get(user_id)

2. Само-документирует код.
Аннотации позволят вам не указывать в документации их тип.

3. IDE и статические анализаторы используют аннотации для проверки типов и подсказок.
Давайте рассмотрим как использовать статический анализатор mypy
1. Устанавливаем mypy: pip install mypy.
2. Запускаем проверку mypy main.py или другой ваш файл или директория.
3. Для строгой проверки используйте флаг —strict mypy —strict main.py
Вам выведется подробное сообщение анализа.

Какие есть типы аннотаций?
1. Базовые типы.
int, str, float, bool и т.п - это всё простые типы. Они явно указывают что объект является их типом.

2. Коллекции.
Давайте рассмотрим основные:
- list - Для указания типа используются квадратные скобки. Список чисел: list[str]
- tuple - Для указания точного кортежа например кортежа из трёх строк используйте такую аннотацию: tuple[str, str, str], для переменного количества аргументов используйте Ellipsis(Троеточие): tuple[str, ...]
- dict - Указывается два значения, первый это тип ключа, второй это тип значения.
Пример словарь строка-число dict[str, int]
- set, frozenset - Аналогично list и tuple. Тип значение указывается в квадратных скобках set[str]

3. Другие аннотации.
Для других аннотаций используется модуль typing из стандартной библиотеки
- Union. Если вы хотите указать что тип может быть как одним так и другим типом используйте синтаксис int | str(Число или строка). Для старых версий Python используйте Union[int, str].
- Literal. Если вы хотите указать что переменная/аргумент/функция принимает только конкретные типы из определённого списка, используйте Literal['+', '-', "*", "/"].
- Optional - Если вы хотите указать что тип может быть как одним так и другим типом используйте синтаксис int | None (Число или None).
❗️ В Python 3.9 и ниже используйте Optional[int]
- Any. Если вы хотите указать что переменная/аргумент/функция принимает произвольное значение, то используйте Any.
❗️ Any - спорный тип. При его использовании теряется смысл типизации. Используйте в местах где вы точно уверенны что тип произвольный.
🔥 Если вы используете аннотации из typing, не забудьте импортировать явно from typing import Optional, Union, Literal, Any или использовать точечную нотацию typing.Any


✍️ Это был первый обзор аннотаций в Python. В этом посте я покрыл не все темы аннотаций. В последующих постах я разберу как создавать свои Generic классы. Что такое Protocol и чем он отличается от абстрактных классов. Более продвинутые аннотации и как их использовать.

❗️ Если вы ещё не используете аннотации, то используйте их в своем коде!

PEP 484 - PEP про аннотации
typing - Документация typing
Binobinos - Python #typehints
1🔥31
🔥 Аннотации типов в Python часть 2. TypedDict и другие продвинутые аннотации. PEP 484

В предыдущем посте мы разобрали что такое аннотации типов и как их указывать. Узнали основные аннотации и про статический анализатор mypy. В этом посте мы разберём более сложные аннотации.

TypedDict

Допустим у нас есть функция get_account которая возвращает данные пользователя:
users: dict[str, dict[str, str]] = {
"123":
{
"name": "Bino",
"docs": "This is docs profile",
"color": "orange"
}
}


def get_account(user_id: int) -> dict[str, str] | None:
if user_id in users:
return users.get(user_id)
raise KeyError("User not found")

Следуя всем стандартам мы типизировали глобальный словарь, входное аргументы и возвращаемое значение. Все верно. Но аннотация может не совсем отражать сути.

У нас есть аннотация dict[str, str] что означает что ключ и значение это строки. Она верна для нашего словаря, но давайте изменим глобальный словарь:
users: dict[str, dict[str, str]] = {
"123":
{
"name": "Bino",
"docs": "This is docs profile",
"color": "orange"
},
"124":
{
"first name": "Bino",
"last name": "Bino",
"docs": "This is docs profile",
"color": "orange"
}
}

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

Для решения этой проблемы создадим новый класс UserData унаследованный от TypedDict из модуля typing
from typing import TypedDict


class UserData(TypedDict):
name: str
docs: str
color: str

В данном классе мы создали поля которые отражают данные о пользователе. Изменим существующий код:
from typing import TypedDict


class UserData(TypedDict):
name: str
docs: str
color: str


users: dict[str, UserData] = {
"123":
{
"name": "Bino",
"docs": "This is docs profile",
"color": "orange"
},
"124":
{
"name": "Bino",
"docs": "This is docs profile",
"color": "orange"
}
}


def get_account(user_id: int) -> UserData | None:
if user_id in users:
return users.get(user_id)
raise KeyError("User not found")

В данном коде мы изменили аннотацию dict[str, str] на более точную аннотацию UserData.

Рассмотрим пример с добавленным ключом text:
from typing import TypedDict


class UserData(TypedDict):
name: str
docs: str
color: str
text: str

Запустим mypy:
PS D:\Framework> mypy .\main.py
main_2.py:20: error: Missing key "text" for TypedDict "UserData"
[typeddict-item]
Found 1 error in 1 file (checked 1 source file)

Мы видим что мы пропустили новое поле text. Это может являться проблемой если данный словарь получается динамически, например от get запроса к API, где поля text может не быть. Для этого есть аннотация NotRequired. Применим её:
from typing import TypedDict, NotRequired


class UserData(TypedDict):
name: str
docs: str
color: str
text: NotRequired[str]

Запустим mypy:
PS D:\Framework> mypy .\main.py
Success: no issues found in 1 source file

Как мы видим мы сделали поле text не обязательным. Но есть ситуации когда не обязательных полей может быть больше чем обязательных, для этой ситуации добавим настройку total=False:
from typing import TypedDict, Required


class UserData(TypedDict, total=False):
name: Required[str]
docs: Required[str]
color: Required[str]
text: str

Аргумент total делает все поля не обязательными(По умолчанию все поля обязательные). Required Указывает что поле обязательное, NotRequired что поле не обязательное.
2
Также стоит отметить возможность создания аннотации через функциональный стиль:
UserData = TypedDict("UserData", {'name': str, 'docs': str, 'color': str, "text": NotRequired[str]}, total=False)

Резюмируя этот раздел. TypedDict - Это продвинутая аннотация для типизации словарей, структуру которых мы знаем. Примерами будут динамические API ответы или структура базы данных. Но данная аннотация вам не подойдёт если вы работает с словарями с разными полями

Callable

Давайте попробуем сделать декоратор print_arg который выводит аргументы
from typing import Any


def print_args(func):
def wrap(*args: Any, **kwargs: Any) -> None:
print(f"Позициональные аргументы: {args}, именованные: {kwargs}")
func(*args, **kwargs)

return wrap


@print_args
def add(a: int, b: float) -> None:
print(a + b)

Мы аннотировали весь код, кроме входного и возвращаемого значения. Давайте добавим аннотации и к ним:
from collections.abc import Callable
from typing import Any


def print_args(func: Callable[..., Any]) -> Callable[..., Any]:
def wrap(*args: Any, **kwargs: Any) -> None:
print(f"Позициональные аргументы: {args}, именованные: {kwargs}")
func(*args, **kwargs)

return wrap


В данном примере мы указываем что мы принимаем функцию с произвольной сигнатурой которая возвращает любое значение. Это означает что мы принимает любую функцию.
Мы импортируем Callable из модуля collections.abc, так как в версии Python 3.9 Callable из typing и много других Generic аннотаций устарел (В пример list[int] вместо List из typing, PEP 585) и.

Приведу пример более типизированной аннотации
from collections.abc import Callable

add: Callable[[int, int], int] = lambda a, b: a + b

В данной аннотации мы указываем что функция принимает два числа, и возвращает число.
Первым аргументом этой аннотации является список типов которые представляют сигнатуру функции(Ellipsis для произвольной сигнатуры), вторым является возвращаемое значение.

TypeAlias или type
Давайте рассмотрим на примере
from collections.abc import Callable
from typing import Any


def print_args(func: Callable[..., Any]) -> Callable[..., Any]:
def wrap(*args: Any, **kwargs: Any) -> None:
print(f"Позициональные аргументы: {args}, именованные: {kwargs}")
func(*args, **kwargs)

return wrap


@print_args
def add(a: int, b: float) -> None:
print(a + b)

Мы видим что Callable[..., Any] дублируется несколько раз. Чтобы убрать дублирование мы можем вынести аннотацию в переменную.
from collections.abc import Callable
from typing import Any, TypeAlias

any_callable: TypeAlias = Callable[..., Any]


def print_args(func: any_callable) -> any_callable:
def wrap(*args: Any, **kwargs: Any) -> None:
print(f"Позициональные аргументы: {args}, именованные: {kwargs}")
func(*args, **kwargs)

return wrap


@print_args
def add(a: int, b: float) -> None:
print(a + b)

Здесь мы указываем переменной any_callable аннотацию TypeAlias. Это позволяет выносить частые или сложные аннотации и переиспользовать их в коде.

❗️В Python 3.12+ Можно указывать вот так:
type any_callable = Callable[..., Any]

Это делает аннотации более явными, не требует импорта TypeAlias из typing

🔥 Подведём итоги Этой части статьи про Type Hints:
TypedDict - Позволит вам типизировать словари с настройкой полей
Callable - Поможет вам в типизировании декораторов и фабрик декораторов, а также других callable объекты
TypeAlias или type - Поможет вам переиспользовать сложные или часто используемые аннотации

PEP 484 - PEP про аннотации
PEP 585 - PEP Аннотации в стандартных коллекциях
PEP 655 - PEP про (Not)Required в TypedDict
PEP 695 - PEP про синтаксис type в аннотациях
typing - Документация typing
Binobinos - Python #typehints
13🔥1
🔥 Типизированные декораторы в Python

В прошлых постах мы разобрали базовые аннотации и продвинутые типы вроде TypedDict. Казалось бы, мы можем затипизировать всё что угодно. Но когда дело доходит до декораторов — многие разработчики опускают руки. Видел продвинутых разработчиков которые не знают как типизировать их.

Проблема в том, что обычные аннотации не умеют сохранять информацию о типах оригинальной функции. Давайте разберём эту проблему на реальном примере и посмотрим, как её решить с помощью новых возможностей Python 3.12.

Давайте представим, что мы хотим создать декоратор для логирования вызовов функций:
def logger(func):
def wrapper(*args, **kwargs):
print(f"Вызываем функцию {func.__name__}")
result = func(*args, **kwargs)
print(f"Функция {func.__name__} завершилась")
return result
return wrapper

@logger
def calculate_sum(a: int, b: int) -> int:
return a + b

Если мы попробуем использовать эту функцию, то столкнёмся с проблемой:
result = calculate_sum(5, 3)  # OK Работает
result = calculate_sum("5", "3") # Ошибка типов, но mypy не видит

Наш декоратор "съел" все аннотации оригинальной функции. Вместо оригинальной сигнатуры, новая функция принимает любые args и kwargs. Mypy теперь думает, что calculate_sum может принимать любые аргументы, и мы теряем всю пользу от типизации.

1. Первый шаг: TypeVar - аннотирование возвращаемого значения

Для простых случаев, когда мы хотим сохранить только возвращаемый тип, можно использовать TypeVar:
from typing import TypeVar, Callable, Any

T = TypeVar("T")

def logger(func: Callable[..., T]) -> Callable[..., T]:
def wrapper(*args: Any, **kwargs: Any) -> T:
print(f"Вызываем функцию {func.__name__}")
result = func(*args, **kwargs)
print(f"Функция {func.__name__} завершилась")
return result
return wrapper

@logger_simple
def calculate_sum(a: int, b: int) -> int:
return a + b

Теперь mypy понимает, что функция возвращает int, но аргументы всё ещё не проверяются. Это лучше, чем ничего, но всё равно не идеально.

2. Второй шаг: ParamSpec - Сигнатура функции

В Python 3.12 появился мощный инструмент — ParamSpec. Он позволяет "запомнить" сигнатуру оригинальной функции и передать её через декоратор.
from typing import ParamSpec, TypeVar, Callable

P = ParamSpec("P")
R = TypeVar("R")

def logger(func: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Вызываем функцию {func.__name__}")
result = func(*args, **kwargs)
print(f"Функция {func.__name__} завершилась")
return result
return wrapper

@logger_advanced
def calculate_sum(a: int, b: int) -> int:
return a + b

Теперь магия происходит здесь: P.args и P.kwargs сохраняют информацию о типах аргументов оригинальной функции. Mypy теперь полностью понимает, что:
result = calculate_sum(5, 3)      # Всё правильно
result = calculate_sum("5", "3") # mypy: ошибка типов

3. Edge-case: Concatenate - Декоратор добавляет параметры

Многие это знают, но что если наш декоратор должен добавлять дополнительные параметры? Например, мы хотим передавать уровень логирования:
from typing import Concatenate

def logger_with_level(func: Callable[Concatenate[str, P], R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("[INFO] Начало выполнения")
result = func("[DEBUG] ", *args, **kwargs) # Добавляем префикс
print("[INFO] Конец выполнения")
return result
return wrapper

@logger_with_level
def write_message(prefix: str, message: str) -> None:
print(f"{prefix}{message}")

write_message("Привет мир!") # Mypy понимает, что prefix уже добавлен декоратором

Здесь Concatenate говорит: "оригинальная функция принимает строку первым аргументом, а потом все остальные аргументы из P, но наш декоратор уже добавит эту строку сам".

Что изменилось в Python 3.12?

В новой версии Python синтаксис стал ещё чище. Теперь можно писать так:
type CallableFunc[**P, R] = Callable[P, R]
def logger_clean(func: CallableFunc[P, R]) -> CallableFunc[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Вызываем {func.__name__}")
return func(*args, **kwargs)
return wrapper

Почему это важно в реальной разработке?

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

· IDE не будет показывать подсказки для задекорированных функций
· mypy не сможет найти ошибки в вызовах

С правильной типизацией декораторов ваш код становится самодокументируемым и безопасным.

Резюме для практического использования:

· Для простых случаев используйте TypeVar — он сохранит возвращаемый тип
· Для полного сохранения сигнатуры используйте ParamSpec — это золотой стандарт для декораторов
· Если декоратор меняет аргументы используйте Concatenate
· В Python 3.12 всё стало ещё удобнее с новым синтаксисом type

Теперь ваши декораторы могут быть полностью типобезопасными, а другие разработчики скажут вам спасибо за понятные подсказки в IDE!

---

P.S. Частая проблема бывает в том, что многие разработчики пишут типизированный код, но плохо типизированные декораторы сводят всю полезность на нет.

Также TypeVar имеет ещё много применений кроме аннотирования возвращаемого значения, в следующих постах расскажу и про его применение подробно.

Ссылки:

- PEP 484 - PEP про аннотации
- PEP 612 — ParamSpec
- PEP 695 - PEP про синтаксис type в аннотациях
- typing - Документация typing
Binobinos - Python #typehints #decorators
🎃4👎21
🔥 Асимптотические обозначения и их значения O Ω Θ o ω

При разработке определенного алгоритма для определённой задачи нужно уметь анализировать его сложность и эффективность работы.

Для конкретной реализации мы можем подсчитать время работы при определенных данных. Но это время зависит от характеристик выбранного устройства на котором тестируется алгоритм, также это зависит от входных данных - например "отсортировать" уже отсортированный массив может быть намного легче чем случайный массив. В таких случаях нам нужен способ абстрагироваться от устройства и данных и ввести новую систему оценки

Для оценки сложности применяется асимптотическая оценка. Она показывает рост функции (то есть нашего алгоритма) при размере данных стремящихся к бесконечности.

Предположим мы оцениваем сложность сортировки пузырьком:
Для массива размером n, то есть состоящим из n элементов он будет делать вложено сначала n операций, потом n - 1, n - 2 и тд. пока не достигнет 0. Мы можем это записать так Σ k=0 до n n - k что является арифметической прогрессией и равно n ^ 2 / 2 = 0.5 * n ^ 2. В асимптотических обозначениях константные множители не учитываются, так как при огромных значениях рост самой функции будет намного более значительный, так что константу можно не учитывать.

Сложность данного алгоритма это O(n^2). Вот самые частные сложности:

Эффективные:
O(1) - Константная сложность, не зависит от входных данных. Например сложение
O(log n) - логарифмическая сложность. (log - логарифм по основания 2). Например бинарный поиск
O(√n) - корневая сложность (корень из n). Например проверка на простоту
O(n) - линейная сложность. растет пропорционально размеру данных. Например поиск наибольшего подмассива
O(n log n) - Квазилинейная сложность. Растет быстрее линейного но все еще оптимально. Например сортировка последовательности

Средние:

O(n ^ 2) - Квадратичная сложность. Например сортировка пузырьком
O(n ^ 3) - Кубическая сложность. Например наивный способ перемножения матриц

Не эффективные:

O(2 ^ n) - Экспоненциальная сложность. Одна из худших. Например вычисления чисел фибоначчи
O(n!) - Факториальная сложность (Где n! - факториал от n). перебор всех маршутов в задаче коммивояжера

Также различаются и сами функции оценки:
f(n) - наша исходная функция

O(g(n)) - О большое - Показывает что f(n) показывает что растет не быстрее c*g(n) при
n >= n_0, где c, n_0 - некие положительные константы
Формально O(g(n)) = {f(n): c > 0, n_0 > 0: 0 <= f(n) <= c*g(n), Ɐ n >= n_0}

Даёт верхнюю асимптотическую оценку функции

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


Ω(g(n)) - Омега большая - Показывает что f(n) показывает что растет не медленее чем c*g(n) при
n >= n_0, где c, n_0 - некие положительные константы
Формально Ω(g(n)) = {f(n): c > 0, n_0 > 0: 0 <= c*g(n) <= f(n), Ɐ n >= n_0}

Даёт нижнюю асимптотическую оценку функции

Θ(g(n)) - Тета большая - Показывает что f(n) показывает что растет не медленее чем c_1*g(n) и не быстрее чем c_2*g(n) при
n >= n_0, где c_1, c_2, n_0 - некие положительные константы
Формально Θ(g(n)) = {f(n): c_1 > 0, c_2 > 0, n_0 > 0: 0 <= c_1*g(n) <= f(n) <= c_2*g(n), Ɐ n >= n_0}

Даёт асимптотически точную оценку

o(g(n)) - о малое - Показывает что f(n) показывает что растет медленее c*g(n) при
n >= n_0, где c, n_0 - некие положительные константы
Формально o(g(n)) = {f(n): c > 0, n_0 > 0: 0 <= f(n) < c*g(n), Ɐ n >= n_0}

Даёт нижнюю оценку верхней оценки. Например для бинарного поиска "формально" оценка O(n^100) подходит, но точная оценка o(n)

ω(g(n)) - Омега большая - Показывает что f(n) показывает что растет не медленее чем c*g(n) при
n >= n_0, где c, n_0 - некие положительные константы

Формально ω(g(n)) = {f(n): ∀c > 0, ∃n_0 > 0: 0 <= c*g(n) < f(n), Ɐ n >= n_0}

Даёт верхнюю оценку нижней оценки.

Сравнение с знаками равенства:
O(g(n)) - <=
Ω(g(n)) - >=
Θ(g(n)) - ==
o(g(n)) - <
ω(g(n)) - >
👍2👾1
Для алгоритмов с одинаковой асимптотической сложностью можно сравнивать константу. Например при одинаковой сложности алгоритм может работать медленно из за c = 10, а другой алгоритм имеющий меньшую константу, будет работать быстрее

Пространственная сложность алгоритма схоже с скоростью выполнения, она показывает сколько дополнительной оперативной памяти(RAM) использует алгоритм в зависимости от входных данных.

Для её оценки применяются такие же асимптотические обозначения и правила что и для временной оценки

Binobinos - Python #Алгоритмы #Python
1💊1