Skip to content

Python на собеседовании: разбор вопросов с кодом

Что реально спрашивают на собеседованиях по Python — с короткими ответами и примерами кода: модель данных, ООП, генераторы, память и GIL, асинхронность, инструменты.

v3 0 stars 0 forks 0 watchers 1 branch 0 runs Public
Вычитка: три опечаткиv3

Как пользоваться

Это не сборник заученных формулировок, а карта того, что проверяют вопросом. Почти каждый вопрос на собеседовании задаётся не ради термина: спрашивающий хочет понять, держите ли вы в голове модель — как устроены объекты, ссылки и время жизни. Поэтому у ответов здесь есть код: формулировку можно вызубрить, а объяснить, что напечатает пример, — нет.

Если на вопрос хочется ответить одним словом, это почти всегда признак, что модель за словом не стоит.

Типы и модель данных

Какие в Python типы и как они делятся?

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

Неизменяемые: int, float, bool, str, bytes, tuple, frozenset, None. Изменяемые: list, dict, set, bytearray, обычные объекты своих классов.

И отдельно: имя в Python — не коробка со значением, а ярлык на объект. a = b не копирует объект, а вешает второй ярлык.

Чем list отличается от tuple?

Не «изменяемый и неизменяемый» — это следствие, а не ответ. Существенная разница в том, что неизменяемость даёт кортежу хешируемость, а значит право быть ключом словаря и элементом множества.

python
1d = {(55.75, 37.62): "Москва"} # ок
2d = {[55.75, 37.62]: "Москва"} # TypeError: unhashable type: 'list'

Оговорка, которую любят на собеседовании: кортеж хешируем не всегда — только если хешируемо всё, что внутри.

python
1hash((1, 2)) # ок
2hash((1, [2])) # TypeError

Что может быть ключом словаря?

Любой объект, у которого есть __hash__ и __eq__, и чей хеш не меняется за время жизни. Отсюда практическое правило: неизменяемые — можно, изменяемые — нельзя.

Обратная сторона, про которую забывают: если вы определили __eq__ в своём классе, Python снимает унаследованный __hash__, и объект перестаёт быть ключом. Возвращать его надо явно.

python
1class Point:
2 def __init__(self, x, y):
3 self.x, self.y = x, y
4
5 def __eq__(self, other):
6 return (self.x, self.y) == (other.x, other.y)
7
8 __hash__ = None # так делает Python сам; чтобы работало — верните хеш:
9 # def __hash__(self):
10 # return hash((self.x, self.y))

Отрицательные индексы и слайсы

Отрицательный индекс считает с конца: -1 — последний элемент. Слайс a[start:stop:step] берёт срез, stop не включается, любой из трёх параметров можно опустить.

python
1a = [0, 1, 2, 3, 4]
2a[-1] # 4
3a[1:4] # [1, 2, 3]
4a[::2] # [0, 2, 4]
5a[::-1] # [4, 3, 2, 1, 0] — разворот
6b = a[:] # ПОВЕРХНОСТНАЯ копия списка

Важно: слайс списка создаёт новый список, а слайс выходит за границы без ошибки — a[10:20] вернёт [], тогда как a[10] бросит IndexError.

В чём разница между is и ==?

== спрашивает «равны ли значения», is — «это один и тот же объект». Путаница возникает потому, что на маленьких числах и коротких строках is иногда «работает»: CPython держит готовые объекты для небольших целых и интернирует некоторые строки.

python
1a, b = 256, 256
2a is b # True — кеш малых int
3a, b = 257, 257
4a is b # False (в REPL; внутри одной функции может быть True)

Правило без исключений: is — только для None, True, False и проверки идентичности объекта. Во всех остальных случаях ==.

Как проверить, что один кортеж содержит все элементы другого?

Очевидный ответ — через множества:

python
1set(b).issubset(a)

И сразу ловушка, ради которой вопрос и задают: множества теряют количество. Для a = (1, 2) и b = (1, 1, 2) ответ будет True, хотя двух единиц в a нет. Если количество важно — считайте:

python
1from collections import Counter
2need, have = Counter(b), Counter(a)
3all(have[k] >= v for k, v in need.items())

Ссылки, изменяемость, копирование

Аргументы передаются по значению или по ссылке?

Ни то, ни другое в привычном смысле — передаётся ссылка на объект, по значению. Внутри функции имя указывает на тот же объект; присваивание нового значения имени связь разрывает, а изменение объекта на месте видно снаружи.

