Перейти к содержанию

Python Gotchas и Tricky Questions

~17 минут чтения

Предварительно: Подготовка к Python-интервью | Особенности языка

По данным опросов на Stack Overflow и HackerRank, mutable default arguments -- gotcha #1, с которой сталкиваются 73% Python-разработчиков в первый год. На собеседованиях вопросы категории "что выведет код" встречаются в 80%+ Python-интервью (FAANG, финтех, Яндекс). Здесь собраны 19 категорий ловушек -- от классических до продвинутых -- с кодом, объяснениями и решениями.


Содержание

  1. Mutable Default Arguments
  2. Late Binding Closures
  3. Integer Caching и String Interning
  4. is vs == (Identity vs Equality)
  5. Shallow vs Deep Copy
  6. LEGB Rule и Variable Scope
  7. Truthiness и Falsy Values
  8. Circular Imports
  9. Exception Handling: finally и return
  10. Memory Management и GC
  11. Global Interpreter Lock (GIL)
  12. List Multiplication Gotcha
  13. Dictionary Ordering
  14. init vs new и Metaclasses
  15. Generator и StopIteration
  16. Walrus Operator (:=)
  17. Decorators и functools.wraps
  18. Class vs Instance Variables
  19. Дополнительные 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, а не при вызове
  • Один и тот же список переиспользуется между вызовами

Решение

def append_to_list(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst

Почему спрашивают?

  • Проверяет понимание модели выполнения 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 (рекомендуется):

funcs = [lambda i=i: i for i in range(5)]
print([f() for f in funcs])  # [0, 1, 2, 3, 4] ✅

2. Closure factory:

def make_func(i):
    return lambda: i

funcs = [make_func(i) for i in range(5)]

3. IIFE (Immediately Invoked Function Expression):

funcs = [(lambda x: lambda: x)(i) for i in range(5)]

4. functools.partial:

from functools import partial
funcs = [partial(lambda i: i, i) for i in range(5)]

Почему спрашивают?

  • Проверяет понимание 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

Почему спрашивают?

  • Проверяет понимание разницы is vs ==
  • Выявляет знание 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"

def foo():
    print(y)  # NameError: name 'y' is not defined

2. "local variable referenced before assignment"

x = 10
def bar():
    x = x + 1  # UnboundLocalError

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 и nonlocal keywords

Источники: - 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:

# ✅ Хорошо
if x is None:
    ...

# ❌ Плохо (если x может быть 0, [], "", etc.)
if not x:
    ...

Почему спрашивают?

  • Проверяет понимание 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:

from module_b import func_b

def func_a():
    return "A" + func_b()

module_b.py:

from module_a import func_a  # ❌ Circular import!

def func_b():
    return "B"

Ошибка:

ImportError: cannot import name 'func_a' from partially initialized module 'module_a'

Почему происходит?

  1. Python начинает загружать module_a
  2. Встречает from module_b import func_b
  3. Начинает загружать module_b
  4. Встречает from module_a import func_a
  5. module_a еще не загружен полностью → ошибка

Решения

1. Import внутри функции (deferred import)

# module_a.py
def func_a():
    from module_b import func_b  # ✅ Ленивая загрузка
    return "A" + func_b()

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

Ключевые правила

  1. finally всегда выполняется (даже при return, break, continue)
  2. finally с return переопределяет любой предыдущий return
  3. Порядок: tryfinally → return
  4. Переменные: Значение 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:

x = []
print(sys.getrefcount(x))  # 2 (не 1!)
# getrefcount() сам создает временную ссылку

2. GC не отслеживает все типы:

# Отслеживаются:
list, dict, set, tuple (если содержат ссылки), classes

# НЕ отслеживаются:
int, str, float, True, None (immutable без ссылок)

3. Binary extensions могут утекать:

# C-расширения имеют свой memory management
# Могут не подчиняться Python GC

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

  1. НЕ отключайте GC без крайней необходимости
  2. НЕ вызывайте gc.collect() вручную (Python сам знает, когда)
  3. Используйте weak references для breaking cycles
  4. Используйте generators вместо больших списков
  5. 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)

# Экспериментальный build без GIL
python3.13 --disable-gil script.py

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:

import copy
inner = [0, 0, 0]
matrix = [copy.deepcopy(inner) for _ in range(3)]

3. Вложенный list comprehension:

matrix = [[0 for _ in range(3)] for _ in range(3)]

Проверка независимости

# Проверить, что списки разные
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 обычно достаточно:

# Python 3.7+: dict уже ordered
d = {}
# Порядок сохраняется автоматически

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

Ключевые правила

  1. __new__ вызывается первым (до __init__)
  2. __new__ должен вернуть instance (обычно cls)
  3. __init__ НЕ должен ничего возвращать (только None)
  4. Если __new__ не вернул instance of cls, __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:

# type.__new__ = ...  # TypeError

3. init returning non-None:

class Bad:
    def __init__(self):
        return 42  # TypeError: __init__() should return 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:

# ❌ Плохо
def bad_gen():
    yield 1
    raise StopIteration

# ✅ Хорошо
def good_gen():
    yield 1
    return

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 для делегирования:

def outer():
    yield from inner()  # Правильное делегирование

Почему спрашивают?

  • Проверяет знание 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:

# Python 3.7 и ниже:
# SyntaxError: invalid syntax

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

  1. Всегда используйте instance variables для mutable state
  2. Модифицируйте class variables через class name: ClassName.var = ...
  3. Используйте UPPERCASE для constант: MAX_SIZE = 100
  4. Документируйте 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 превращает необработанное исключение в нормальный возврат.


Резюме по категориям

Самые критичные для собеседований:

  1. Mutable Default Arguments - #1 gotcha
  2. Late Binding Closures - частый вопрос
  3. is vs == - базовое понимание
  4. Shallow vs Deep Copy - важно для работы с данными
  5. GIL - senior-level вопрос

Intermediate уровень:

  1. LEGB Rule - для понимания scoping
  2. Circular Imports - архитектурный вопрос
  3. finally и return - subtle behavior
  4. Class vs Instance Variables - OOP fundamentals
  5. List Multiplication - common pitfall

Advanced уровень:

  1. Memory Management & GC - internals
  2. Metaclasses - "black magic"
  3. StopIteration changes - version awareness
  4. Walrus Operator - modern Python
  5. 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