Библиотека Python разработчика | Книги по питону
18K subscribers
1.06K photos
403 videos
82 files
1.25K links
Погружение в CPython и архитектуру. Разбираем неочевидное поведение (GIL, Memory), Best Practices (SOLID, DDD) и тонкости Django/FastAPI. Решаем задачи с подвохом и оптимизируем алгоритмы. 🐍

По всем вопросам @evgenycarter

РКН clck.ru/3Ko7Hq
Download Telegram
Обычно вы взаимодействуете с генератором, запрашивая данные с помощью next(gen). В Python 3 вы также можете отправлять значения обратно в генератор с помощью g.send(x). Но существует техника, которой вы, вероятно, не пользуетесь каждый день, а возможно, и вовсе не знаете: выбрасывание исключений внутри генератора.

С помощью gen.throw(e) можно выбросить исключение в той точке, где генератор gen приостановлен — то есть на инструкции yield. Если генератор обрабатывает это исключение, gen.throw(e) возвращает следующее значение, полученное через yield (или выбрасывает StopIteration, если генератор завершён). Если генератор не перехватывает исключение, оно пробрасывается обратно к вызывающему коду.


def gen():
try:
yield 1
except ValueError:
yield 2

g = gen()

next(g)
# Out: 1

g.throw(ValueError)
# Out: 2

g.throw(RuntimeError('TEST'))
# RuntimeError: TEST


Эта техника позволяет более точно управлять поведением генератора — не только передавать данные внутрь, но и, например, сообщать о проблемах со значениями, полученными через yield. Однако такие случаи бывают редко, и встретить g.throw в дикой природе почти невозможно.

Тем не менее, декоратор @contextmanager из модуля contextlib использует именно такую технику, позволяя коду внутри контекста перехватывать исключения.


from contextlib import contextmanager

@contextmanager
def atomic():
print('BEGIN')

try:
yield
except Exception:
print('ROLLBACK')
else:
print('COMMIT')

with atomic():
print('ERROR')
raise RuntimeError()



BEGIN
ERROR
ROLLBACK


📲 Мы в MAX

👉@BookPython
👍3
Стандартный механизм расширения путей в оболочке называется globbing. Шаблоны, которые вы используете для сопоставления путей, называются globs.


$ echo /li*
/lib /lib64


Python поддерживает globbing с помощью модуля glob. Однако есть важное замечание: оболочка возвращает сам шаблон, если файлы не найдены, а Python — нет:


$ echo /zz**
/zz**
$ python -c 'from glob import glob; print(glob("/zz**"))'
[]


📲 Мы в MAX

👉@BookPython
👍3❤1🔥1
Функция super() позволяет обращаться к родительскому (базовому) классу. Это может быть очень полезно в случаях, когда производный класс хочет добавить что-то к реализации метода, а не полностью переопределять его:


class BaseTestCase(TestCase):
def setUp(self):
self._db = create_db()

class UserTestCase(BaseTestCase):
def setUp(self):
super().setUp()
self._user = create_user()


Имя функции super не означает "отличный" или "очень хороший". В данном контексте слово super означает "выше" (как, например, в слове superintendent — заведующий). Несмотря на это, super() не всегда ссылается на базовый класс — он может вернуть и "соседний" класс. Более точным названием была бы, возможно, функция next(), так как возвращается следующий класс согласно цепочке разрешения методов (MRO — Method Resolution Order).

Пример:


class Top:
def foo(self):
return 'top'

class Left(Top):
def foo(self):
return super().foo()

class Right(Top):
def foo(self):
return 'right'

class Bottom(Left, Right):
pass

# выводит 'right'
print(Bottom().foo())


Обрати внимание: результат работы super() может отличаться в зависимости от MRO вызвавшего объекта.


>>> Bottom().foo()
'right'
>>> Left().foo()
'top'


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4❤1🔥1
Функция map вызывает другую функцию для каждого элемента итерируемого объекта. Это значит, что функция должна принимать одно значение в качестве аргумента:


In : list(map(lambda x: x ** 2, [1, 2, 3]))
Out: [1, 4, 9]


Однако если каждый элемент итерируемого объекта — это кортеж, было бы удобно передавать каждый элемент кортежа как отдельный аргумент. В Python 2 это было возможно благодаря распаковке параметров кортежа (обратите внимание на скобки):


>>> map(lambda (a, b): a + b, [(1, 2), (3, 4)])
[3, 7]


В Python 3 эта возможность исчезла, но есть другое решение — itertools.starmap. Она распаковывает кортежи за вас, будто функция вызывается со звёздочкой: f(*arg) (отсюда и название функции):