python
1def f(xs, y):
2 xs.append(1) # видно снаружи: изменили ТОТ ЖЕ список
3 xs = [9] # НЕ видно: переназначили локальное имя
4 y += 1 # НЕ видно: int неизменяем
5
6items, n = [], 0
7f(items, n)
8items, n # ([1], 0)

Почему нельзя писать def f(x=[])?

Значение по умолчанию вычисляется один раз — при определении функции, а не при каждом вызове. Изменяемый объект в умолчании становится общим для всех вызовов.

python
1def add(item, bucket=[]):
2 bucket.append(item)
3 return bucket
4
5add(1) # [1]
6add(2) # [1, 2] ← а ожидали [2]

Правильно — сторож None:

python
1def add(item, bucket=None):
2 if bucket is None:
3 bucket = []
4 bucket.append(item)
5 return bucket

Копия и глубокая копия

copy.copy копирует контейнер, но не то, что внутри: вложенные объекты остаются общими. copy.deepcopy рекурсивно копирует всё дерево (и корректно переживает циклические ссылки).

python
1import copy
2
3a = [[1, 2], [3]]
4b = copy.copy(a) # то же, что a[:] или list(a)
5b[0].append(99)
6a # [[1, 2, 99], [3]] — задели оригинал
7
8c = copy.deepcopy(a)
9c[0].append(0)
10a # не изменился

Deepcopy недёшев: для больших структур это заметная цена, и часто честнее пересобрать объект явно.

Зачем id() и когда он врёт

id(x) возвращает идентичность объекта (в CPython — адрес). Годится, чтобы показать, что два имени указывают на один объект, но как «постоянный номер» не годится: после смерти объекта тот же id может достаться новому.

python
1id([1]) == id([2]) # МОЖЕТ быть True: первый список умер,
2 # второй занял его место

Именно поэтому is сравнивают только с объектами, которые заведомо живы, — например, с None.

Функции

Что такое lambda и где она уместна

Анонимная функция из одного выражения. Она не делает ничего, чего не делает def; смысл — не заводить имя там, где функция живёт одну строку: ключ сортировки, аргумент map/filter, значение по умолчанию в defaultdict.

python
1users.sort(key=lambda u: (u.last_name, u.first_name))

Если внутри лямбды появился тернарник в два этажа — это уже def с именем, и читателю станет легче.

*args и **kwargs

В объявлении — «собери остальные позиционные в кортеж» и «остальные именованные в словарь». В вызове те же звёздочки работают наоборот — распаковывают.

python
1def log(level, *args, **kwargs):
2 print(level, args, kwargs)
3
4log("INFO", 1, 2, user="mike") # INFO (1, 2) {'user': 'mike'}
5
6def wrapper(*args, **kwargs):
7 return log(*args, **kwargs) # прозрачный проброс

Это же ответ на вопрос «как передать необязательные параметры из одной функции в другую»: не перечислять их руками, а пробросить **kwargs — тогда добавление параметра в нижнюю функцию не требует правки верхней.

Функции высшего порядка и замыкания

Функция — обычный объект: её можно передать, вернуть, положить в словарь. Функция, принимающая или возвращающая функцию, и называется функцией высшего порядка (sorted(key=...), map, декораторы).

Замыкание запоминает не значение переменной, а саму переменную. Отсюда классический подвох:

python
1fs = [lambda: i for i in range(3)]
2[f() for f in fs] # [2, 2, 2], а не [0, 1, 2]
3
4fs = [lambda i=i: i for i in range(3)]
5[f() for f in fs] # [0, 1, 2] — связали значение в умолчании

/ и * в сигнатуре

Это способ запретить неудобный способ вызова. Всё до / можно передать только позиционно, всё после * — только по имени.

python
1def connect(host, port, /, *, timeout=5.0, retries=3):
2 ...
3
4connect("db", 5432, timeout=1) # ок
5connect(host="db", port=5432) # TypeError

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

Что такое pass и что такое docstring

pass — заглушка: синтаксис требует тела, а делать пока нечего. Ничего не выполняет.

Docstring — строковый литерал первой строкой модуля, функции или класса; в отличие от комментария, он остаётся в объекте и доступен через __doc__, help() и подсказки редактора.

python
1def area(r):
2 """Площадь круга радиуса r."""
3
4area.__doc__ # 'Площадь круга радиуса r.'
5help(area)
6dir(area) # чем ещё обладает объект

