Python tricks, которые я использую каждый день
Это не список экзотических фишек ради демонстрации знания языка. Всё что ниже — то, что я реально использую в повседневной работе и что реально экономит время.
Walrus operator := (Python 3.8+)
Позволяет присваивать переменную прямо в выражении. Особенно полезно в while-циклах и условиях:
# Старый способ
line = file.readline()
while line:
process(line)
line = file.readline()
# С walrus
while line := file.readline():
process(line)
# В условиях
if m := re.search(r'\d+', text):
print(f"Found number: {m.group()}")
itertools — мощь без лишнего кода
Модуль, который надо знать наизусть:
from itertools import islice, chain, groupby, batched # batched — Python 3.12
# Разбить список на чанки (до 3.12)
def chunks(lst, n):
it = iter(lst)
while batch := list(islice(it, n)):
yield batch
# Разбить на чанки (3.12+)
for batch in batched(items, 100):
process_batch(batch)
# Объединить несколько итерируемых
all_items = list(chain(list1, list2, list3))
# Группировка (требует предварительной сортировки)
data = sorted(records, key=lambda x: x['status'])
for status, group in groupby(data, key=lambda x: x['status']):
print(f"{status}: {len(list(group))} records")
dataclasses с умом
Dataclasses — это не просто замена namedtuple. Несколько паттернов:
from dataclasses import dataclass, field, asdict
from typing import ClassVar
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
tags: list = field(default_factory=list) # правильный mutable default
_instance_count: ClassVar[int] = 0 # не попадёт в __init__
def __post_init__(self):
# Валидация при инициализации
if not 1 <= self.port <= 65535:
raise ValueError(f"Invalid port: {self.port}")
# Конвертация в dict (рекурсивная)
config = Config(host="db.internal", port=5432)
config_dict = asdict(config) # {'host': 'db.internal', 'port': 5432, 'tags': []}
contextlib для управления ресурсами
Не всегда нужен полноценный класс с __enter__/__exit__:
from contextlib import contextmanager, suppress, asynccontextmanager
import time
@contextmanager
def timer(name=""):
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"{name}: {elapsed:.3f}s")
with timer("database query"):
result = db.execute(heavy_query)
# suppress — игнорировать конкретные исключения
with suppress(FileNotFoundError):
os.unlink("/tmp/tempfile")
# asynccontextmanager для async кода
@asynccontextmanager
async def managed_connection(pool):
conn = await pool.acquire()
try:
yield conn
finally:
await pool.release(conn)
functools.cache / lru_cache
from functools import lru_cache, cache
# cache (Python 3.9+) — unbounded cache
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
# lru_cache с ограничением
@lru_cache(maxsize=128)
def get_user_permissions(user_id: int) -> frozenset:
return frozenset(db.get_permissions(user_id))
# Инвалидация кеша
get_user_permissions.cache_clear()
# Статистика
print(get_user_permissions.cache_info())
Типизация: TypedDict и Protocol
Вместо просто dict — TypedDict для явной документации структуры:
from typing import TypedDict, Protocol, runtime_checkable
class UserData(TypedDict):
id: int
name: str
email: str
is_active: bool
def process_user(user: UserData) -> None:
print(f"Processing {user['name']}")
# Protocol — duck typing с проверкой типов
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict: ...
def to_json(self) -> str: ...
def save(obj: Serializable) -> None:
data = obj.to_dict()
# ...
Параллелизм с asyncio
Паттерн, который использую для параллельных I/O операций:
import asyncio
from asyncio import TaskGroup # Python 3.11+
async def fetch_all(urls: list[str]) -> list[dict]:
async with TaskGroup() as tg:
tasks = [tg.create_task(fetch(url)) for url in urls]
return [t.result() for t in tasks]
# Semaphore для ограничения параллелизма
sem = asyncio.Semaphore(10)
async def fetch_limited(url: str):
async with sem:
return await fetch(url)
Список можно продолжать долго. Следующая часть будет про декораторы и метаклассы — но это уже когда будет желание объяснять магию.