Этот код демонстрирует:
🔘 короткое замыкание (401 и 429 останавливают конвейер);
🔘 прохождение запроса дальше при None;
🔘 терминальный обработчик 404.
Политики прохождения запроса
🔘 Short-circuit (рекомендуется по умолчанию): первый, кто вернул результат, «закрывает» цепочку;
🔘 Fall-through: все обработчики обязаны отработать (например, аудит/метрики/логирование), результат — агрегация;
🔘 Микс: часть «жёстких» (аутентификация, лимиты) — short-circuit; «мягкие» (логирование) — всегда проходят.
#основы
@zen_of_python
Политики прохождения запроса
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Что такое линтер и что он делает
Линтер - это инструмент статического анализа кода, который проверяет ваш код на темы:
🔘 Синтаксис - правильно ли расставлены скобки, двоеточия;
🔘 Импорты - все ли импорты используются;
🔘 Переменные - все ли переменные определены;
🔘 Стиль - соответствие PEP 8;
🔘 Типы - если используется типизация.
Зачем запускать после изменений
⚡️ Обнаружение новых ошибок
⚡️ Проверка совместимости
⚡️ Соблюдение стандартов
⚡️ Проблемы безопасности
⚡️ Неэффективный код
Настройка линтера
Обычно линтер настраивается через файлы конфигурации:
Мы можем тонко настраивать, каким именно правилам следовать:
Классические линтеры
🔘 Pylint
🔘 ruff
🔘 Flake8
🔘 mypy
🔘 black
Пользователи Cursor, возможно, обратили внимание на завтозапуск линтера после практически каждого внесенного изменения. Эта IDE не отдает предпочтения какому-то конкретному инструменту, но запускает несколько из них.
Список правил ruff с детализацией в доках
#основы
@zen_of_python
Линтер - это инструмент статического анализа кода, который проверяет ваш код на темы:
Зачем запускать после изменений
# После добавления нового кода
def new_feature():
unused_var = "not used" # Линтер найдет неиспользуемую переменную
return "feature"
# После изменения импортов
from new_module import new_function
# Линтер проверит, существует ли new_function
# После рефакторинга
class MyClass:
def __init__(self):
self.very_long_attribute_name_that_should_be_shorter = None
# Линтер предложит сократить имя
import subprocess
user_input = input("Enter command: ")
subprocess.run(user_input, shell=True) # Линтер предупредит о SQL injection
# Неэффективно
my_list = []
for i in range(1000):
my_list.append(i)
# Линтер может предложить list comprehension
my_list = [i for i in range(1000)]
Настройка линтера
Обычно линтер настраивается через файлы конфигурации:
# pyproject.toml
[tool.ruff]
line-length = 88 # Максимальная длина строки
target-version = "py38" # Версия Python
Мы можем тонко настраивать, каким именно правилам следовать:
[tool.ruff.lint]
select = ["E", "F", "W", "C90", "I", "N", "UP", "YTT", "S", "BLE", "FBT", "B", "A", "COM", "C4", "DTZ", "T10", "EM", "EXE", "FA", "ISC", "ICN", "G", "INP", "PIE", "T20", "PYI", "PT", "Q", "RSE", "RET", "SLF", "SLOT", "SIM", "TID", "TCH", "INT", "ARG", "PTH", "TD", "FIX", "ERA", "PD", "PGH", "PL", "TRY", "FLY", "NPY", "AIR", "PERF", "FURB", "LOG", "RUF"]
ignore = ["E501", "S101", "PLR0913", "PLR0912", "PLR0915"]
Классические линтеры
Пользователи Cursor, возможно, обратили внимание на завтозапуск линтера после практически каждого внесенного изменения. Эта IDE не отдает предпочтения какому-то конкретному инструменту, но запускает несколько из них.
Список правил ruff с детализацией в доках
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
✍2❤1👍1
Что значит стрелка ->
Если вы столкнулись со знаком -> в коде на Python и задумались,а не код из R ли это нужно ли это, как это работает и откуда оно взялось, этот лонгрид ответит на такие вопросы.
Стрелка в определении функции служит для аннотации типа возвращаемого значения. Она ставится после списка параметров и перед двоеточием:
Здесь
Краткая история оператора
Идея аннотаций функций впервые появилась в PEP 3107 (Function Annotations), который ввёл возможность добавлять произвольные метаданные к параметрам и к возвращаемому значению. На базе этой синтаксики PEP 484 затем ввёл систему типовых подсказок (type hints) для Python, сделав их стандартной практикой для тех, кто хочет статической проверки типов. Все эти дополнения остались опциональными — сам интерпретатор не будет выбрасывать ошибки, если аннотации не совпадают с фактическим типом во время выполнения.
Простой пример:
Что происходит, если функция возвращает другой тип?
Python сам по себе не будет проверять, совпадает ли реальный возвращаемый тип с аннотацией — код просто выполнится. Но когда вы используете статический анализатор типов (например, mypy) или линтер, несоответствие будет обнаружено и выведет предупреждение / ошибку. Это ключевая идея: аннотации дают выгоду при статической проверке, CI и чтении кода, но не изменяют семантику программы во время выполнения.
Какие типы можно указывать после
Почти любые:
🔘 простые встроенные (int, str, bool),
🔘 коллекции (list[str], dict[str, int]),
🔘 собственные классы и проч.
Где ещё применяются стрелки и аннотации?
Хотя наиболее часто
🔘 аннотаций параметров функций;
🔘 переменных и атрибутов классов (PEP 526);
🔘 аннотаций методов,
🔘 документации и автодополнения в IDE.
Инструменты типа mypy, pyright, IDE (PyCharm, VS Code) и линтеры используют аннотации, чтобы находить ошибки и улучшать подсказки.
Ограничения и подводные камни
🔘 Аннотации не выполняются: они не гарантия корректности в рантайме;
🔘 Сложные типы для сложной структуры данных или динамических конструкций типы могут стать громоздкими; иногда приходится балансировать читабельность и строгость;
#основы
@zen_of_python
Если вы столкнулись со знаком -> в коде на Python и задумались,
Стрелка в определении функции служит для аннотации типа возвращаемого значения. Она ставится после списка параметров и перед двоеточием:
def count_titles(titles: list[str]) -> int:
return len(titles)
Здесь
-> int
говорит: «ожидается, что функция вернёт int». Аннотации опциональны и сами по себе ничего в рантайме не проверяют — это метаданные.Краткая история оператора
Идея аннотаций функций впервые появилась в PEP 3107 (Function Annotations), который ввёл возможность добавлять произвольные метаданные к параметрам и к возвращаемому значению. На базе этой синтаксики PEP 484 затем ввёл систему типовых подсказок (type hints) для Python, сделав их стандартной практикой для тех, кто хочет статической проверки типов. Все эти дополнения остались опциональными — сам интерпретатор не будет выбрасывать ошибки, если аннотации не совпадают с фактическим типом во время выполнения.
Простой пример:
import random
def get_game_recommendation(titles: list[str]) -> str:
return random.choice(titles)
games = ["Minecraft", "Cyberpunk 2077", "The Witcher 3", "Elden Ring"]
recommendation = get_game_recommendation(games)
print(f"Рекомендуемая игра: {recommendation}") # Например, Minecraft
print(f"Тип возвращаемого значения: {type(recommendation).__name__}") # str
Что происходит, если функция возвращает другой тип?
Python сам по себе не будет проверять, совпадает ли реальный возвращаемый тип с аннотацией — код просто выполнится. Но когда вы используете статический анализатор типов (например, mypy) или линтер, несоответствие будет обнаружено и выведет предупреждение / ошибку. Это ключевая идея: аннотации дают выгоду при статической проверке, CI и чтении кода, но не изменяют семантику программы во время выполнения.
Какие типы можно указывать после
->
?Почти любые:
Где ещё применяются стрелки и аннотации?
Хотя наиболее часто
->
встречается в объявлениях функций, аннотации — это часть более широкой системы типизации в Python. Типы используются для:Callable
, Protocol
и т.д.;Инструменты типа mypy, pyright, IDE (PyCharm, VS Code) и линтеры используют аннотации, чтобы находить ошибки и улучшать подсказки.
Ограничения и подводные камни
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍3
Что такое JSON и зачем он нужен
JSON (JavaScript Object Notation) — текстовый формат для обмена данными, удобный для людей и машин. Он возник в экосистеме JavaScript, но стал язык-независимым стандартом для API, конфигураций и документных хранилищ. Формат простой: объекты (пар «ключ: значение») и массивы — это основные строительные блоки.
Почему Python-разработчику это важно:
🔘 JSON часто используется в web-API, логах, конфигурациях и при обмене данными между сервисами.
🔘 Python имеет встроенный модуль json, который делает сериализует (Python → JSON) и десериализует (JSON → Python).
Синтаксис JSON
🔘 Строки — в двойных кавычках (
🔘 Логические значения — в нижнем регистре:
🔘 Отсутствующее значение —
🔘 Нельзя оставлять комментарии и нельзя ставить завершающие запятые после последнего элемента.
Валидный JSON:
Модуль json
Главные функции одноименного модуля стандартной библиотеки:
🔘
🔘
🔘
🔘
Эти функции следуют стандартному отображению типов: словари → объекты, списки → массивы, строки → строки, числа → числа,
Полезные параметры dumps / dump
При сериализации полезны параметры:
🔘
🔘
🔘
🔘
🔘
Маппинг типов
При загрузке JSON в Python выполняется обратное отображение типов:
*
*
*
*
*
*
JSON-ключи — всегда строки. Если у вас в исходном Python-словаре были числовые ключи (например
Полезные утилиты и приёмы
🔘
🔘 Онлайн-валидаторы (например, JSONLint) и редакторы с подсветкой помогут быстро найти синтаксические ошибки (комментарии, лишние запятые и т. п.).
Подборка примеров
Читаем JSON из файла:
Записываем с красивым форматированием:
Сериализуем datetime:
Проверяем валидность JSON в CLI
#основы
@zen_of_python
JSON (JavaScript Object Notation) — текстовый формат для обмена данными, удобный для людей и машин. Он возник в экосистеме JavaScript, но стал язык-независимым стандартом для API, конфигураций и документных хранилищ. Формат простой: объекты (пар «ключ: значение») и массивы — это основные строительные блоки.
Почему Python-разработчику это важно:
Синтаксис JSON
"..."
);true / false
(в Python — True / False
);null
(в Python → None
);Валидный JSON:
{
"name": "Frieda",
"isDog": true,
"hobbies": ["eating", "sleeping"],
"age": 8,
"address": {"home": ["Berlin", "Germany"], "work": null}
}
Модуль json
Главные функции одноименного модуля стандартной библиотеки:
json.dumps(obj, **opts)
— сериализует Python-объект в строку JSON;json.dump(obj, fp, **opts)
— сериализует и записывает в файл-объект;json.loads(s)
— парсит JSON-строку в Python-объект;json.load(fp)
— считывает JSON из файла и парсит.Эти функции следуют стандартному отображению типов: словари → объекты, списки → массивы, строки → строки, числа → числа,
True
/ False
→ true
/ false
, None
→ null
.
import json
data = {"name": "Frieda", "age": 8, "is_dog": True}
s = json.dumps(data) # --> '{"name": "Frieda", "age": 8, "is_dog": true}'
obj = json.loads(s) # --> {'name': 'Frieda', 'age': 8, 'is_dog': True}
Полезные параметры dumps / dump
При сериализации полезны параметры:
indent
— делает вывод читабельным (pretty print) (например, indent=2);sort_keys=True
— сортирует ключи объекта по алфавиту;separators
— позволяет контролировать символы между элементами (полезно для минификации);ensure_ascii=False
— по умолчанию json экранирует не-ASCII символы; если хотите сохранить UTF-8 в читаемом виде, ставьте False;skipkeys=True
— пропускает неподдерживаемые типы ключей вместо TypeError (использовать осторожно — вы можете потерять данные).Маппинг типов
При загрузке JSON в Python выполняется обратное отображение типов:
*
object
→ dict
*
array
→ list
*
string
→ str
*
number
→ int
/ float
*
true`/`false
→ True`/`False
*
null
→ None
JSON-ключи — всегда строки. Если у вас в исходном Python-словаре были числовые ключи (например
{1: "a"}`), при сериализации они станут строками (`"1"`). После `json.loads()
вы получите ключи как строки — Python не "угадает" исходный тип. Это частая ловушка при конвертации словарей ключами-числами.Полезные утилиты и приёмы
python -m json.tool
— встроенная утилита для форматирования/проверки JSON в терминале (можно использовать для prettify/minify);Подборка примеров
Читаем JSON из файла:
import json
with open("data.json", "r", encoding="utf-8") as f:
data = json.load(f)
Записываем с красивым форматированием:
with open("data_pretty.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, sort_keys=True, ensure_ascii=False)
Сериализуем datetime:
from datetime import datetime
import json
def default(o):
if isinstance(o, datetime):
return o.isoformat()
raise TypeError
json.dumps({"now": datetime.utcnow()}, default=default)
Проверяем валидность JSON в CLI
python -m json.tool input.json > /dev/null
# код возврата 0 — валидный JSON
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10
Фабрики в Python
В программировании довольно часто встречается задача: в зависимости от входных данных нужно выбрать подходящий класс для обработки. Например, у нас есть XML-файлы и JSON-файлы, и мы хотим написать систему, которая будет их импортировать.
На первый взгляд решение простое — внутри метода проверять расширение файла и создавать соответствующий объект. Но у такого подхода есть несколько серьёзных недостатков. Именно здесь на помощь приходит паттерн «Фабрика».
Рассмотрим упрощённый пример кода:
На первый взгляд код рабочий, но здесь нарушаются два принципа SOLID:
1️⃣ Принцип единственной ответственности (Single Responsibility Principle): класс Document отвечает сразу за создание объектов XMLImporter, JSONImporter и их использование.
2️⃣ Принцип открытости / закрытости (Open / Closed Principle).
Если появится новый формат (например, CSV), придётся снова лезть в метод import_file и расширять блок if / elif.
Решение: фабрика
Чтобы убрать ответственность за создание объектов из класса Document, вводится так называемася фабрика. Это объект, который берёт на себя ответственность за создание других объектов. Она не избавляет нас полностью от изменения кода при добавлении новых форматов, но значительно улучшает читаемость, гибкость и расширяемость системы.
Теперь
Преимущества фабрики
1️⃣ Изоляция ответственности: логика создания объектов теперь сосредоточена только в одном месте — в ImporterFactory.
2️⃣ Гибкость: если нужно добавить поддержку нового формата (CSV, YAML и т.д.), достаточно изменить фабрику, не трогая Document.
3️⃣ Удобное тестирование: в тестах можно подменить фабрику на «фиктивную» (mock) и проверять работу Document независимо от импортеров.
Что дальше?
Фабрика — это базовый паттерн. На его основе строятся более сложные подходы, например:
🔘 Factory Method — когда создание объектов делегируется подклассам.
🔘 Abstract Factory — когда фабрика создаёт целые семейства объектов, согласованных между собой.
#основы
@zen_of_python
В программировании довольно часто встречается задача: в зависимости от входных данных нужно выбрать подходящий класс для обработки. Например, у нас есть XML-файлы и JSON-файлы, и мы хотим написать систему, которая будет их импортировать.
На первый взгляд решение простое — внутри метода проверять расширение файла и создавать соответствующий объект. Но у такого подхода есть несколько серьёзных недостатков. Именно здесь на помощь приходит паттерн «Фабрика».
Рассмотрим упрощённый пример кода:
class XMLImporter:
def __init__(self, filename):
self.filename = filename
def execute(self):
print("XML imported from", self.filename)
class JSONImporter:
def __init__(self, filename):
self.filename = filename
def execute(self):
print("JSON imported from", self.filename)
class Document:
def import_file(self, filename):
if filename.endswith(".xml"):
importer = XMLImporter(filename)
elif filename.endswith(".json"):
importer = JSONImporter(filename)
data = importer.execute()
document = Document()
document.import_file("text.xml")
document.import_file("text.json")
На первый взгляд код рабочий, но здесь нарушаются два принципа SOLID:
Если появится новый формат (например, CSV), придётся снова лезть в метод import_file и расширять блок if / elif.
Решение: фабрика
Чтобы убрать ответственность за создание объектов из класса Document, вводится так называемася фабрика. Это объект, который берёт на себя ответственность за создание других объектов. Она не избавляет нас полностью от изменения кода при добавлении новых форматов, но значительно улучшает читаемость, гибкость и расширяемость системы.
class ImporterFactory:
def get_importer(self, filename):
if filename.endswith(".xml"):
return XMLImporter(filename)
elif filename.endswith(".json"):
return JSONImporter(filename)
Теперь
Document
больше не знает, как именно создаются нужные классы. Ему просто передают готовую фабрику:
class Document:
def __init__(self, factory):
self.factory = factory
def import_file(self, filename):
importer = self.factory.get_importer(filename)
data = importer.execute()
factory = ImporterFactory()
document = Document(factory)
document.import_file("text.xml")
document.import_file("text.json")
Преимущества фабрики
Что дальше?
Фабрика — это базовый паттерн. На его основе строятся более сложные подходы, например:
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
👎4🔥2❤1
Что такое замыкание и зачем оно нужно
Замыкания (Closures) — понятие, которое кажется сложным при первом знакомстве. Но на самом деле вы уже его скорее всего используете неосознанно, настолько это стало базой.
Представим программу, где пользователь вводит число, нажимает OK, и программа сохраняет это число в список, выводя все введённые значения:
Код работает, но есть проблема: переменная
🔘 функция зависит от переменной, объявленной в другом месте;
🔘 код становится менее гибким — нельзя просто перенести функцию в другой модуль, не взяв с собой
Замыкание помогает «связать» данные и логику в одном месте без использования классов:
Когда мы вызываем внешнюю функцию
Ключевая идея замыкания:
Даже когда
Используйте замыкания, если хотите:
🔘 инкапсулировать состояние в функции без создания класса;
🔘 нужно создать функцию-конфигуратор (например, с частично зафиксированными параметрами);
Замыкание — это функция, которая:
🔘 определена внутри другой функции;
🔘 использует переменные из внешней функции;
🔘 «запоминает» эти переменные даже после завершения внешней функции.
#основы
@zen_of_python
Замыкания (Closures) — понятие, которое кажется сложным при первом знакомстве. Но на самом деле вы уже его скорее всего используете неосознанно, настолько это стало базой.
Представим программу, где пользователь вводит число, нажимает OK, и программа сохраняет это число в список, выводя все введённые значения:
numbers = []
def enter_number(x):
numbers.append(x)
print(numbers)
enter_number(3) # [3]
enter_number(7) # [3, 7]
enter_number(4) # v
Код работает, но есть проблема: переменная
numbers
находится вне функции, то есть она глобальная. Это значит, что:numbers
.Замыкание помогает «связать» данные и логику в одном месте без использования классов:
def enter_number_outer():
numbers = [] # локальная переменная
def enter_number_inner(x):
numbers.append(x)
print(numbers)
return enter_number_inner
Когда мы вызываем внешнюю функцию
enter_number_outer()
, она создаёт свой контекст с переменной numbers
и возвращает внутреннюю функцию, которая имеет к ней доступ.
enter_num = enter_number_outer()
enter_num(3) # [3]
enter_num(7) # [3, 7]
enter_num(4) # [3, 7, 4]
Ключевая идея замыкания:
Внутренняя функция «замыкает» (сохраняет) значения переменных из области видимости внешней функции.
Даже когда
enter_number_outer()
завершает выполнение, её переменные не уничтожаются, потому что они нужны внутренней функции, которая всё ещё существует. Это и есть closure — функция, которая запоминает контекст, в котором была создана.Используйте замыкания, если хотите:
Замыкание — это функция, которая:
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
👍12❤2🤣1
Как писать docstrings
Докстринги (буквально «строки документации») — это встроенная в код документация (обычно после инициализации функции / класса и прочих объектов между двумя '''), которую могут читать люди и инструменты (help(), pydoc, автогенераторы). В этом лонгриде мы разберемся, где и как их писать.
Зачем нужны docstrings — и чем они отличаются от комментариев
🔘 Комментарии (#) объясняют реализацию и помогают разработчикам; интерпретатор их игнорирует.
🔘 Докстринги — это строковые литералы (обычно в
Докстринги описывают интерфейс (что делает код, какие аргументы и что возвращает), а комментарий — реализацию и все остальное.
Многострочные докстринги используются когда нужно подробнее описать параметры, поведение, побочные эффекты, примеры использования. По PEP 257 закрывающие кавычки обычно ставят на отдельной строке в многострочном docstring:
Чтобы получить доступ к docstring в коде и терминале, вызываем:
🔘
🔘
🔘
Что писать в docstring для модулей, функций и классов
Модуль:
🔘 Краткое описание назначения модуля.
🔘 При необходимости — описание экспортируемых переменных/классов/функций, примеры использования.
Функция / метод:
🔘 Краткое резюме (1–2 предложения).
🔘 Секция
🔘 Секция
🔘 Исключения: какие ошибки может выбросить функция (опционально, но полезно).
🔘 Пример использования или заметки о поведении (если нужно).
Класс:
🔘 Краткое описание назначения класса.
🔘 Описание атрибутов (публичных), краткая информация о методах (если интерфейс не очевиден).
🔘 Для сложных иерархий — примеры создания/использования. ([realpython.com][1])
#основы
@zen_of_python
Докстринги (буквально «строки документации») — это встроенная в код документация (обычно после инициализации функции / класса и прочих объектов между двумя '''), которую могут читать люди и инструменты (help(), pydoc, автогенераторы). В этом лонгриде мы разберемся, где и как их писать.
Зачем нужны docstrings — и чем они отличаются от комментариев
"""`), помещённые сразу после определения модуля / функции / класса / метода; они сохраняются в атрибуте
.__doc__` и доступны в рантайме (через .__doc__
, help()
и инструментах вроде pydoc
. Докстринги описывают интерфейс (что делает код, какие аргументы и что возвращает), а комментарий — реализацию и все остальное.
Многострочные докстринги используются когда нужно подробнее описать параметры, поведение, побочные эффекты, примеры использования. По PEP 257 закрывающие кавычки обычно ставят на отдельной строке в многострочном docstring:
def get_book(publication_year, title):
"""
Retrieve a Harry Potter book by its publication year and name.
Parameters:
publication_year (int): The year the book was published.
title (str): The title of the book.
Returns:
str: A sentence describing the book and its publication year.
"""
Чтобы получить доступ к docstring в коде и терминале, вызываем:
obj.__doc__
— возвращает сырой docstring (часто краткий);help(obj)
— даёт структурированный вывод, полезный для модулей и классов;python -m pydoc module
— позволяет просматривать документацию из терминала и генерировать статичные страницы. Что писать в docstring для модулей, функций и классов
Модуль:
Функция / метод:
Parameters`/`Args
: имена параметров, типы, краткое описание.Returns
/ Yields
: что возвращается, тип.Класс:
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1👍1
Зачем нужны «ленивые» (lazy) импорты
Когда модуль импортируется, интерпретатор выполняет весь код на глобальном уровне этого модуля, включая все его собственные импорты и инициализации. В больших приложениях и тестовых наборах это может заметно замедлять запуск и фазу сбора тестов. Поэтому идея «ленивого импорта» — откладывать импорт «тяжёлых» зависимостей до момента, когда они действительно понадобятся — помогает улучшить отзывчивость приложения и сократить время тестирования.
Переносим import внутрь функции
Самый очевидный и безопасный способ сделать импорт ленивым — переместить
Плюсы: простота. Минусы: если импорт нужен во многих местах, придётся либо дублировать
Вариант с importlib — когда нужно контролировать пространство имён
Если хочется более явного контроля (например, избежать появления имени в локальной области каждой функции), можно использовать
Пример:
Как найти «тяжёлые» импорты — инструмент `python -X importtime
Прежде чем делать импорты ленивыми, полезно понять, что именно тормозит загрузку. Для этого есть встроенная опция:
Особая зона внимания — pytest и фаза collection
Pytest во время collection импортирует все тестовые файлы — следовательно, импорты в глобальной области тестов будут исполнены на этапе collection, даже если сам тест не будет запущен. Это распространённый источник задержек в больших тестовых наборах. Решение — переносить импорты внутрь тестовых функций, использовать
«Глобальный» трюк
Если модуль содержит множество функций, которые все используют одну и ту же тяжёлую библиотеку, имеет смысл импортировать её при первом нужном вызове и сохранить в глобальной переменной модуля (через `global`).
Короткая иллюстрация:
Когда ленивые импорты — плохая идея
🔘 Если импорт жизненно важен для модуля и должен бросать ошибки во время старта (fail fast), откладывание импорта может скрыть проблему до момента выполнения, что усложнит отладку.
🔘 Когда импорт идёт с побочными эффектами, которые вы ожидаете увидеть при импортировании модуля — откладывая импорт, вы меняете поведение.
#основы
@zen_of_python
Когда модуль импортируется, интерпретатор выполняет весь код на глобальном уровне этого модуля, включая все его собственные импорты и инициализации. В больших приложениях и тестовых наборах это может заметно замедлять запуск и фазу сбора тестов. Поэтому идея «ленивого импорта» — откладывать импорт «тяжёлых» зависимостей до момента, когда они действительно понадобятся — помогает улучшить отзывчивость приложения и сократить время тестирования.
Переносим import внутрь функции
Самый очевидный и безопасный способ сделать импорт ленивым — переместить
import
из глобальной области видимости внутрь функции или метода, где ресурс реально используется. При таком подходе импорт произойдёт только при первом вызове этой функции (и далее кешируется в sys.modules
, поэтому реальной «повторной» загрузки не происходит). Это даёт быстрый выигрыш для модулей, которые редко используются или инициализируют тяжёлые зависимости:
def do_heavy_task():
import heavy_lib
heavy_lib.run()
Плюсы: простота. Минусы: если импорт нужен во многих местах, придётся либо дублировать
import
(что допустимо), либо устанавливать глобальную переменную после первого импорта.Вариант с importlib — когда нужно контролировать пространство имён
Если хочется более явного контроля (например, избежать появления имени в локальной области каждой функции), можно использовать
importlib.import_module()
и присваивать результат в переменную (глобальную или локальную). Это зачастую полезно при динамическом импорте по имени строки:Пример:
from importlib import import_module
def use_feature():
mod = import_module("heavy_lib")
mod.do()
Как найти «тяжёлые» импорты — инструмент `python -X importtime
Прежде чем делать импорты ленивыми, полезно понять, что именно тормозит загрузку. Для этого есть встроенная опция:
python -X importtime your_program.py
— она выводит дерево импорта с временами, позволяя увидеть самые затратные узлы. Это особенно полезно при оптимизации большого проекта или ускорении фазы сбора тестов.Особая зона внимания — pytest и фаза collection
Pytest во время collection импортирует все тестовые файлы — следовательно, импорты в глобальной области тестов будут исполнены на этапе collection, даже если сам тест не будет запущен. Это распространённый источник задержек в больших тестовых наборах. Решение — переносить импорты внутрь тестовых функций, использовать
importlib
внутри тестов. «Глобальный» трюк
Если модуль содержит множество функций, которые все используют одну и ту же тяжёлую библиотеку, имеет смысл импортировать её при первом нужном вызове и сохранить в глобальной переменной модуля (через `global`).
Короткая иллюстрация:
# module.py
heavy = None
def first_use():
global heavy
if heavy is None:
import heavy_lib
heavy = heavy_lib
heavy.do()
Когда ленивые импорты — плохая идея
#основы
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1