ООП

Зачем нужен self и почему он явный

Метод — это обычная функция в теле класса, а self — её первый обычный параметр, куда при вызове через экземпляр подставляется сам экземпляр. Это не ключевое слово — имя можно заменить, хотя так никто не делает.

python
1class Counter:
2 def bump(self):
3 self.n += 1
4
5c = Counter(); c.n = 0
6c.bump() # то же самое, что:
7Counter.bump(c) # видно, откуда берётся self

Ответ «чтобы обратиться к полям экземпляра» верен, но мелок. Точнее: в Python нет неявного контекста вроде this, и связывание видно в сигнатуре.

__init__ и __new__

__new__ создаёт объект и возвращает его; __init__ только настраивает уже созданный и ничего не возвращает. Поэтому для неизменяемых типов и для «один экземпляр на всех» работать надо в __new__.

python
1class Config:
2 _instance = None
3
4 def __new__(cls, *a, **kw):
5 if cls._instance is None:
6 cls._instance = super().__new__(cls)
7 return cls._instance
8
9Config() is Config() # True

Засада такого синглтона: __init__ вызовется каждый раз и перезатрёт поля. В большинстве задач честнее модульная переменная или functools.lru_cache на фабрике.

@classmethod, @staticmethod, @property

classmethod получает класс (cls) — главное применение это альтернативные конструкторы, которые правильно работают в наследниках. staticmethod не получает ничего — это просто функция, живущая рядом. property превращает вызов в доступ по атрибуту.

python
1class Temp:
2 def __init__(self, c): self._c = c
3
4 @classmethod
5 def from_f(cls, f): # в наследнике вернёт НАСЛЕДНИКА
6 return cls((f - 32) / 1.8)
7
8 @staticmethod
9 def valid(c): return c >= -273.15
10
11 @property
12 def f(self): return self._c * 1.8 + 32
13
14Temp.from_f(212).f # 212.0

property — это и есть питоновский ответ на вопрос про аксессоры и мутаторы: геттеры пишут не заранее, а тогда, когда за атрибутом появилась логика — внешний код при этом не меняется.

Множественное наследование и MRO

Множественное наследование есть. Порядок поиска метода (MRO) строится линеаризацией C3, а не «слева направо вглубь», и его можно просто посмотреть.

python
1class A: pass
2class B(A): pass
3class C(A): pass
4class D(B, C): pass
5
6[k.__name__ for k in D.__mro__] # ['D', 'B', 'C', 'A', 'object']

super() идёт не «к родителю», а К СЛЕДУЮЩЕМУ КЛАССУ В MRO текущего экземпляра — именно поэтому в D вызов super() из B попадёт в C, а не в A. Несовместимый порядок основ — ошибка уже на создании класса (TypeError: Cannot create a consistent MRO).

Что хранится в __dict__ и зачем __slots__

Обычный экземпляр держит атрибуты в словаре __dict__. Это удобно (атрибут можно добавить на ходу), но стоит памяти. __slots__ заменяет словарь фиксированным набором слотов.

python
1class P:
2 __slots__ = ("x", "y")
3 def __init__(self, x, y): self.x, self.y = x, y
4
5p = P(1, 2)
6p.z = 3 # AttributeError: 'P' object has no attribute 'z'
7p.__dict__ # AttributeError — словаря нет вовсе

Берут ради памяти, когда объектов миллионы. Цена: нет динамических атрибутов, сложнее множественное наследование, и если хоть у одного класса в иерархии слотов нет, словарь вернётся и выигрыш испарится.

Магические (dunder) методы

Методы вида __len__, __eq__, __iter__, __enter__ — это точки, через которые ваш объект встраивается в язык. Питон не спрашивает тип — он спрашивает протокол.

python
1class Bag:
2 def __init__(self, items): self._items = list(items)
3 def __len__(self): return len(self._items)
4 def __iter__(self): return iter(self._items)
5 def __repr__(self): return f"Bag({self._items!r})"
6
7b = Bag([1, 2])
8len(b), list(b), bool(b) # (2, [1, 2], True)

Полезная деталь для ответа: bool(b) без __bool__ падает на __len__ — пустой контейнер ложен.

Дескрипторы — и чем они не декораторы

Дескриптор — объект с __get__/__set__, положенный в КЛАСС: он перехватывает доступ к атрибуту. Декоратор же просто оборачивает функцию в момент определения. Родство в том, что property, classmethod и обычные методы сами реализованы как дескрипторы.