from itertools import starmap

In [3]: list(starmap(lambda a, b: a + b, [(1, 2), (3, 4)]))
Out[3]: [3, 7]


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Когда вы используете модуль multiprocessing, и в одном из процессов происходит исключение, оно передаётся в основную программу с помощью механизма сериализации (pickling). Исключение сериализуется, передаётся в другой процесс и там десериализуется обратно.

Однако сериализация исключений может быть непростой задачей. Исключение создаётся с любым количеством аргументов, которые сохраняются в атрибуте args. Эти же аргументы используются при десериализации для воссоздания объекта исключения.

Но это может не сработать так, как вы ожидаете, особенно если используется наследование. Посмотрите на пример:


import pickle

class TooMuchWeightError(Exception):
def __init__(self, weight):
super().__init__()
self._weight = weight

pickled = pickle.dumps(TooMuchWeightError(42))
pickle.loads(pickled)


Вызов TooMuchWeightError.__init__ приводит к вызову Exception.__init__, который устанавливает args как пустой кортеж. Этот пустой кортеж затем используется в качестве аргументов при десериализации, что, очевидно, приводит к ошибке:


TypeError: __init__() missing 1 required positional argument: 'weight'


Обходное решение — либо вообще не вызывать super().__init__() (что обычно считается плохой практикой при наследовании), либо передавать все аргументы явно в конструктор родительского класса:


class TooMuchWeightError(Exception):
def __init__(self, weight):
super().__init__(weight)
self._weight = weight


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥1
yield from — элегантная передача управления

Если вы пишете генераторы, которые вызывают другие генераторы — забудьте про for x in sub(): yield x. Есть способ проще и мощнее.

Оператор yield from позволяет передавать элементы из подгенератора напрямую, без лишнего кода. Но фишка не только в лаконичности — он также автоматически пробрасывает исключения и возвращаемые значения из подгенератора.

Вот классика:


def gen():
for x in range(3):
yield x

def wrapper():
for x in gen():
yield x


Можно короче и лучше:


def wrapper():
yield from gen()


Но главное — yield from пробрасывает return-значение из подгенератора (начиная с Python 3.3):


def sub():
yield 1
yield 2
return 'done'

def main():
result = yield from sub()
print('Sub returned:', result)

for _ in main():
pass
# Выведет: Sub returned: done


А ещё через yield from можно проксировать значения внутрь генератора — например, в сопрограммах:


def delegator():
result = yield from coroutine()
print('coroutine done:', result)

def coroutine():
x = yield
y = yield
return x + y

g = delegator()
next(g) # Старт
next(g) # coroutine ждет x
g.send(10) # x = 10
print(g.send(20)) # y = 20 → return 30
# Выведет: coroutine done: 30


Итог: если вы пишете генераторы — освоение yield from даст вам лаконичный синтаксис, проброс return-значений, исключений и взаимодействие на новом уровне.

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1👍1
default_factory в dataclass: мощнее, чем кажется

Многие используют dataclass как удобный способ задать структуру с полями. Но редко кто по-настоящему раскрывает силу default_factory. А зря — он спасает от багов и даёт гибкость.

Когда нужно задать значение по умолчанию для поля в dataclass, логично тянуться к default=. Но если это изменяемый тип (например, список или словарь) — вас поджидает ловушка.


from dataclasses import dataclass, field

@dataclass
class User:
name: str
tags: list[str] = [] # ⚠️ опасно!


Все экземпляры User будут делить один и тот же список. То есть:


a = User("Alice")
b = User("Bob")
a.tags.append("admin")

print(b.tags) # ['admin'] 😱


Вместо этого используйте default_factory:


@dataclass
class User:
name: str
tags: list[str] = field(default_factory=list)


Теперь у каждого User будет свой список:


a = User("Alice")
b = User("Bob")
a.tags.append("admin")

print(b.tags) # []


Но default_factory не только про списки. Это отличный способ задать любое значение "по умолчанию", включая кастомную логику:


import uuid

@dataclass
class Session:
id: str = field(default_factory=lambda: str(uuid.uuid4()))


Или, например, значения из окружения:


import os

@dataclass
class Config:
debug: bool = field(default_factory=lambda: os.getenv("DEBUG") == "1")


Кстати, это ещё и отличное место для внедрения DI:


@dataclass
class Service:
client: "Client" = field(default_factory=create_default_client)


