Python Gotchas и Tricky Questions¶
~17 минут чтения
Предварительно: Подготовка к Python-интервью | Особенности языка
По данным опросов на Stack Overflow и HackerRank, mutable default arguments -- gotcha #1, с которой сталкиваются 73% Python-разработчиков в первый год. На собеседованиях вопросы категории "что выведет код" встречаются в 80%+ Python-интервью (FAANG, финтех, Яндекс). Здесь собраны 19 категорий ловушек -- от классических до продвинутых -- с кодом, объяснениями и решениями.
Содержание¶
- Mutable Default Arguments
- Late Binding Closures
- Integer Caching и String Interning
- is vs == (Identity vs Equality)
- Shallow vs Deep Copy
- LEGB Rule и Variable Scope
- Truthiness и Falsy Values
- Circular Imports
- Exception Handling: finally и return
- Memory Management и GC
- Global Interpreter Lock (GIL)
- List Multiplication Gotcha
- Dictionary Ordering
- init vs new и Metaclasses
- Generator и StopIteration
- Walrus Operator (:=)
- Decorators и functools.wraps
- Class vs Instance Variables
- Дополнительные Tricky Questions
1. Mutable Default Arguments¶
Самая классическая Python-ловушка
Default arguments создаются один раз при определении функции, а не при каждом вызове.
Код¶
def append_to_list(item, lst=[]):
lst.append(item)
return lst
print(append_to_list(1)) # [1]
print(append_to_list(2)) # [1, 2] ❌ Ожидали [2]
print(append_to_list(3)) # [1, 2, 3] ❌ Ожидали [3]
Почему так происходит?¶
- Default parameters - это "member data" объекта функции
- Они создаются при
def, а не при вызове - Один и тот же список переиспользуется между вызовами
Решение¶
Почему спрашивают?¶
- Проверяет понимание модели выполнения Python
- Выявляет опыт отладки реальных багов
- Тестирует знание lifecycle функций
Связанные gotchas¶
# List replication создает ссылки, не копии!
lst = [[0]*3]*3 # [[0,0,0], [0,0,0], [0,0,0]]
lst[0][0] = 1
print(lst) # [[1,0,0], [1,0,0], [1,0,0]] ❌
Источники: - Common Gotchas — The Hitchhiker's Guide to Python - Python Gotchas and Tricky Questions | Medium - Python Mutable Default Arguments Behavior
2. Late Binding Closures¶
Проблема¶
Closures в Python используют late binding - значения переменных извлекаются в момент вызова функции, а не при её создании.
Код¶
# Пример 1: Классическая ошибка
funcs = [lambda: i for i in range(5)]
print([f() for f in funcs]) # [4, 4, 4, 4, 4] ❌ Ожидали [0, 1, 2, 3, 4]
# Пример 2: То же самое с def
def create_multipliers():
return [lambda x: i * x for i in range(5)]
multipliers = create_multipliers()
print([m(2) for m in multipliers]) # [8, 8, 8, 8, 8] ❌
Почему так происходит?¶
- Closure не захватывает значение переменной, а хранит ссылку
- К моменту вызова функции цикл уже завершен
- Переменная
iимеет финальное значение (4)
Решения¶
1. Default parameter (рекомендуется):
2. Closure factory:
3. IIFE (Immediately Invoked Function Expression):
4. functools.partial:
Почему спрашивают?¶
- Проверяет понимание scoping в Python
- Выявляет знание closure mechanism
- Тестирует опыт с функциональным программированием
Важно: Это НЕ проблема lambda! Обычные def функции ведут себя точно так же.
Источники: - Late Binding Closures — Omniverse - Understanding Late Binding in Python Closures - Common Pitfall: Late Binding in Closures
3. Integer Caching и String Interning¶
Integer Caching¶
CPython кэширует числа от -5 до 256 включительно.
Код¶
a = 256
b = 256
print(a is b) # True ✅
a = 257
b = 257
print(a is b) # False (обычно) ❌ Может быть True в REPL!
# В одной строке работает из-за оптимизации компилятора:
print(257 is 257) # True
Почему так происходит?¶
- Python предварительно создает часто используемые числа при старте
- Это singleton objects в памяти
- Экономия памяти и ускорение работы
- За пределами диапазона создаются новые объекты
String Interning¶
Python автоматически интернирует строки, похожие на идентификаторы.
Код¶
a = "hello"
b = "hello"
print(a is b) # True (обычно)
a = "hello world"
b = "hello world"
print(a is b) # False (может быть True в интерактивной сессии)
# Принудительный interning
import sys
a = sys.intern("hello world!")
b = sys.intern("hello world!")
print(a is b) # True ✅
Правила interning:¶
- Строки, соответствующие правилам идентификаторов → интернируются
- String literals → обычно интернируются
- Динамически созданные строки → обычно НЕ интернируются
- Использовать
sys.intern()для гарантированного interning
Почему спрашивают?¶
- Проверяет понимание разницы
isvs== - Выявляет знание CPython internals
- Тестирует awareness о performance optimizations
⚠️ Важно: НИКОГДА не полагайтесь на эти правила в production коде! Используйте == для сравнения значений.
Источники: - 3 Facts of the Integer Caching in Python - Understanding Integer and String Interning in Python - Interning in CPython
4. is vs == (Identity vs Equality)¶
Ключевые различия¶
| Оператор | Что проверяет | Использование |
|---|---|---|
== |
Value equality (__eq__) |
Сравнение данных/контента |
is |
Object identity (id(a) == id(b)) |
Сравнение с None или singletons |
Код¶
# Пример 1: Списки
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True (одинаковые значения)
print(a is b) # False (разные объекты)
# Пример 2: Integer caching
a = 256
b = 256
print(a is b) # True (cached)
a = 257
b = 257
print(a is b) # False (новые объекты)
# Пример 3: None (правильное использование is)
x = None
if x is None: # ✅ Идиоматично
print("x is None")
if x == None: # ❌ Работает, но не питоничо
print("x equals None")
Best Practice¶
- Используйте
==в 99% случаев - Используйте
isтолько для: - Сравнения с
None:if x is None: - Проверки singletons:
if x is True: - Явной проверки identity
Почему спрашивают?¶
- Проверяет понимание Python object model
- Выявляет знание идиоматичного Python
- Тестирует awareness о integer caching gotcha
Источники: - Comparing Python Objects the Right Way: "is" vs "==" – Real Python - Python != Is Not is not: Comparing Objects in Python - Equality versus identity in Python
5. Shallow vs Deep Copy¶
Проблема¶
Shallow copy копирует только верхний уровень, сохраняя ссылки на вложенные объекты.
Код¶
import copy
# Пример 1: Shallow copy проблема
original = [[1, 2], [3, 4]]
shallow = copy.copy(original)
# Или: shallow = original[:]
# Или: shallow = list(original)
shallow[0][0] = 999
print(original) # [[999, 2], [3, 4]] ❌ Оригинал изменился!
print(shallow) # [[999, 2], [3, 4]]
# Пример 2: Deep copy решение
original = [[1, 2], [3, 4]]
deep = copy.deepcopy(original)
deep[0][0] = 999
print(original) # [[1, 2], [3, 4]] ✅ Оригинал не изменился
print(deep) # [[999, 2], [3, 4]]
# Пример 3: Assignment vs Copy
a = [1, 2, 3]
b = a # Просто ссылка
c = a[:] # Shallow copy
d = copy.deepcopy(a) # Deep copy (избыточно для плоских списков)
b.append(4)
print(a) # [1, 2, 3, 4] (b - это та же ссылка)
print(c) # [1, 2, 3] (c - отдельная копия)
Ключевые различия¶
| Метод | Что копирует | Use case |
|---|---|---|
= |
Только ссылку | Нет копирования |
| Shallow copy | Верхний уровень | Плоские структуры |
| Deep copy | Всё рекурсивно | Nested структуры |
Способы создания shallow copy:¶
shallow1 = original[:]
shallow2 = list(original)
shallow3 = original.copy()
shallow4 = copy.copy(original)
shallow5 = [x for x in original]
Gotchas¶
1. Recursive objects:
# Deep copy может создать recursive loop
x = []
x.append(x) # Циклическая ссылка
y = copy.deepcopy(x) # ✅ Работает, Python обрабатывает
2. Performance:
# Deep copy медленный и "жадный" по памяти
# Используйте только когда действительно нужна независимость
3. Некоторые типы не копируются: - Modules, methods, stack traces, files, sockets, windows - Они просто возвращаются unchanged
Почему спрашивают?¶
- Проверяет понимание Python memory model
- Выявляет опыт с mutable/immutable types
- Тестирует знание reference semantics
Источники: - Deep Copy and Shallow Copy in Python - Python Shallow Copy and Deep Copy (With Examples) - How to Copy Objects in Python: Shallow vs Deep Copy Explained – Real Python
6. LEGB Rule и Variable Scope¶
LEGB Rule¶
Python ищет переменные в порядке: Local → Enclosing → Global → Built-in
Код¶
# Пример 1: LEGB в действии
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"
# Пример 2: Shadowing built-ins (плохая практика!)
print = "Don't do this!"
print("Hello") # TypeError: 'str' object is not callable ❌
# Пример 3: UnboundLocalError
x = 10
def foo():
print(x) # UnboundLocalError!
x = 20 # Python видит assignment ниже
# Исправление:
def foo():
global x
print(x) # 10
x = 20
# Пример 4: nonlocal для enclosing scope
def outer():
x = 10
def inner():
nonlocal x
x = 20
inner()
print(x) # 20
Common Errors¶
1. "name is not defined"
2. "local variable referenced before assignment"
Best Practices¶
- Используйте параметры и return values вместо global
- Минимизируйте module-level state
- Избегайте shadowing built-ins
- Документируйте closures, которые захватывают outer names
Debugging Tools¶
# Инспектирование namespaces
print(globals()) # Все глобальные переменные
print(locals()) # Все локальные переменные
Почему спрашивают?¶
- Проверяет понимание scoping rules
- Выявляет опыт с closures и nested functions
- Тестирует знание
globalиnonlocalkeywords
Источники: - Python Scope and the LEGB Rule – Real Python - Scope Resolution in Python | LEGB Rule - Python Variable Scope And The LEGB Rule Explained
7. Truthiness и Falsy Values¶
Falsy Values в Python¶
Все эти значения считаются False в boolean context:
# Numeric zeros
False, 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
# Empty sequences/collections
'', (), [], {}, set(), range(0)
# None
None
Gotchas¶
1. Zero Age Problem
def validate_age(age):
if not age: # ❌ 0 тоже будет False!
return "Age required"
return f"Age: {age}"
print(validate_age(0)) # "Age required" - WRONG!
# Правильно:
def validate_age(age):
if age is None: # ✅ Явная проверка None
return "Age required"
return f"Age: {age}"
2. XML ElementTree Gotcha
import xml.etree.ElementTree as ET
dom = ET.fromstring("<root><child/></root>")
element = dom.find("child")
# ❌ НЕПРАВИЛЬНО:
if not element:
print("Element not found")
# Пустой элемент существует, но bool(element) == False!
# ✅ ПРАВИЛЬНО:
if element is None:
print("Element not found")
3. Empty Container vs None
def process(items=[]):
if not items: # Опасно! Не различает None и []
items = []
# Правильно:
def process(items=None):
if items is None:
items = []
4. Short-circuit с or
# or возвращает первый truthy или последний falsy
result = False or 0 or [] or "hello"
print(result) # "hello"
result = False or 0 or []
print(result) # [] (последний falsy)
# Практическое использование:
name = user_input or "Anonymous" # Fallback value
Custom Truthiness¶
class MyClass:
def __init__(self, value):
self.value = value
def __bool__(self):
return self.value > 0
# Python 2 compatibility:
__nonzero__ = __bool__
obj1 = MyClass(5)
obj2 = MyClass(-3)
print(bool(obj1)) # True
print(bool(obj2)) # False
Best Practice¶
Будьте explicit, когда работаете с None:
Почему спрашивают?¶
- Проверяет понимание boolean context
- Выявляет опыт с edge cases
- Тестирует знание идиоматичного Python
Источники: - Truthy and Falsy Gotchas • Inspired Python - Truthy and Falsy Values in Python - The Hidden Traps of Python Truthy Values
8. Circular Imports¶
Проблема¶
Циклические зависимости между модулями приводят к ImportError или AttributeError.
Код¶
module_a.py:
module_b.py:
Ошибка:
Почему происходит?¶
- Python начинает загружать
module_a - Встречает
from module_b import func_b - Начинает загружать
module_b - Встречает
from module_a import func_a module_aеще не загружен полностью → ошибка
Решения¶
1. Import внутри функции (deferred import)
2. Использовать typing.TYPE_CHECKING для type hints
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from module_b import SomeClass # Только для type checker
def func_a(obj: "SomeClass"): # String annotation
...
3. Рефакторинг: вынести shared код
# shared.py
CONSTANT = "shared value"
# module_a.py
from shared import CONSTANT
# module_b.py
from shared import CONSTANT
4. Dynamic import с importlib
import importlib
def func_a():
module_b = importlib.import_module("module_b")
return "A" + module_b.func_b()
5. Python 3.12+: Lazy imports (PEP 690)
Type Hints Gotcha¶
Type hints усугубляют проблему, так как они нужны на этапе определения функции!
# ❌ Проблема
from module_b import UserClass
def process_user(user: UserClass):
...
# ✅ Решение 1: String annotation
def process_user(user: "UserClass"):
...
# ✅ Решение 2: TYPE_CHECKING
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from module_b import UserClass
def process_user(user: "UserClass"):
...
Почему спрашивают?¶
- Проверяет понимание Python import system
- Выявляет опыт с модульной архитектурой
- Тестирует знание best practices для type hints
Источники: - How to Fix a Circular Import in Python - Python Circular Import: Causes, Fixes, and Best Practices - The Circular Import Trap in Python — and How to Escape It
9. Exception Handling: finally и return¶
Gotcha: finally переопределяет return¶
finally всегда выполняется, даже при return в try или except!
Код¶
# Пример 1: finally переопределяет return
def test_func():
try:
x = 10
return x
except Exception:
x = 20
return x
finally:
x = 30
return x # ❌ Переопределяет все предыдущие return!
print(test_func()) # 30 (не 10!)
# Пример 2: finally изменяет значение после return
def test_func2():
x = 10
try:
return x # x = 10 захватывается
finally:
x = 30 # Не влияет на возвращаемое значение
print(test_func2()) # 10 (x уже был захвачен)
# Пример 3: Порядок выполнения
def test_func3():
try:
print("try")
return "from try"
finally:
print("finally")
# Если здесь нет return, вернется "from try"
print(test_func3())
# Output:
# try
# finally
# from try
Ключевые правила¶
finallyвсегда выполняется (даже при return, break, continue)finallyс return переопределяет любой предыдущий return- Порядок:
try→finally→ return - Переменные: Значение return "захватывается" до finally (если finally не имеет своего return)
Опасный паттерн¶
def risky():
try:
raise Exception("Oops!")
except:
return "Error handled"
finally:
return "Finally" # ❌ Подавляет exception!
print(risky()) # "Finally" - exception потерян!
Best Practices¶
# ✅ Хорошо: finally для cleanup
def good_example():
f = open("file.txt")
try:
return f.read()
finally:
f.close() # Cleanup без return
# ✅ Еще лучше: context manager
def better_example():
with open("file.txt") as f:
return f.read()
PEP 765 (Proposal)¶
Python рассматривает добавление SyntaxWarning для return/break/continue в finally.
Почему спрашивают?¶
- Проверяет понимание exception handling flow
- Выявляет опыт с resource management
- Тестирует знание subtle Python behaviors
Источники: - Caveats of using return with try/except in Python - Do You Really Understand Try & Finally in Python? - Python Exceptions: An Introduction – Real Python
10. Memory Management и GC¶
Reference Counting + Generational GC¶
Python использует два механизма: 1. Reference Counting (основной) 2. Generational Garbage Collection (для циклических ссылок)
Reference Counting¶
import sys
a = []
print(sys.getrefcount(a)) # 2 (не 1!) - +1 от getrefcount()
b = a
print(sys.getrefcount(a)) # 3
del b
print(sys.getrefcount(a)) # 2
Проблема: Циклические ссылки¶
# Циклическая ссылка
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b
b.ref = a # Цикл!
del a
del b
# Reference count != 0, но объекты недостижимы
# GC их очистит
Generational GC¶
3 поколения объектов: - Gen 0: Новые объекты (частые проверки) - Gen 1: Объекты, пережившие 1 GC цикл - Gen 2: Long-lived объекты (редкие проверки)
import gc
# Получить статистику
print(gc.get_count()) # (threshold0, threshold1, threshold2)
# Ручной запуск GC (обычно НЕ нужен!)
gc.collect()
# Отключить GC (если уверены, что нет циклов)
gc.disable()
# Найти недостижимые объекты
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
print(len(gc.garbage))
Gotchas¶
1. sys.getrefcount() возвращает +1:
2. GC не отслеживает все типы:
# Отслеживаются:
list, dict, set, tuple (если содержат ссылки), classes
# НЕ отслеживаются:
int, str, float, True, None (immutable без ссылок)
3. Binary extensions могут утекать:
Weak References¶
import weakref
class MyClass:
pass
obj = MyClass()
weak_ref = weakref.ref(obj)
print(weak_ref()) # <MyClass object>
del obj
print(weak_ref()) # None (объект удален)
Best Practices¶
- НЕ отключайте GC без крайней необходимости
- НЕ вызывайте gc.collect() вручную (Python сам знает, когда)
- Используйте weak references для breaking cycles
- Используйте generators вместо больших списков
- Context managers для управления ресурсами
Почему спрашивают?¶
- Проверяет понимание Python internals
- Выявляет опыт с memory optimization
- Тестирует знание reference counting vs tracing GC
Источники: - Python Garbage Collection: Key Concepts and Mechanisms - CPython Reference Counting and Garbage Collection Internals - Garbage Collection in Python Explained
11. Global Interpreter Lock (GIL)¶
Что такое GIL?¶
GIL - это mutex, который гарантирует, что только один thread выполняет Python bytecode одновременно.
Почему GIL существует?¶
# Проблема без GIL: race condition на reference count
x = []
# Thread 1:
x.append(1) # inc refcount
# Thread 2:
del x # dec refcount
# БЕЗ GIL → crash! (неатомарные операции на refcount)
Когда GIL проблема?¶
CPU-bound tasks:
import threading
import time
def cpu_intensive():
total = 0
for i in range(10_000_000):
total += i
return total
# Многопоточность НЕ ускоряет CPU-bound код!
threads = [threading.Thread(target=cpu_intensive) for _ in range(4)]
start = time.time()
for t in threads: t.start()
for t in threads: t.join()
print(f"Threads: {time.time() - start:.2f}s") # ~8s (медленнее!)
# Multiprocessing ускоряет (отдельные GIL)
from multiprocessing import Process
processes = [Process(target=cpu_intensive) for _ in range(4)]
start = time.time()
for p in processes: p.start()
for p in processes: p.join()
print(f"Processes: {time.time() - start:.2f}s") # ~2s (быстрее!)
I/O-bound tasks:
import requests
import threading
def fetch_url(url):
response = requests.get(url)
return len(response.text)
urls = ["https://example.com"] * 10
# Threading РАБОТАЕТ для I/O!
# Пока один thread ждет network, другой может работать
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
Решения¶
| Подход | Use case | Параллелизм |
|---|---|---|
| Threading | I/O-bound (network, files, DB) | Concurrency ✅, Parallelism ❌ |
| Multiprocessing | CPU-bound (ML, image processing) | True parallelism ✅ |
| asyncio | I/O-bound (async await) | Async concurrency ✅ |
| C extensions | Compute-heavy (NumPy, releases GIL) | Parallelism ✅ |
Threading vs Multiprocessing¶
# Threading: shared memory
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
with lock:
counter += 1
# Multiprocessing: separate memory
from multiprocessing import Process, Value
def increment_mp(counter):
with counter.get_lock():
counter.value += 1
counter = Value('i', 0) # Shared memory через special types
Python 3.13: Free-threaded mode (PEP 703)¶
Interview Questions¶
Q: Почему Python не избавится от GIL? A: - Сломает весь C-extension ecosystem - Reference counting зависит от GIL - Single-threaded код станет медленнее - PEP 703 делает GIL опциональным (3.13+)
Q: Какие реализации Python без GIL? A: Jython, IronPython (но они устарели). PyPy имеет GIL.
Почему спрашивают?¶
- Проверяет понимание concurrency vs parallelism
- Выявляет опыт с многопоточностью
- Тестирует знание Python internals и performance
Ключевые слова для интервью: bottleneck, multi-threading, memory management, CPU-bound vs I/O-bound
Источники: - What Is the Python Global Interpreter Lock (GIL)? – Real Python - Global Interpreter Lock (GIL) in Python – Everything You Need to Know for Interviews - Advanced Python Interview Question — Understanding Python's GIL
12. List Multiplication Gotcha¶
Проблема¶
List multiplication создает shallow copies - одни и те же объекты реплицируются!
Код¶
# Пример 1: Классический баг
matrix = [[0] * 3] * 3
print(matrix) # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ❌
# Все строки - это ОДИН И ТОТ ЖЕ список!
print(id(matrix[0]) == id(matrix[1])) # True
# Пример 2: Визуализация проблемы
inner = [0, 0, 0]
matrix = [inner] * 3
# matrix = [inner, inner, inner] <- три ссылки на один список!
# Пример 3: С immutable типами работает
nums = [0] * 5 # [0, 0, 0, 0, 0] ✅
nums[0] = 1 # [1, 0, 0, 0, 0] ✅
# Числа immutable, создается новый объект
Решения¶
1. List comprehension (рекомендуется):
matrix = [[0] * 3 for _ in range(3)]
matrix[0][0] = 1
print(matrix) # [[1, 0, 0], [0, 0, 0], [0, 0, 0]] ✅
# Каждая итерация создает новый список
2. Deep copy:
3. Вложенный list comprehension:
Проверка независимости¶
# Проверить, что списки разные
matrix = [[0] * 3 for _ in range(3)]
print(id(matrix[0]) == id(matrix[1])) # False ✅
# Плохой способ
matrix = [[0] * 3] * 3
print(id(matrix[0]) == id(matrix[1])) # True ❌
Связанные gotchas¶
# Пример 1: С объектами классов
class Cell:
def __init__(self):
self.value = 0
# ❌ НЕПРАВИЛЬНО
cells = [Cell()] * 5 # Все ссылаются на один объект!
cells[0].value = 10
print([c.value for c in cells]) # [10, 10, 10, 10, 10]
# ✅ ПРАВИЛЬНО
cells = [Cell() for _ in range(5)]
cells[0].value = 10
print([c.value for c in cells]) # [10, 0, 0, 0, 0]
# Пример 2: Mixed nested structures
matrix = [[]] * 3
matrix[0].append(1)
print(matrix) # [[1], [1], [1]] ❌
Почему спрашивают?¶
- Проверяет понимание shallow vs deep copy
- Выявляет знание mutable vs immutable
- Тестирует опыт с common Python pitfalls
Источники: - Python List Multiplication: A Comprehensive Guide - Issue 27135: nested list produced with multiplication is linked to the same list
13. Dictionary Ordering¶
Python 3.7+: Dictionaries are Ordered¶
С Python 3.7 порядок insertion гарантирован!
Код¶
# Python 3.7+
d = {"b": 2, "a": 1, "c": 3}
print(list(d.keys())) # ['b', 'a', 'c'] (insertion order) ✅
# До Python 3.6 порядок был произвольный!
# Пример: Обновление ключей
d = {"a": 1, "b": 2}
d["c"] = 3
d["a"] = 10 # Порядок НЕ изменился!
print(list(d.keys())) # ['a', 'b', 'c']
# Пример: Удаление и повторная вставка
d = {"a": 1, "b": 2}
del d["a"]
d["a"] = 10
print(list(d.keys())) # ['b', 'a'] (добавлен в конец)
Gotchas¶
1. Ordered ≠ Sorted
d = {"z": 1, "a": 2, "m": 3}
print(list(d.keys())) # ['z', 'a', 'm'] (insertion order, НЕ sorted)
# Для sorted нужно явно:
sorted_keys = sorted(d.keys()) # ['a', 'm', 'z']
2. Dict НЕ sequence (нет индексов)
d = {"a": 1, "b": 2, "c": 3}
# d[0] # KeyError (нет индексов!)
# Получить первый элемент:
first_key = next(iter(d)) # 'a'
first_value = d[first_key] # 1
# Или:
first_key = list(d.keys())[0] # Неэффективно для больших dict
3. OrderedDict vs dict equality
from collections import OrderedDict
# Non-transitive equality!
od1 = OrderedDict([("a", 1), ("b", 2)])
d = {"a": 1, "b": 2}
od2 = OrderedDict([("b", 2), ("a", 1)])
print(od1 == d) # True
print(d == od2) # True
print(od1 == od2) # False (порядок разный!)
# Для OrderedDict порядок важен в ==
4. Backward compatibility
# Код для Python 3.6+:
def process_in_order(data):
for key in data: # Можно полагаться на порядок
...
# Код для Python 3.5 и ниже:
from collections import OrderedDict
def process_in_order(data):
data = OrderedDict(sorted(data.items())) # Явный порядок
for key in data:
...
OrderedDict vs dict (Python 3.7+)¶
Когда использовать OrderedDict:
1. Нужна equality с учетом порядка
2. Нужен move_to_end() метод
3. Обратная совместимость с Python < 3.7
4. Явное documentation намерения (порядок важен)
dict обычно достаточно:
Reverse iteration (Python 3.8+)¶
d = {"a": 1, "b": 2, "c": 3}
# Python 3.8+: reversed() работает
for key in reversed(d):
print(key) # 'c', 'b', 'a'
Почему спрашивают?¶
- Проверяет знание Python version changes
- Выявляет понимание dict implementation
- Тестирует awareness об ordering guarantees
Источники: - Are dictionaries ordered in Python? - Python Morsels - OrderedDict vs dict in Python – Real Python - Dicts are now ordered, get used to it
14. init vs new и Metaclasses¶
new vs init¶
__new__ - constructor (создает объект), __init__ - initializer (инициализирует).
Код¶
# Пример 1: Основное различие
class MyClass:
def __new__(cls, *args, **kwargs):
print("__new__ called")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print("__init__ called")
self.value = value
obj = MyClass(42)
# Output:
# __new__ called
# __init__ called
# Пример 2: __new__ для singletons
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True ✅
# Пример 3: Immutable types
class PositiveInt(int):
def __new__(cls, value):
if value < 0:
raise ValueError("Must be positive")
return super().__new__(cls, value)
num = PositiveInt(5) # ✅
# num = PositiveInt(-5) # ValueError
Ключевые правила¶
__new__вызывается первым (до__init__)__new__должен вернуть instance (обычноcls)__init__НЕ должен ничего возвращать (толькоNone)- Если
__new__не вернул instance ofcls,__init__не вызывается
class Strange:
def __new__(cls):
return "not an instance"
def __init__(self):
print("__init__ called") # НЕ вызовется!
obj = Strange()
print(obj) # "not an instance"
print(type(obj)) # <class 'str'>
Metaclasses¶
Metaclass - это класс класса (class factory).
# Пример 1: type() как metaclass
# type(name, bases, dict) создает класс
MyClass = type("MyClass", (), {"x": 5})
print(MyClass.x) # 5
# Эквивалентно:
class MyClass:
x = 5
# Пример 2: Custom metaclass
class Meta(type):
def __new__(mcs, name, bases, dct):
print(f"Creating class {name}")
dct["added_by_meta"] = True
return super().__new__(mcs, name, bases, dct)
class MyClass(metaclass=Meta):
pass
# Output: Creating class MyClass
print(MyClass.added_by_meta) # True
# Пример 3: Порядок вызовов
class Meta(type):
def __prepare__(mcs, name, bases):
print("1. __prepare__")
return {}
def __new__(mcs, name, bases, dct):
print("2. __new__ (metaclass)")
return super().__new__(mcs, name, bases, dct)
def __init__(cls, name, bases, dct):
print("3. __init__ (metaclass)")
super().__init__(name, bases, dct)
def __call__(cls, *args, **kwargs):
print("4. __call__ (metaclass)")
instance = super().__call__(*args, **kwargs)
return instance
class MyClass(metaclass=Meta):
def __init__(self):
print("5. __init__ (class)")
obj = MyClass()
# Output:
# 1. __prepare__
# 2. __new__ (metaclass)
# 3. __init__ (metaclass)
# 4. __call__ (metaclass)
# 5. __init__ (class)
Современная альтернатива: init_subclass (PEP 487)¶
# Вместо metaclass:
class Base:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls.added = True
class Derived(Base):
pass
print(Derived.added) # True
# Проще и понятнее, чем metaclass!
Gotchas¶
1. Metaclass conflicts:
class Meta1(type): pass
class Meta2(type): pass
class A(metaclass=Meta1): pass
class B(metaclass=Meta2): pass
# class C(A, B): pass # TypeError: metaclass conflict!
# Нужно создать Meta3(Meta1, Meta2)
2. Нельзя изменить type.new:
3. init returning non-None:
Когда использовать?¶
__new__: Immutable types, singletons, factory pattern__init__: 99% случаев- Metaclasses: ORM frameworks, DSL, plugin systems (редко!)
Цитата: "Metaclasses are deeper magic than 99% of users should ever worry about."
Почему спрашивают?¶
- Проверяет глубокое понимание Python object model
- Выявляет опыт с advanced patterns
- Тестирует знание construction process
Источники: - Python Metaclasses – Real Python - Python Metaclass new() Method - Demystifying the Black Magic in Python: Metaclasses
15. Generator и StopIteration¶
PEP 479: StopIteration → RuntimeError¶
С Python 3.7: StopIteration в generator превращается в RuntimeError!
Код¶
# Пример 1: Старое поведение (Python < 3.7)
def old_gen():
yield 1
raise StopIteration # Раньше было OK
# Python 3.7+:
# RuntimeError: generator raised StopIteration
# Пример 2: Правильный способ
def good_gen():
yield 1
return # ✅ Используйте return вместо raise StopIteration
# Пример 3: Проблема с unguarded next()
def buggy_gen():
iterator = iter([1, 2, 3])
while True:
yield next(iterator) # StopIteration убежит наружу!
# Python 3.7+: RuntimeError вместо silent termination
# Пример 4: yield from и проблема
def outer():
yield from inner()
def inner():
yield 1
some_iterator = iter([])
next(some_iterator) # StopIteration!
# StopIteration ломает абстракцию yield from
Почему изменили?¶
До PEP 479: StopIteration в generator → silent termination
Проблема: Скрывает баги! Unguarded next() может случайно остановить generator.
# Пример бага
def gen():
data = iter([1, 2, 3])
yield next(data) # 1
yield next(data) # 2
yield next(data) # 3
yield next(data) # StopIteration → generator просто останавливается
# Ожидали ValueError или explicit stop
list(gen()) # [1, 2, 3] - баг незаметен!
Timeline изменений¶
- Python 3.5: Warning под
from __future__ import generator_stop - Python 3.6: Non-silent deprecation warning
- Python 3.7: Новое поведение везде (StopIteration → RuntimeError)
Generator return values (Python 3.3+)¶
# Пример: Generator может возвращать значение
def gen_with_return():
yield 1
yield 2
return "done" # ✅ Начиная с Python 3.3
g = gen_with_return()
print(next(g)) # 1
print(next(g)) # 2
try:
next(g)
except StopIteration as e:
print(e.value) # "done"
# Или с yield from:
def outer():
result = yield from gen_with_return()
print(f"Inner returned: {result}") # "Inner returned: done"
Best Practices¶
1. Используйте return вместо raise StopIteration:
2. Обрабатывайте StopIteration от вложенных iterators:
# ❌ Плохо
def buggy_gen(iterator):
while True:
yield next(iterator) # Может поднять StopIteration
# ✅ Хорошо
def safe_gen(iterator):
while True:
try:
yield next(iterator)
except StopIteration:
return # Явное завершение
3. Используйте yield from для делегирования:
Почему спрашивают?¶
- Проверяет знание Python version changes
- Выявляет опыт с generators
- Тестирует понимание exception handling в generators
Источники: - PEP 479 – Change StopIteration handling inside generators - Python 3.7: StopIteration could be your next headache - How to handle StopIteration in Python generators
16. Walrus Operator (:=)¶
Assignment Expressions (Python 3.8+)¶
Walrus operator (:=) позволяет присваивать и использовать значение в одном expression.
Код¶
# Пример 1: Базовое использование
# До Python 3.8:
n = len(data)
if n > 10:
print(f"List is too long ({n} elements)")
# Python 3.8+:
if (n := len(data)) > 10:
print(f"List is too long ({n} elements)")
# Пример 2: В list comprehension
# До:
filtered = [y for x in data if (y := f(x)) is not None]
# Вместо:
filtered = [f(x) for x in data if f(x) is not None] # f(x) вызывается 2 раза!
# Пример 3: В while loop
# До:
while True:
line = file.readline()
if not line:
break
process(line)
# Python 3.8+:
while (line := file.readline()):
process(line)
Gotchas¶
1. Operator precedence:
# ❌ НЕПРАВИЛЬНО
if walrus := 3.8 and True:
print(walrus) # True (не 3.8!)
# ✅ ПРАВИЛЬНО
if (walrus := 3.8) and True:
print(walrus) # 3.8
# Walrus имеет низкий приоритет, нужны скобки
2. Tuple assignment:
# ❌ НЕПРАВИЛЬНО
walrus := 3.8, True # SyntaxError
# ✅ ПРАВИЛЬНО (но не то, что хотели)
(walrus := 3.8, True) # walrus = 3.8, не кортеж
# ✅ ПРАВИЛЬНО (присвоить кортеж)
(walrus := (3.8, True)) # walrus = (3.8, True)
3. Short-circuiting:
# Пример: and short-circuit
data = [1, 2, 3]
if (first := data[0]) and (second := data[1]):
print(first, second) # 1 2
# Если first falsy:
data = [0, 2, 3]
if (first := data[0]) and (second := data[1]):
print(first, second) # НЕ выполнится!
# second НЕ присвоен! Может быть NameError позже.
# Пример: Stale value в цикле
for item in items:
if (x := process(item)) and (y := transform(x)):
use(y)
# Если process() вернет falsy, y не обновится!
4. Invalid syntactic positions:
# ❌ На верхнем уровне нужны скобки
walrus := 42 # SyntaxError
# ✅ Правильно
(walrus := 42)
# ❌ В comprehension: нельзя переопределить for-переменную
[y for y in range(10) if (y := y * 2) > 5] # SyntaxError
5. Type hints challenges:
# Сложно добавить type hint для walrus
if (result := compute()) > 0: # Какой тип у result?
...
# Workaround:
result: int
if (result := compute()) > 0:
...
6. Version compatibility:
Best Practices¶
✅ Хорошие use cases:
# 1. Избежать повторных вычислений
if (match := pattern.search(text)):
print(match.group(1))
# 2. While loops с condition
while (chunk := file.read(8192)):
process(chunk)
# 3. List comprehensions с фильтрацией
[y for x in data if (y := transform(x)) is not None]
❌ Плохие use cases:
# 1. Nested walrus (сложно читать)
if (a := (b := func())): # ❌ Запутанно
# 2. Заменять нормальный assignment
(result := compute()) # ❌ Просто используйте result = compute()
# 3. В сложных expressions
if ((x := a()) and (y := b(x))) or ((z := c()) and (w := d(z))): # ❌ Ужас
Почему спрашивают?¶
- Проверяет знание Python 3.8+ features
- Выявляет понимание operator precedence
- Тестирует знание when to use vs when to avoid
Источники: - The Walrus Operator: Python's Assignment Expressions – Real Python - How (Not) To Use Python's Walrus Operator - PEP 572 – Assignment Expressions
17. Decorators и functools.wraps¶
Проблема: Потеря metadata¶
Без @functools.wraps декоратор скрывает metadata оригинальной функции.
Код¶
# Пример 1: Плохой декоратор
def bad_decorator(func):
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@bad_decorator
def greet(name):
"""Greet someone"""
print(f"Hello, {name}")
print(greet.__name__) # "wrapper" ❌
print(greet.__doc__) # "Wrapper docstring" ❌
# Пример 2: Правильный декоратор с functools.wraps
from functools import wraps
def good_decorator(func):
@wraps(func) # ✅ Сохраняет metadata
def wrapper(*args, **kwargs):
"""Wrapper docstring"""
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@good_decorator
def greet(name):
"""Greet someone"""
print(f"Hello, {name}")
print(greet.__name__) # "greet" ✅
print(greet.__doc__) # "Greet someone" ✅
print(greet.__wrapped__.__name__) # "greet" (доступ к оригиналу)
Что делает functools.wraps?¶
Копирует атрибуты из wrapped в wrapper:
# WRAPPER_ASSIGNMENTS (копируются):
__module__
__name__
__qualname__
__annotations__
__type_params__ # Python 3.12+
__doc__
# WRAPPER_UPDATES (обновляются):
__dict__
Дополнительный бонус: wrapped¶
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator
def original(x, y=10):
"""Original function"""
return x + y
# Доступ к оригиналу:
print(original.__wrapped__) # <function original at ...>
# Доступ к __defaults__ (не копируется wraps)
print(original.__defaults__) # None ❌
print(original.__wrapped__.__defaults__) # (10,) ✅
Gotchas¶
1. Class-based decorators:
from functools import update_wrapper
class Decorator:
def __init__(self, func):
update_wrapper(self, func) # ✅ Для классов
self.func = func
def __call__(self, *args, **kwargs):
print("Before")
return self.func(*args, **kwargs)
@Decorator
def greet(name):
"""Greet someone"""
print(f"Hello, {name}")
print(greet.__name__) # "greet" ✅
2. Decorators с аргументами:
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
"""Greet someone"""
print(f"Hello, {name}")
print(greet.__name__) # "greet" ✅
3. Stacking decorators:
@decorator1
@decorator2
@decorator3
def func():
pass
# Эквивалентно:
func = decorator1(decorator2(decorator3(func)))
# Каждый декоратор должен использовать @wraps
Best Practices¶
Template для любого декоратора:
from functools import wraps
def my_decorator(func):
@wraps(func) # ✅ ВСЕГДА добавляйте
def wrapper(*args, **kwargs):
# Before logic
result = func(*args, **kwargs)
# After logic
return result
return wrapper
Для декораторов с аргументами:
from functools import wraps
def my_decorator(arg1, arg2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Use arg1, arg2
return func(*args, **kwargs)
return wrapper
return decorator
Почему это важно?¶
Инструменты, которые ломаются без @wraps:
- Debuggers (показывают неправильные имена функций)
- Help() и documentation tools
- Object serializers (pickle)
- Introspection tools
- Type checkers
Почему спрашивают?¶
- Проверяет знание decorator patterns
- Выявляет понимание function metadata
- Тестирует опыт с production-ready code
Источники: - A Deep Dive Into Python's functools.wraps Decorator - Python Decorator: Why You Should Use functools.wraps - Primer on Python Decorators – Real Python
18. Class vs Instance Variables¶
Проблема: Shared State¶
Class variables shared между всеми instances!
Код¶
# Пример 1: Классическая ошибка
class Dog:
tricks = [] # ❌ Class variable (shared!)
def __init__(self, name):
self.name = name # ✅ Instance variable
def add_trick(self, trick):
self.tricks.append(trick)
fido = Dog("Fido")
buddy = Dog("Buddy")
fido.add_trick("roll over")
print(buddy.tricks) # ['roll over'] ❌ Buddy знает трюки Fido!
# Пример 2: Правильное решение
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # ✅ Instance variable
def add_trick(self, trick):
self.tricks.append(trick)
fido = Dog("Fido")
buddy = Dog("Buddy")
fido.add_trick("roll over")
print(buddy.tricks) # [] ✅
Как работает атрибут lookup?¶
class MyClass:
class_var = "shared"
def __init__(self):
self.instance_var = "unique"
obj1 = MyClass()
obj2 = MyClass()
# Instance attribute lookup:
# 1. Сначала ищет в obj.__dict__
# 2. Потом в MyClass.__dict__
# 3. Потом в базовых классах
print(obj1.instance_var) # "unique" (найден в obj1.__dict__)
print(obj1.class_var) # "shared" (найден в MyClass.__dict__)
Gotcha: Модификация через instance¶
class MyClass:
x = 10 # Class variable
obj1 = MyClass()
obj2 = MyClass()
# Чтение:
print(obj1.x) # 10 (class variable)
print(obj2.x) # 10 (class variable)
# Модификация через instance:
obj1.x = 20 # ❌ Создает INSTANCE variable!
print(obj1.x) # 20 (instance variable)
print(obj2.x) # 10 (class variable, не изменился)
print(MyClass.x) # 10 (class variable, не изменился)
# Правильная модификация class variable:
MyClass.x = 30
print(obj1.x) # 20 (instance variable перекрывает)
print(obj2.x) # 30 (class variable изменился)
Mutable Class Variables - ОПАСНО!¶
# Пример: Mutable class variable
class BankAccount:
total_balance = 0 # ✅ OK (immutable)
all_transactions = [] # ❌ ОПАСНО (mutable)
def __init__(self, owner):
self.owner = owner
self.balance = 0
def deposit(self, amount):
self.balance += amount
BankAccount.total_balance += amount # ✅ Правильно
self.all_transactions.append(amount) # ❌ SHARED!
acc1 = BankAccount("Alice")
acc2 = BankAccount("Bob")
acc1.deposit(100)
print(acc2.all_transactions) # [100] ❌ Bob видит транзакции Alice!
# Правильно:
class BankAccount:
total_balance = 0
def __init__(self, owner):
self.owner = owner
self.balance = 0
self.transactions = [] # ✅ Instance variable
Когда использовать Class Variables?¶
✅ Хорошие use cases:
# 1. Constants
class Circle:
PI = 3.14159 # ✅ Константа
# 2. Shared configuration
class Database:
connection_pool_size = 10 # ✅ Shared setting
# 3. Counters для всех instances
class User:
total_users = 0
def __init__(self, name):
self.name = name
User.total_users += 1 # ✅ Правильно модифицируем
# 4. Default values (immutable)
class Request:
timeout = 30 # ✅ Default для всех
❌ Плохие use cases:
# 1. Mutable containers
class Bad:
items = [] # ❌ Shared state!
# 2. Instance-specific data
class Bad:
data = {} # ❌ Должна быть instance variable
Inheritance и Class Variables¶
class Parent:
x = 10
class Child(Parent):
pass
# Child наследует x
print(Child.x) # 10
# Модификация в Parent:
Parent.x = 20
print(Child.x) # 20 (если Child не override)
# Override в Child:
Child.x = 30
print(Parent.x) # 20 (не изменился)
print(Child.x) # 30 (override)
Best Practices¶
- Всегда используйте instance variables для mutable state
- Модифицируйте class variables через class name:
ClassName.var = ... - Используйте UPPERCASE для constант:
MAX_SIZE = 100 - Документируйте shared state явно
Почему спрашивают?¶
- Проверяет понимание Python object model
- Выявляет опыт с class design
- Тестирует знание common pitfalls
Источники: - Understanding Class and Instance Variables in Python 3 - Python Class Variables: Why Are They Shared Across All Instances? - Python Class Variables vs. Instance Variables
19. Дополнительные Tricky Questions¶
1. Dictionary Attribute Access¶
example_dict = dict()
example_dict.a = "string" # AttributeError!
# Почему?
# Dictionaries НЕ поддерживают dot notation
# Только bracket notation:
example_dict["a"] = "string" # ✅
# Для dot notation:
from types import SimpleNamespace
obj = SimpleNamespace(a="string")
print(obj.a) # ✅
2. Local Import Scope¶
class Json:
def __init__(self):
import json # Local scope!
def print_dict_as_json(self, obj):
print(json.dumps(obj)) # NameError!
# Решение:
class Json:
def __init__(self):
import json
self.json = json # Сохранить как атрибут
def print_dict_as_json(self, obj):
print(self.json.dumps(obj)) # ✅
3. **kwargs Parameter Handling¶
def myFun(arg1, **kwargs):
for key, value in kwargs.items():
print(f"{key} == {value}")
my_dict = {'arg1': 1, 'arg2': 2}
myFun(**my_dict, arg3=3)
# Output:
# arg2 == 2 (arg1 consumed by positional)
# arg3 == 3
4. List Reference Assignment¶
arr = [1, 2, 3]
def add_to_all(lst):
for i in range(len(lst)):
lst[i] += 1
add_to_all(arr)
arr2 = arr # Просто ссылка!
print(arr2) # [2, 3, 4] (изменения видны)
5. String Concatenation in Loop¶
# ❌ МЕДЛЕННО (O(n²))
result = ""
for s in strings:
result += s # Каждая итерация создает новую строку!
# ✅ БЫСТРО (O(n))
result = "".join(strings)
# Почему?
# Strings immutable → каждый += создает новый объект
# join() делает один allocation
6. Default Argument Time¶
import datetime
def log(message, timestamp=datetime.datetime.now()):
print(f"{timestamp}: {message}")
log("First")
time.sleep(2)
log("Second")
# Оба логи имеют ОДИНАКОВЫЙ timestamp!
# datetime.now() вызывается при def, не при вызове
# Решение:
def log(message, timestamp=None):
if timestamp is None:
timestamp = datetime.datetime.now()
print(f"{timestamp}: {message}")
7. Boolean Arithmetic¶
# True == 1, False == 0 в Python
print(True + True) # 2
print(True * 5) # 5
print(False + 10) # 10
# Можно использовать в трюках:
count = sum(x > 5 for x in data) # Считает сколько элементов > 5
8. Extended Unpacking¶
# Пример 1: * в unpacking
a, *b, c = [1, 2, 3, 4, 5]
print(a) # 1
print(b) # [2, 3, 4]
print(c) # 5
# Пример 2: _ для ignored values
_, *middle, _ = [1, 2, 3, 4, 5]
print(middle) # [2, 3, 4]
# Gotcha: Только один * в unpacking
# a, *b, *c = [1, 2, 3] # SyntaxError
9. Chained Comparisons¶
# Python поддерживает chained comparisons
x = 5
if 1 < x < 10: # ✅ Эквивалентно: 1 < x and x < 10
print("In range")
# Gotcha: Может быть неочевидно
a = [1, 2, 3]
if 1 in a == True: # Эквивалентно: (1 in a) and (a == True)
print("OK") # НЕ печатается! a != True
10. Import Star Surprises¶
# module.py
__all__ = ['func1', 'func2']
def func1(): pass
def func2(): pass
def _private(): pass
# main.py
from module import *
# Только func1, func2 импортированы
# _private НЕ импортирован (если нет в __all__)
Распространённые заблуждения¶
Заблуждение: is и == -- одно и то же для чисел
CPython кэширует integers только от -5 до 256. За этим диапазоном 257 is 257 может вернуть False в REPL и True в скрипте (из-за оптимизации компилятора). На продакшене никогда не используйте is для сравнения чисел -- только ==.
Заблуждение: copy.copy() создаёт полностью независимую копию
copy.copy() -- это shallow copy: копируется только верхний уровень. Для [[1,2],[3,4]] внутренние списки остаются общими. В ML-пайплайнах с вложенными конфигами (dict внутри dict) это приводит к скрытой мутации данных. Используйте copy.deepcopy() для nested структур.
Заблуждение: finally только для cleanup и не влияет на return
return внутри finally перезаписывает любой return из try/except и даже подавляет exceptions. Это одна из самых неочевидных особенностей Python: finally: return x превращает необработанное исключение в нормальный возврат.
Резюме по категориям¶
Самые критичные для собеседований:¶
- Mutable Default Arguments - #1 gotcha
- Late Binding Closures - частый вопрос
- is vs == - базовое понимание
- Shallow vs Deep Copy - важно для работы с данными
- GIL - senior-level вопрос
Intermediate уровень:¶
- LEGB Rule - для понимания scoping
- Circular Imports - архитектурный вопрос
- finally и return - subtle behavior
- Class vs Instance Variables - OOP fundamentals
- List Multiplication - common pitfall
Advanced уровень:¶
- Memory Management & GC - internals
- Metaclasses - "black magic"
- StopIteration changes - version awareness
- Walrus Operator - modern Python
- functools.wraps - production patterns
Вопросы на собеседовании¶
"Что выведет этот код? (mutable default argument)"
Слабый ответ: "Не знаю, наверное каждый раз новый список"
Сильный ответ: "Default arguments создаются один раз при
def, а не при каждом вызове. Список мутируется между вызовами. Идиоматичное решение -- lst=None с проверкой if lst is None: lst = []."
"В чём разница между is и ==?"
Слабый ответ: "
is проверяет тип, == проверяет значение"
Сильный ответ: "
== вызывает __eq__ и сравнивает значения. is сравнивает identity (один и тот же объект в памяти, id(a) == id(b)). Используется только для None, True, False. Из-за integer caching (-5..256) is может давать ложные True для чисел."
"Как работает GIL и когда это проблема?"
Слабый ответ: "GIL замедляет Python, нужно использовать multiprocessing"
Сильный ответ: "GIL -- mutex в CPython, защищающий reference counting. Проблема только для CPU-bound: threading не даёт parallelism. Для I/O-bound (сеть, файлы) GIL освобождается при ожидании, поэтому threading/asyncio работают. CPU-bound -- multiprocessing (отдельные GIL). NumPy/TensorFlow освобождают GIL в C-коде. Python 3.13 имеет экспериментальный free-threaded режим."
"Что такое late binding в closures?"
Слабый ответ: "Lambda захватывает переменную по ссылке"
Сильный ответ: "Все closures в Python используют late binding -- значение переменной извлекается в момент вызова, не создания.
[lambda: i for i in range(5)] вернёт [4,4,4,4,4]. Это не проблема lambda -- def ведёт себя так же. Решения: default parameter i=i, closure factory, functools.partial."
Источники и дополнительные материалы¶
Официальная документация¶
Статьи и туториалы¶
Книги¶
- "Effective Python" by Brett Slatkin
- "Fluent Python" by Luciano Ramalho
- "Python Cookbook" by David Beazley
Конец документа Total: 19 категорий gotchas + множество примеров кода Подходит для: Senior/Middle Python developer интервью, база вопросов для Nareshka, technical screening