python
1class Positive:
2 def __set_name__(self, owner, name): self.name = "_" + name
3 def __get__(self, obj, cls): return getattr(obj, self.name)
4 def __set__(self, obj, value):
5 if value <= 0:
6 raise ValueError("должно быть > 0")
7 setattr(obj, self.name, value)
8
9class Order:
10 qty = Positive()
11
12Order().qty = -1 # ValueError

Зачем: правило проверки описано ОДИН раз и переиспользуется, а не копируется в каждый сеттер.

Как создать класс без слова class и что такое метакласс

Класс — тоже объект, и его можно создать вызовом type(имя, основы, словарь). Именно это делает инструкция class под капотом.

python
1User = type("User", (), {"greet": lambda self: "hi"})
2User().greet() # 'hi'

Метакласс — класс класса: он управляет тем, как класс создаётся. Годится там, где правило надо применить ко всем подклассам сразу: регистрация плагинов, ORM, проверка договора на этапе объявления.

Честный ответ на вопрос «когда применять»: почти никогда. Сегодня большую часть задач закрывают __init_subclass__ и __set_name__ — и это хороший ответ собеседнику.

Monkey patching

Подмена атрибута или метода уже загруженного класса/модуля во время работы программы. Возможно потому, что класс — обычный объект с изменяемым словарём.

python
1import datetime
2
3class FakeDate(datetime.date):
4 @classmethod
5 def today(cls): return cls(2026, 1, 1)
6
7datetime.date = FakeDate # с этой секунды весь процесс видит другой date

Законное применение одно — тесты (unittest.mock.patch делает ровно это и возвращает как было). В боевом коде это правка на расстоянии: читая модуль, невозможно узнать, что его подменили.

NotImplementedError и NotImplemented — разные вещи

NotImplementedError — исключение: его бросают в заготовке метода, который обязан переопределить наследник.

NotImplemented — значение-сигнал, которое возвращают из __eq__, __add__ и прочих, чтобы сказать «я не умею с этим типом, попробуй второй операнд».

python
1class Money:
2 def __init__(self, amount, cur): self.amount, self.cur = amount, cur
3
4 def __eq__(self, other):
5 if not isinstance(other, Money):
6 return NotImplemented # а не False!
7 return (self.amount, self.cur) == (other.amount, other.cur)

Разница не косметическая: вернёте False — и Money(1, "RUB") == чужой_объект никогда не даст второму типу шанса ответить.

Декораторы

Что такое декоратор и как написать свой

Декоратор — функция, которая принимает функцию и возвращает другую. Синтаксис @deco над def f — ровно f = deco(f), ничего больше.

python
1import functools, time
2
3def timed(fn):
4 @functools.wraps(fn) # сохраняет __name__ и __doc__
5 def inner(*args, **kwargs):
6 t = time.perf_counter()
7 try:
8 return fn(*args, **kwargs)
9 finally:
10 print(f"{fn.__name__}: {time.perf_counter() - t:.3f}с")
11 return inner
12
13@timed
14def work(): ...

Забытый functools.wraps — частый вопрос на собеседовании: без него функция «переименовывается» в inner, ломая логи и help().

Декоратор с параметрами: повторить N раз

Популярное задание на собеседовании. Суть: декоратор с аргументами — это ещё один уровень вложенности.

python
1import functools, time
2
3def retry(times=3, delay=0.5, catch=Exception):
4 def deco(fn):
5 @functools.wraps(fn)
6 def inner(*args, **kwargs):
7 for attempt in range(1, times + 1):
8 try:
9 return fn(*args, **kwargs)
10 except catch:
11 if attempt == times:
12 raise # последнюю ошибку не глотаем
13 time.sleep(delay * attempt)
14 return inner
15 return deco
16
17@retry(times=5, catch=TimeoutError)
18def fetch(url): ...

Две вещи, которые смотрят отдельно: последняя попытка должна пробрасывать исключение, а не возвращать None, и ловить Exception целиком по умолчанию — спорно.

map, filter, reduce и однострочники

map и filter возвращают ленивые итераторы, reduce живёт в functools и сворачивает последовательность в одно значение.

python
1from functools import reduce
2
3nums = [1, 2, 3, 4, 5, 6]
4list(filter(lambda n: n % 2 == 0, nums)) # [2, 4, 6]
5reduce(lambda a, b: a * b, nums) # 720