default_factory — это маленький хак, который позволяет сделать код чище и безопаснее, особенно когда работаешь с изменяемыми структурами или сложной инициализацией.

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥2
Скрытые фичи Enum: как выжать максимум

Многие используют Enum как простой список констант. Но у enum.Enum в Python есть куда больше возможностей — и они могут сделать код чище и мощнее.


Вот несколько приёмов, которые мало кто использует — но зря.


1. Добавление поведения в Enum


from enum import Enum

class Status(Enum):
DRAFT = 'draft'
PUBLISHED = 'published'
ARCHIVED = 'archived'

def is_visible(self):
return self in {Status.DRAFT, Status.PUBLISHED}


Теперь Status.DRAFT.is_visible() — это просто и элегантно.


2. Enum с полями


from enum import Enum

class Color(Enum):
RED = ('#FF0000', 'danger')
GREEN = ('#00FF00', 'safe')

def __init__(self, hex_code, label):
self.hex_code = hex_code
self.label = label



Color.RED.hex_code # '#FF0000'
Color.RED.label # 'danger'



3. Автоматические значения с auto()


from enum import Enum, auto

class Role(Enum):
ADMIN = auto()
USER = auto()
GUEST = auto()


Удобно, если не важны конкретные значения, а нужны уникальные.


4. Строгая сериализация

В реальных приложениях (API, базы) лучше контролировать сериализацию enum'ов:


import json

class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Enum):
return obj.value
return super().default(obj)

json.dumps(Status.PUBLISHED, cls=CustomEncoder) # "published"



5. Сравнение по значению


Status('draft') == Status.DRAFT # True
Status('draft') is Status.DRAFT # True (enum гарантирует singleton)


Итого: Enum — это не просто константы. Это лёгкий способ инкапсулировать поведение и данные, улучшить читаемость и сделать код устойчивее к ошибкам.

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2❤1
Иногда в программе нужна очередь — контейнер, куда элементы добавляются с одной стороны и извлекаются с другой. В Python для этого можно использовать list:


In : lst = [1, 2, 3]
In : lst.pop()
Out: 3
In : lst
Out: [1, 2]
In : lst[:0] = [4] # push
In : lst
Out: [4, 1, 2]


Однако list выглядит не очень удобно (взгляните на этот "push") и работает неэффективно.


In : lst = [0] * 10_000_000

In : %timeit lst[:0] = [1]
9.5 ms ± 111 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In : %timeit lst.pop()
84.3 ns ± 4.01 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)


Как видно, операция pop() в 100 раз быстрее, чем вставка в начало списка. Это связано с тем, как устроен list в Python: легко добавлять и удалять элементы с конца, но удаление/добавление в начало требует создания нового списка.

Для очередей лучше использовать collections.deque. Он специально для этого создан:


In : from collections import deque
In : d = deque([1] * 100_000_000)
In : %timeit d.popleft()
65 ns ± 0.436 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)


📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Некоторый код может выводить интересующие вас данные в stdout, вместо того чтобы предоставлять API, возвращающий строку, пригодную для использования в программе.

Вместо рефакторинга такого кода можно воспользоваться менеджером контекста contextlib.redirect_stdout, который позволяет временно перенаправить stdout в любой объект, поддерживающий файловый интерфейс. В сочетании с io.StringIO это позволяет сохранить вывод в переменную.


from contextlib import redirect_stdout
from io import StringIO

s = StringIO()
with redirect_stdout(s):
print(42)

print(s.getvalue())


Также существует contextlib.redirect_stderr для перенаправления вывода sys.stderr.

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Популярный способ объявить абстрактный метод в Python — это выбросить исключение NotImplementedError:


def human_name(self):
raise NotImplementedError


Хотя этот подход довольно распространён и даже поддерживается IDE (например, PyCharm считает такие методы абстрактными), у него есть недостаток: ошибка возникает только при вызове метода, а не при создании экземпляра класса.

Чтобы избежать этой проблемы, используйте модуль abc:


from abc import ABCMeta, abstractmethod

class Service(metaclass=ABCMeta):
@abstractmethod
def human_name(self):
pass


Также важно помнить, что NotImplemented — это не то же самое, что NotImplementedError. NotImplemented — это специальное значение (как True и False), а не исключение. Оно используется, например, в специальных методах (__eq__(), __add__() и др.), чтобы сообщить Python, что операция не реализована для данного типа, и попытаться вызвать альтернативный метод (например, если a.__add__(b) возвращает NotImplemented, Python попробует вызвать b.__radd__(a)).

📲 Мы в MAX

👉@BookPython
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1