Библиотека Python разработчика | Книги по питону
Погружение в CPython и архитектуру. Разбираем неочевидное поведение (GIL, Memory), Best Practices (SOLID, DDD) и тонкости Django/FastAPI. Решаем задачи с подвохом и оптимизируем алгоритмы. 🐍 По всем вопросам @evgenycarter РКН clck.ru/3Ko7Hq
显示更多📈 Telegram 频道 Библиотека Python разработчика | Книги по питону 的分析概览
频道 Библиотека Python разработчика | Книги по питону (@bookpython) 俄语 语言赛道中的 是活跃参与者。目前社区聚集了 18 128 名订阅者,在 技术与应用 类别中位列第 7 078,并在 俄罗斯 地区排名第 36 538 位。
📊 受众指标与增长动态
自 невідомо 创建以来,项目保持高速增长,吸引了 18 128 名订阅者。
根据 25 八月, 2026 的最新数据,频道保持稳定运转。过去 30 天订阅人数变化为 -71,过去 24 小时变化为 -6,整体触达仍然可观。
- 认证状态: 未认证
- 互动率 (ER): 平均受众互动率为 5.49%。内容发布后 24 小时内通常能获得 2.60% 的反应,占订阅者总量。
- 帖子覆盖: 每篇帖子平均可获得 995 次浏览,首日通常累积 472 次浏览。
- 互动与反馈: 受众积极参与,单帖平均反应数为 3。
- 主题关注点: 内容集中在 numbers, yield, модуль, none, декоратор 等核心主题上。
📝 描述与内容策略
作者将该频道定位为表达主观观点的平台:
“Погружение в CPython и архитектуру. Разбираем неочевидное поведение (GIL, Memory), Best Practices (SOLID, DDD) и тонкости Django/FastAPI. Решаем задачи с подвохом и оптимизируем алгоритмы. 🐍
По всем вопросам @evgenycarter
РКН clck.ru/3Ko7Hq”
凭借高频更新(最新数据采集于 26 八月, 2026),频道始终保持新鲜度与高覆盖。分析显示受众积极互动,使其成为 技术与应用 类别中的关键影响点。
try ... except Exception, которая предназначена для отлова «любых ошибок». Чтобы безопасно это обработать внутри корутины, приходится писать примерно так:
try:
await action()
except asyncio.CancelledError:
raise
except Exception:
logging.exception('action failed')
📲 Мы в MAX
👉@BookPythonwith внутри блока if, не заключив туда весь блок with. Это часто приводит к дублированию кода:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
if path:
with open(path) as f:
print(f.read(), end='')
else:
print(file_obj.read(), end='')
Способ борьбы с этой проблемой — использовать ExitStack и вызывать enter_context внутри if:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
with ExitStack() as stack:
if path:
file_obj = stack.enter_context(
open(path)
)
print(file_obj.read(), end='')
Однако более очевидный способ достичь того же — использовать тривиальные менеджеры контекста, которые ничего не делают, когда они не нужны, вместо «настоящих». Начиная с Python 3.7, их можно получить с помощью contextlib.nullcontext:
def print_whole_file(
*,
path: Optional[str] = None,
file_obj: Optional[TextIO] = None
):
assert path or file_obj
if path:
context = open(path)
else:
context = nullcontext(file_obj)
with context as f:
print(f.read(), end='')
📲 Мы в MAX
👉@BookPythonrange(2, 10) математически означает [2, 10),
или, говоря на языке Python: [2, 3, 4, 5, 6, 7, 8, 9].
Несмотря на асимметрию, это не ошибка и не случайность.
В этом есть логика: такой подход позволяет "склеивать" два соседних интервала без риска ошибиться на единицу:
[a, c) = [a, b) + [b, c)
Для сравнения, если бы использовались закрытые интервалы, получалось бы так:
[a, c] = [a, b] + [b+1, c]
Эта же идея объясняет, почему индексация начинается с нуля:
[0, N) содержит ровно N элементов.
Эдсгер Дейкстра написал на эту тему отличную статью ещё в 1982 году.
📲 Мы в MAX
👉@BookPythonfor и условия if:
In : [(x, y) for x in range(3) for y in range(3)]
Out: [
(0, 0), (0, 1), (0, 2),
(1, 0), (1, 1), (1, 2),
(2, 0), (2, 1), (2, 2)
]
In : [
(x, y)
for x in range(3)
for y in range(3)
if x != 0
if y != 0
]
Out: [(1, 1), (1, 2), (2, 1), (2, 2)]
Кроме того, любое выражение внутри for и if может использовать все переменные, которые были определены ранее:
In : [
(x, y)
for x in range(3)
for y in range(x + 2)
if x != y
]
Out: [
(0, 1),
(1, 0), (1, 2),
(2, 0), (2, 1), (2, 3)
]
Можно смешивать if и for в любом порядке:
In : [
(x, y)
for x in range(5)
if x % 2
for y in range(x + 2)
if x != y
]
Out: [
(1, 0), (1, 2),
(3, 0), (3, 1), (3, 2), (3, 4)
]
📲 Мы в MAX
👉@BookPythonx in g.
Python будет итерироваться по g, пока не найдёт x или пока генератор не закончится.
>>> def g():
... print(1)
... yield 1
... print(2)
... yield 2
... print(3)
... yield 3
...
>>> 2 in g()
1
2
True
Однако range() делает для вас больше.
У него переопределён магический метод __contains__, который позволяет оператору in работать с O(1) сложностью:
In [1]: %timeit 10**20 in range(10**30)
375 ns ± 10.7 ns per loop
Имейте в виду, что это не работает для функции xrange() в Python 2.
📲 Мы в MAX
👉@BookPython[]), реализовав магический метод __getitem__.
Вот так можно создать объект, который виртуально содержит бесконечное количество повторяющихся элементов:
class Cycle:
def __init__(self, lst):
self._lst = lst
def __getitem__(self, index):
return self._lst[index % len(self._lst)]
print(Cycle(['a', 'b', 'c'])[100]) # 'b'
Необычность здесь в том, что оператор [] поддерживает особый синтаксис. Его можно использовать не только так — [2], но и так — [2:10], [2:10:2], [2::2] или даже [:].
Обычно это интерпретируется как [start:stop:step], но вы можете задать любую логику для своих объектов.
Что же передаётся в параметр index метода __getitem__, если использовать такой синтаксис? Для этого в Python существуют объекты slice.
class Inspector:
def __getitem__(self, index):
print(index)
Inspector()[1]
# 1
Inspector()[1:2]
# slice(1, 2, None)
Inspector()[1:2:3]
# slice(1, 2, 3)
Inspector()[:]
# slice(None, None, None)
Можно даже комбинировать кортежи и срезы:
Inspector()[:, 0, :]
# (slice(None, None, None), 0, slice(None, None, None))
Объект slice сам по себе ничего не делает — он просто хранит атрибуты start, stop и step:
s = slice(1, 2, 3)
s.start # 1
s.stop # 2
s.step # 3
📲 Мы в MAX
👉@BookPythondatetime имеют специальный интерфейс для поддержки часовых поясов (а именно атрибут tzinfo), но сам модуль реализует этот интерфейс лишь частично, оставляя остальную работу другим модулям.
Наиболее популярный модуль для этой задачи — pytz.
Хитрость в том, что pytz не полностью соответствует интерфейсу tzinfo. В документации pytz это указано уже в первых строках:
«Эта библиотека отличается от задокументированного Python API для реализаций tzinfo».Вы не можете использовать объекты часовых поясов
pytz напрямую в качестве tzinfo. Если попробовать, можно получить совершенно неожиданные результаты:
In : paris = pytz.timezone('Europe/Paris')
In : str(datetime(2017, 1, 1, tzinfo=paris))
Out: '2017-01-01 00:00:00+00:09'
Обратите внимание на смещение +00:09.
Правильное использование pytz выглядит так:
In : str(paris.localize(datetime(2017, 1, 1)))
Out: '2017-01-01 00:00:00+01:00'
Также после любых арифметических операций с датами рекомендуется нормализовать объект datetime на случай изменения смещения (например, на границе перехода на летнее время):
In : new_time = time + timedelta(days=2)
In : str(new_time)
Out: '2018-03-27 00:00:00+01:00'
In : str(paris.normalize(new_time))
Out: '2018-03-27 01:00:00+02:00'
Начиная с Python 3.6, рекомендуется использовать dateutil. tz вместо pytz.
Он полностью совместим с tzinfo, может напрямую передаваться в атрибут tzinfo, не требует нормализации, хотя работает немного медленнее.
📲 Мы в MAX
👉@BookPython
>>> '\N{EM DASH}'
'—'
>>> '\u2014'
'—'
Это также совместимо с f-строками:
>>> width = 800
>>> f'Width \N{EM DASH} {width}'
'Width — 800'
📲 Мы в MAX
👉@BookPython
class Task:
_task_id = 0
def __init__(self):
self._id = self._task_id
type(self)._task_id += 1
Учтите, что нельзя писать self._task_id += 1. Это создаст атрибут _task_id в экземпляре, а не в классе. Вместо этого стоит использовать фабричный метод, чтобы сделать код красивее:
class Task:
_task_id = 0
def __init__(self, task_id):
self._id = task_id
@classmethod
def create(cls):
obj = cls(cls._task_id)
cls._task_id += 1
return obj
Эта версия также проще для тестирования, так как можно легко задать любой пользовательский ID.
📲 Мы в MAX
👉@BookPythonbreak подавляет исключение, если используется в блоке finally, даже когда блок except отсутствует:
for i in range(10):
try:
1 / i
finally:
print('finally')
break
print('after try')
print('after while')
Вывод:
finally after whileТо же самое верно и для
continue, однако его нельзя использовать в блоке finally до версии Python 3.8:
SyntaxError: 'continue' not supported inside 'finally' clause📲 Мы в MAX 👉@BookPython
await asyncio.sleep(x):
import asyncio
async def do(n=0):
print(n)
await asyncio.sleep(1)
loop.create_task(do(n + 1))
loop.create_task(do(n + 1))
loop = asyncio.get_event_loop()
loop.create_task(do())
loop.run_forever()
Однако создание новой задачи может быть затратным и не требуется, если вы не собираетесь выполнять асинхронные операции (как в функции do из примера).
Другой способ сделать это — использовать функции loop.call_later и loop.call_at, которые планируют вызов асинхронного колбэка:
import asyncio
def do(n=0):
print(n)
loop = asyncio.get_event_loop()
loop.call_later(1, do, n+1)
loop.call_later(1, do, n+1)
loop = asyncio.get_event_loop()
do()
loop.run_forever()
📲 Мы в MAX
👉@BookPython__call__.
class SavingOrig:
def __init__(self, another_decorator):
self._another = another_decorator
def __call__(self, f):
decorated = self._another(f)
if hasattr(f, 'orig'):
decorated.orig = f.orig
else:
decorated.orig = f
return decorated
saving_orig = SavingOrig
Последняя строка позволяет одновременно дать классу имя в стиле CamelCase и сохранить имя декоратора в стиле snake_case.
📲 Мы в MAX
👉@BookPython__hash__. Этот метод может возвращать любое целое число при одном важном условии: равные объекты должны иметь одинаковые хэши (обратное не обязательно).
Также следует избегать использования изменяемых объектов в качестве ключей, потому что если объект изменится и перестанет быть равным самому себе в прошлом состоянии, его больше нельзя будет найти в словаре.
Есть ещё один странный момент, который может удивить при отладке или написании юнит-тестов:
class A:
def __init__(self, x):
self.x = x
def __hash__(self):
return self.x
hash(A(2)) # 2
hash(A(1)) # 1
hash(A(0)) # 0
hash(A(-1)) # -2 (!)
hash(A(-2)) # -2
В CPython значение -1 зарезервировано для внутренних состояний ошибок, поэтому оно автоматически преобразуется в -2.
📲 Мы в MAX
👉@BookPythonexcept переменные, в которых хранятся перехваченные исключения, удаляются из locals(), даже если они существовали раньше:
>>> e = 2
>>> try:
... 1/0
... except Exception as e:
... pass
...
>>> e
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'e' is not defined
Если нужно сохранить ссылку на исключение, используйте другую переменную:
>>> error = None
>>> try:
... 1/0
... except Exception as e:
... error = e
...
>>> error
ZeroDivisionError('division by zero',)
В Python 2 это правило не действует.
📲 Мы в MAX
👉@BookPythonsys.builtin_module_names. Примеры таких модулей — sys, gc, time и т. д.
Обычно вам не важно, является ли модуль встроенным или нет; однако стоит иметь в виду, что import сначала ищет модуль среди встроенных. Поэтому будет загружен встроенный модуль sys, даже если в текущей директории есть файл sys.py. С другой стороны, если, например, в текущей директории есть файл datetime.py, он действительно может быть загружен вместо стандартного модуля datetime.
📲 Мы в MAX
👉@BookPythoncollections.defaultdict позволяет создать словарь, который возвращает значение по умолчанию, если запрашиваемого ключа нет (вместо того чтобы выбрасывать исключение KeyError).
При создании defaultdict необходимо указывать не само значение по умолчанию, а фабрику для его создания.
Это позволяет создавать словари с бесконечным числом вложенных уровней, что дает возможность писать что-то вроде d[a][b][c]...[z].
>>> def infinite_dict():
... return defaultdict(infinite_dict)
...
>>> d = infinite_dict()
>>> d[1][2][3][4] = 10
>>> dict(d[1][2][3][5])
{}
Такое поведение называется “автовивификация” (от англ. autovivification) — термин пришёл из языка Perl.
📲 Мы в MAX
👉@BookPythonislice:
from itertools import islice
def fib():
a, b = 0, 1
while True:
yield b
a, b = b, (a + b)
list(islice(fib(), 5))
# Результат: [1, 1, 2, 3, 5]
Если вы также хотите получить индексы элементов, можно применить enumerate:
list(enumerate(islice(fib(), 5)))
# Результат: [(0, 1), (1, 1), (2, 2), (3, 3), (4, 5)]
Другой способ сделать это — использовать zip и range, что может показаться более читаемым:
list(zip(range(5), fib()))
# Результат: [(0, 1), (1, 1), (2, 2), (3, 3), (4, 5)]
📲 Мы в MAX
👉@BookPythonfork для создания нового процесса, текущее состояние генератора случайных чисел (включая seed) копируется в дочерний процесс. Это может привести к тому, что разные процессы будут генерировать одинаковые «случайные» значения.
Чтобы избежать этого, необходимо вручную вызывать random.seed() в каждом процессе.
Однако, если вы используете модуль multiprocessing, он уже автоматически выполняет это за вас.
Пример:
import multiprocessing
import random
import os
import sys
def test(a):
print(random.choice(a), end=' ')
a = [1, 2, 3, 4, 5]
# Вызов в основном процессе
for _ in range(5):
test(a)
print()
# Вызов с multiprocessing.Process
for _ in range(5):
p = multiprocessing.Process(
target=test, args=(a,)
)
p.start()
p.join()
print()
# Вызов с использованием os.fork
for _ in range(5):
pid = os.fork()
if pid == 0:
test(a)
sys.exit()
else:
os.wait()
print()
Вывод будет примерно такой:
4 4 4 5 5 1 4 1 3 3 2 2 2 2 2Причём, начиная с Python 3.7,
os.fork также использует механизм at_fork hook, который переинициализирует генератор случайных чисел, как и multiprocessing.
Так что в Python 3.7+ вывод кода выше может быть таким:
1 2 2 1 5 4 4 4 5 5 2 4 1 3 1📲 Мы в MAX 👉@BookPython