Счёт заглавных букв в файле одной строкой — и сразу важная оговорка про память:

python
1with open("text.txt", encoding="utf-8") as f:
2 sum(1 for line in f for ch in line if ch.isupper())

Здесь генераторное выражение, а не f.read(): файл читается построчно и не заезжает в память целиком. На собеседовании ценят именно эту оговорку, а не сам однострочник.

Итераторы и генераторы

Что такое итератор и чем iter отличается от next

Итерируемый объект умеет отдать итератор (__iter__). Итератор умеет отдать следующий элемент (__next__) и бросить StopIteration, когда кончились. iter(x) и next(it) — встроенные функции, дёргающие эти методы.

python
1xs = [1, 2]
2it = iter(xs)
3next(it), next(it) # 1, 2
4next(it) # StopIteration
5next(it, "всё") # 'всё' — умолчание вместо исключения

Список — итерируемый, но НЕ итератор: по нему можно пройти сколько угодно раз. Итератор одноразовый и исчерпывается.

Генератор и yield

Генератор — самый дешёвый способ написать итератор: функция с yield при вызове не выполняется вовсе, а возвращает объект, который выполняет тело порциями и помнит своё место между вызовами.

python
1def countdown(n):
2 print("старт") # НЕ печатается на countdown(3)
3 while n > 0:
4 yield n
5 n -= 1
6
7g = countdown(3) # ничего не выполнилось
8next(g) # «старт», затем 3

Выгода не в красоте, а в памяти: генератор держит один элемент, а не весь результат. Обработка файла на гигабайт через список и через генератор — разница между «не влезло» и «работает».

Что делает return внутри генератора

Любимый вопрос «что напечатается». return в генераторе не возвращает значение вызывающему в привычном смысле — он останавливает итерацию, а значение кладёт в StopIteration.value.

python
1def g():
2 yield 1
3 return "готово"
4 yield 2 # недостижим
5
6list(g()) # [1] — «готово» потеряно
7
8it = g()
9next(it) # 1
10try:
11 next(it)
12except StopIteration as e:
13 e.value # 'готово'

Именно это значение получает yield from — на нём держалась вся до-async корутинная машинерия.

Можно ли «перевернуть» генератор?

Лениво — нет, и это суть ответа. reversed() требует либо __reversed__, либо пары __len__ + __getitem__; у генератора нет ни длины, ни индексов — он вообще может быть бесконечным.

python
1reversed(x for x in range(3))
2# TypeError: argument to reversed() must be a sequence
3
4list(reversed(list(g))) # работает — ценой полной материализации

То есть «перевернуть» можно, но только отказавшись от того единственного, ради чего брали генератор.

collections и itertools, которые стоит назвать

Вопрос проверяет не память, а то, решали ли вы ими задачи.

python
1from collections import Counter, defaultdict, deque, namedtuple
2from itertools import groupby, islice, chain, product
3
4Counter("абракадабра").most_common(2) # [('а', 5), ('б', 2)]
5deque(maxlen=100) # кольцевой буфер «последние 100»
6list(islice(infinite_stream, 10)) # взять 10 и не зависнуть

Главная ловушка здесь — itertools.groupby: он группирует только ПОДРЯД идущие элементы, поэтому без предварительной сортировки по тому же ключу результат окажется не тем, которого ждут от SQL-подобного GROUP BY.

Исключения и контекстные менеджеры

Когда выполняется else в try

Ветка else срабатывает, если исключения не было. Смысл — держать в try только опасную строку, а не весь последующий код: иначе except незаметно поймает ошибку не из того места.

python
1try:
2 row = fetch(id)
3except TimeoutError:
4 log("сеть")
5else:
6 render(row) # сюда TimeoutError из render() УЖЕ не перехватится
7finally:
8 close() # выполнится всегда

Порядок такой: tryexcept (если была ошибка) или else (если не было) → finally в любом случае.

Что такое контекстный менеджер

Объект с __enter__ и __exit__: язык гарантирует, что второй вызовется даже при исключении. То есть это способ не забыть освободить ресурс.

python
1from contextlib import contextmanager
2
3@contextmanager
4def timed(label):
5 import time
6 t = time.perf_counter()
7 try:
8 yield # здесь выполняется тело with
9 finally:
10 print(label, time.perf_counter() - t)
11
12with timed("запрос"):
13 fetch()

Деталь для собеседования: если __exit__ вернёт истинное значение, исключение будет подавлено. Это редко то, чего хотят, и частый источник тихих багов.

Почему голый except: — плохо

Он ловит всё, включая KeyboardInterrupt и SystemExit, то есть мешает остановить программу. Разумная граница — except Exception, а лучше конкретный тип.

python
1try:
2 parse(raw)
3except ValueError as e:
4 raise ParseError(f"строка {n}") from e # сохраняет причину в трейсбеке

Отдельно стоит знать про raise ... from e: без него исходная ошибка превращается в безымянное «During handling of the above exception», и причина теряется при разборе инцидента.

Память и GIL

Как устроена память и сборщик мусора

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

python
1import gc, sys
2
3a = []
4a.append(a) # цикл: сам на себя
5del a # счётчик НЕ ноль → память держится
6gc.collect() # вот теперь освободилась

Сборщик циклов — поколенческий (три поколения): молодые объекты проверяются часто, выжившие — реже, потому что большинство объектов умирают молодыми.

Почему память не возвращается ОС

Потому что между объектами и malloc есть собственный аллокатор (pymalloc) с аренами и пулами. Освобождённый объект возвращается в пул для повторного использования, и арена уйдёт ОС только когда освободится целиком — а один живой объект держит всю арену.

Практическое следствие, которое видно в мониторинге: RSS процесса после пиковой нагрузки часто так и остаётся высоким. Это не утечка; утечка — когда растёт ПОСТОЯННО, от пика к пику.

И отдельно: интернированные строки и кеш малых целых живут до конца работы интерпретатора по замыслу.

Что такое GIL и почему он всё ещё есть

GIL — один замок на интерпретатор: в каждый момент байткод исполняет только один поток. Он защищает счётчики ссылок от гонок — и делает однопоточный код быстрее, чем сделала бы тонкая блокировка каждого объекта.

Что из этого следует на практике:

  • задачи, упирающиеся в процессор (CPU-bound), потоками НЕ ускоряются — нужны процессы;
  • задачи, ждущие ввода-вывода, потоками ускоряются отлично: на время ожидания GIL отпускается;
  • расширения на C (NumPy и прочие) отпускают GIL на тяжёлых участках.

Актуальное дополнение, которое стоит назвать: в CPython 3.13 появилась сборка без GIL (free-threading, PEP 703) — пока отдельная и не везде выгодная, но направление задано.

Потоки, процессы, async — что когда

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

Что делаемЧем
ждём сеть/диск, сотни ожиданийasyncio
ждём ввод-вывод, но библиотека синхроннаяThreadPoolExecutor
считаем числа своим кодомProcessPoolExecutor
считаем числа чужой C-библиотекойпотоки тоже годятся

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

Утечка памяти — с чего начать отладку

Ответ «посмотреть в профайлере» слабый. Сильный — назвать порядок.

python
1import tracemalloc
2
3tracemalloc.start()
4snap1 = tracemalloc.take_snapshot()
5... # нагрузка
6snap2 = tracemalloc.take_snapshot()
7for stat in snap2.compare_to(snap1, "lineno")[:10]:
8 print(stat) # строки, где памяти стало больше

Дальше по убыванию частоты причин: глобальный список/кеш без границы, замыкание или обратный вызов, держащий большой объект, цикл с __del__, расширение на C. gc.get_objects() и objgraph помогают увидеть, КТО держит ссылку.

Производительность

Как ускорить код на Python

Правильный ответ начинается не с приёма, а с замера: без профиля ускоряют не то место.

python
1python -m cProfile -s cumtime app.py | head -20
2python -m timeit -s "from app import f" "f()"

Порядок действий по убыванию выгоды: сменить алгоритм и структуру данных (проверка вхождения в set, а не в list) → убрать работу из цикла → взять готовое на C (NumPy, re, стандартная библиотека) → кеш (functools.lru_cache) → и только потом PyPy, Cython или расширение.

Классический пример алгоритмической разницы:

python
1if x in big_list: # O(n)
2if x in big_set: # O(1) в среднем

Чем массив NumPy лучше списка

Список хранит указатели на разбросанные по памяти объекты, у каждого свой заголовок. Массив NumPy — один непрерывный блок чисел одного типа. Отсюда и память меньше, и операция идёт одним вызовом в C вместо миллиона шагов интерпретатора.

python
1import numpy as np
2
3a = np.arange(1_000_000)
4(a * 2).sum() # цикл внутри C, без байткода на каждый элемент

Оговорка: на маленьких данных NumPy проигрывает — накладные расходы на вызов больше самой работы. Выгода появляется на больших однородных массивах.

Почему в Python нет оптимизации хвостовой рекурсии

Это решение, а не недоработка. Гвидо ван Россум возражал против неё главным образом потому, что сворачивание кадров ломает трейсбек: стек вызовов — главный инструмент отладки, и менять его на экономию кадров не стоит.

Практическое следствие: глубокая рекурсия в Python — путь к RecursionError (по умолчанию предел около 1000 кадров). Рекурсию переписывают циклом или явным стеком.

python
1import sys
2sys.setrecursionlimit(10_000) # не лечение: при реальном переполнении
3 # стека процесс просто упадёт

Модули, пакеты, области видимости

Чем пакет отличается от модуля

Модуль — один файл .py. Пакет — каталог с модулями; файл __init__.py делает его обычным пакетом и исполняется при импорте. С PEP 420 каталог без __init__.py тоже импортируется — как namespace-пакет.

Собрать свой пакет сегодня — это pyproject.toml и одна команда:

toml
1[project]
2name = "mylib"
3version = "0.1.0"
4
5[build-system]
6requires = ["hatchling"]
7build-backend = "hatchling.build"
bash
1python -m build # соберёт wheel и sdist в dist/

Упоминание setup.py в ответе сегодня звучит как старый опыт: он работает, но декларативный pyproject.toml — текущая форма.

wheel и egg, менеджеры пакетов

wheel (.whl) — современный формат сборки: архив, который ставится распаковкой, без выполнения кода сборки на машине пользователя. egg — устаревший формат setuptools, в новом коде его не используют.

Про бинарные зависимости: именно для них есть теги совместимости вроде manylinux — колёсо собирают под конкретную платформу и версию Python, и pip выбирает подходящее.

Менеджеры: pip с venv — база; poetry и uv держат lock-файл, то есть воспроизводимую среду; conda решает ещё и не-Python зависимости.

Транзитивные зависимости — те, что пришли с чужими пакетами. Управляют ими не верой в requirements.txt, а lock-файлом и проверкой уязвимостей в CI.

Правило LEGB, global и nonlocal

Имя ищется в четырёх областях по порядку: Local → Enclosing → Global → Built-in. globals() и locals() показывают соответствующие словари.

python
1n = 0
2
3def outer():
4 k = 0
5 def inner():
6 global n # пишем в модульную
7 nonlocal k # пишем в объемлющую
8 n += 1; k += 1
9 inner()
10 return k

Без global/nonlocal присваивание внутри функции делает имя ЛОКАЛЬНЫМ во всём теле — отсюда знаменитое UnboundLocalError на чтении до присваивания.

Глобальные переменные как способ общения между частями программы — плохая идея: невозможно сказать, кто их меняет, и тесты начинают влиять друг на друга.

«Статическая переменная» внутри функции

Отдельного static, как в C, нет — но состояние между вызовами держать можно несколькими способами, и выбор говорит о вкусе.

python
1# 1. атрибут функции — ближе всего к вопросу
2def hits():
3 hits.n = getattr(hits, "n", 0) + 1
4 return hits.n
5
6# 2. замыкание — если состояние не должно торчать наружу
7def counter():
8 n = 0
9 def bump():
10 nonlocal n
11 n += 1
12 return n
13 return bump
14
15# 3. lru_cache — если речь на самом деле про кеш

Перезагрузка модуля, __pycache__ и компиляция

Импорт выполняет модуль ОДИН раз и кладёт в sys.modules; повторный import берёт готовое. Перезагрузить можно так:

python
1import importlib, mymod
2importlib.reload(mymod)

Но старые объекты, созданные до перезагрузки, продолжают ссылаться на прежние классы, и from mymod import f тоже держит старую функцию. Поэтому reload — инструмент REPL, а не способ обновлять боевой процесс.

Про компиляцию: исходник компилируется в байткод (.pyc в __pycache__), а байткод исполняет виртуальная машина. Кеш связан с версией интерпретатора и обновляется сам; удаление __pycache__ безопасно. Посмотреть байткод можно так:

python
1import dis
2dis.dis(lambda x: x + 1)

Виртуальное окружение и переменные среды

Виртуальное окружение — отдельный каталог со своим site-packages, чтобы зависимости проектов не смешивались.

bash
1python -m venv .venv
2source .venv/bin/activate # Windows: .venv\Scripts\activate

Переменные среды, про которые спрашивают:

  • PYTHONPATH — добавляет каталоги в sys.path;
  • PYTHONDONTWRITEBYTECODE — не писать .pyc;
  • PYTHONHASHSEED — фиксирует соль хеша строк (воспроизводимость в тестах);
  • PYTHONOPTIMIZE (или флаг -O) — выключает assert и __debug__.

Последнее важнее, чем кажется: если проверка прав или валидация написаны через assert, под -O они исчезнут целиком. Файлы .pth в site-packages — ещё один способ добавить пути в sys.path (им пользуются установки в режиме editable).

Асинхронность

Синхронный и асинхронный код

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

python
1import asyncio
2
3async def fetch(n):
4 await asyncio.sleep(1) # отдали управление
5 return n
6
7async def main():
8 return await asyncio.gather(*(fetch(i) for i in range(100)))
9
10asyncio.run(main()) # около секунды, а не ста секунд

Главное правило и главная ошибка новичка: один синхронный time.sleep или тяжёлый расчёт внутри корутины блокирует ВЕСЬ цикл. Такое выносят в asyncio.to_thread или в пул процессов.

Инструменты и окружение

Проверка кода: синтаксис, статический анализ, отладчик

Синтаксис без запуска:

bash
1python -m py_compile app.py # только разбор, код не исполняется

Статический анализ сегодня — это ruff (линтер и форматтер сразу), mypy или pyright (типы), bandit (безопасность). Назвать только pylint сейчас выглядит устаревшим ответом.

Отладчик встроен: breakpoint() в любой строке остановит выполнение и даст pdb.

python
1breakpoint() # n — шаг, s — внутрь, c — продолжить, p — напечатать

Скрипт делают запускаемым не «чем-то одним», а по-разному на разных ОС: shebang и chmod +x в Unix, py-лончер в Windows, а для раздачи — точка входа в pyproject.toml ([project.scripts]) или PyInstaller.

CPython, PyPy, Cython, IronPython — зачем разные

CPython — эталонная реализация на C, та самая, что скачивается с python.org. PyPy — другая реализация с JIT: долго живущий вычислительный код может ускориться в разы, но часть C-расширений работает хуже.

Cython — не реализация, а компилятор в C: на нём пишут горячие участки и связки с C-библиотеками. IronPython и Jython — для .NET и JVM, сегодня экзотика.

Вызвать C из Python можно и без компилятора:

python
1import ctypes
2libc = ctypes.CDLL("libc.so.6")
3libc.printf(b"%d\n", 42)

А современный способ связать быстрый язык с Python — cffi или PyO3 (Rust).

Регулярные выражения и проверка email

Регулярки в re, компиляция кешируется, но в горячем коде шаблон лучше собрать заранее через re.compile.

Задание «напишите регексп для email» — ловушка на самоуверенность. Полный разбор по RFC 5322 на регулярках чудовищен; сильный ответ — назвать границу применимости.

python
1import re
2
3ROUGH = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
4bool(ROUGH.match("a.b+tag@example.co.uk")) # True

Смысл такой проверки — отсечь опечатки в форме, а не доказать существование адреса. Единственная настоящая проверка — отправленное письмо с подтверждением.

Чем фреймворк отличается от библиотеки и когда Python — правильный выбор

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

Рядом часто спрашивают про NumPy и SciPy: NumPy — массив и базовая арифметика, SciPy — надстройка с численными методами (оптимизация, статистика, сигналы) поверх него.

А на вопрос «когда Python — правильный выбор» честный ответ звучит так: когда узкое место — время разработчика, ввод-вывод или готовая экосистема (данные, ML, скрипты, веб), а не микросекунды на ядро или жёсткий реалтайм.

Настройка среды с нуля на Windows: редактор, расширения, терминал, Python, git, Docker, uv. К концу — папка проекта, готовая принять первую строку кода.

updated Aug 4, 2026

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

updated Aug 3, 2026

Разбираем каждую папку по отдельности: за что отвечает, что в неё класть нельзя и почему слои зависят друг от друга только в одну сторону. Создаём дерево до появления кода.

updated Aug 3, 2026

От пустой папки до проекта, который собирается одинаково на любой машине: uv init, закреплённая версия Python, lock-файл, первые зависимости и свои команды вида uv run lint.

updated Aug 3, 2026