← Назад к курсу

Дополнительные вопросы-ответы по Python

1. Что такое PEP8? Как Вы к нему относитесь?

PEP8 (Python Enhancement Proposal 8) — это официальное руководство по стилю кода на Python. Он описывает соглашения о форматировании, именовании, отступах (4 пробела), максимальной длине строки (79 символов) и многом другом для написания читаемого и консистентного кода.

Мое отношение: Строго положительное. Следование PEP8 — это признак профессионализма. Это:

  • Улучшает читаемость: Код становится понятным для всей команды.
  • Облегчает поддержку: Единый стиль снижает когнитивную нагрузку.
  • Автоматизируется: Инструменты (linters, форматтеры) легко проверяют и применяют правила.

Я использую инструменты типа black (автоформатер) и flake8 (линтер), чтобы код соответствовал PEP8 автоматически.

2. Шаблоны проектирования (Design Patterns) в Python

Это типовые решения распространенных проблем проектирования. Python благодаря своей динамической природе часто позволяет реализовать их проще, чем в статически типизированных языках.

Известные шаблоны и их реализация:

  • Singleton (Одиночка): Гарантирует один экземпляр класса. Реализации: через метакласс, декоратор, модуль (как модуль в Python — уже синглтон).
  • Factory (Фабрика): Создает объекты без указания конкретного класса. Реализуется через отдельную функцию-фабрику или класс.
  • Observer (Наблюдатель): Объект уведомляет другие объекты об изменениях своего состояния. Реализуется через списки callback-функций или модуль asyncio.Event.
  • Decorator (Декоратор): Динамически добавляет новую функциональность объекту. В Python реализуется "из коробки" синтаксисом @decorator.
  • Strategy (Стратегия): Определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. Легко реализуется через передачу функций как объектов.

3. Варианты реализации Singleton в Python

  1. Через модуль: Самый простой способ. Модуль в Python загружается один раз. Просто создайте нужный объект в модуле.
    # singleton.py
    instance = SomeClass()
    
  2. Через декоратор класса: Обертывает класс, контролируя создание экземпляров.
    def singleton(cls):
        instances = {}
        def get_instance(*args, **kwargs):
            if cls not in instances:
                instances[cls] = cls(*args, **kwargs)
            return instances[cls]
        return get_instance
    
    Недостаток для тестирования: Затрудняет сброс состояния между тестами, так как инстанс глобально кешируется. Нужно "ломать" декоратор.
  3. Через метакласс: Управляет процессом создания самого класса.
    class SingletonMeta(type):
        _instances = {}
        def __call__(cls, *args, **kwargs):
            if cls not in cls._instances:
                cls._instances[cls] = super().__call__(*args, **kwargs)
            return cls._instances[cls]
    class Singleton(metaclass=SingletonMeta):
        pass
    
    Элегантность: Логика инстанциирования инкапсулирована в метаклассе, сам класс остается "чистым". Более универсален и ООП-ориентирован.
  4. Через переопределение __new__: Контролирует создание экземпляра на уровне класса.
    class Singleton:
        _instance = None
        def __new__(cls, *args, **kwargs):
            if cls._instance is None:
                cls._instance = super().__new__(cls)
            return cls._instance
    

4. Декораторы

Декоратор — это функция, которая принимает другую функцию (или класс) и расширяет её поведение, не меняя исходный код.

  • Базовое понимание: Декоратор — это "синтаксический сахар" для конструкции func = decorator(func). Может быть функцией, принимающей функцию и возвращающей новую функцию.
  • Продвинутое использование: Цепочки декораторов, декораторы с аргументами (требуется дополнительная обертка), декораторы классов, использование functools.wraps для сохранения метаданных оригинальной функции.

5. Public, Private, Static методы в Python

  • Public: Методы и атрибуты без подчеркиваний. Доступны отовсюду.
  • Protected (условно): Одно нижнее подчеркивание _method. Сигнал разработчику "не используй снаружи", но интерпретатор не ограничивает доступ.
  • Private (искажение имени): Два нижних подчеркивания __method. Интерпретатор меняет имя внутри класса на _ClassName__method, чтобы предотвратить случайные переопределения в подклассах, но доступ все еще возможен.
  • Static (@staticmethod): Метод, не получающий ни self, ни cls. Это просто функция, привязанная к пространству имен класса.
  • Class (@classmethod): Метод, получающий первым аргументом класс (cls), а не экземпляр. Используется для фабричных методов или альтернативных конструкторов.

6. Метаклассы

Метакласс — это "класс классов". Он определяет, как создаются классы. Если класс — это шаблон для создания объектов, то метакласс — это шаблон для создания классов.

  • Аналогия: Класс : Объект = Метакласс : Класс.
  • Использование: type — это встроенный метакласс всех классов. Создавая свой метакласс (наследуясь от type), можно перехватывать и изменять процесс создания класса (например, для регистрации, добавления атрибутов, проверки и т.д.).
  • Главное применение: Сложные фреймворки (ORM, валидация), автоматическая генерация кода, синглтоны.

7. Отличия Python 2.x и 3.x (на примере 2.7 и 3.4+)

  • print — функция: print "hello" -> print("hello").
  • Деление: В Python 2 5 / 2 = 2 (целочисленное), в Python 3 5 / 2 = 2.5 (вещественное). Для целочисленного в Python 3 — //.
  • Unicode: В Python 2 строки str — байты, unicode — Unicode. В Python 3 str — Unicode, bytes — байты.
  • xrange: В Python 2 range (список) и xrange (генератор). В Python 3 range — генератор, а xrange удален.
  • Итераторы: В Python 3 dict.keys(), .values(), .items() возвращают view-объекты (итераторы), а не списки.
  • Синтаксис: except Exception as e вместо except Exception, e.
  • Классы: В Python 2 по умолчанию "old-style", нужно явно наследоваться от object. В Python 3 все классы "new-style".

8. __new__ vs __init__

  • __new__(cls, ...): Статический метод (получает класс), создает и возвращает новый экземпляр (объект). Вызывается первым.
  • __init__(self, ...): Метод экземпляра, инициализирует только что созданный объект (self). Вызывается после __new__, если __new__ вернул экземпляр того же класса.
  • Последовательность: __new__ -> (создание объекта) -> __init__ -> (инициализация объекта).
  • Аналогия: __new__ — это "аллокатор" (выделение памяти), __init__ — "конструктор" (заполнение полей).

9. Инструменты проверки code style

  • pylint: Очень строгий, проверяет не только стиль (PEP8), но и ошибки, "запахи кода", избыточность. Много ложных срабатываний, но дает качественные советы.
    • Плюсы: Всесторонний анализ.
    • Минусы: Медленный, может быть излишне педантичным.
  • flake8: Объединяет pyflakes (логические ошибки), pycodestyle (PEP8) и mccabe (сложность кода). Более легковесный и быстрый, чем pylint.
    • Плюсы: Быстрый, модульный, меньше шума.
    • Минусы: Менее "умный", чем pylint.
  • black: Не линтер, а автоформатер. Сам переписывает код в соответствии со своим строгим стилем (совместимым с PEP8).
    • Плюсы: Убирает споры о стиле, код становится единообразным.
    • Минусы: Не настраивается (кроме длины строки). "Любите или ненавидьте".

10. Тестирование кода. Mocking

  • Как тестирую: Использую pytest (удобнее, чем unittest). Для изоляции тестов — pytest с фикстурами. Для запуска в разных окружениях — tox. CI — GitHub Actions/GitLab CI.
  • Mocking (Подмена): Техника замены реальных объектов в коде на "поддельные" (mock-объекты) во время тестов.
    • Зачем: Чтобы изолировать тестируемый модуль от его зависимостей (база данных, API, файловая система).
    • Пример (ответы на вопросы):
      1. Функция с удаленным API: Замокать библиотеку для запросов (requests, aiohttp) или использовать responses для имитации ответов сервера (включая 404, таймауты).
      2. Функция с долгими вычислениями: Замокать внутреннюю функцию, которая выполняет тяжелую работу, чтобы она возвращала заранее подготовленный результат.
  • Патчинг: Процесс временной подмены атрибута (функции, класса) в указанном пространстве имен с помощью unittest.mock.patch.

11. Структуры данных. Mutable/Immutable

  • Mutable (Изменяемые): list, dict, set, bytearray. Могут меняться после создания.
  • Immutable (Неизменяемые): int, float, str, tuple, bytes, frozenset. Не могут быть изменены после создания (операции создают новый объект).
  • Особые типы из collections: defaultdict, OrderedDict (в Python 3.7+ обычный dict тоже сохраняет порядок), Counter, deque, namedtuple.

12. Хэш-таблица (словарь). Коллизии

Как работает: Словарь Python — это хэш-таблица. При добавлении пары ключ-значение:

  1. Вычисляется хэш ключа (должен быть __hash__).
  2. На основе хэша определяется "корзина" (ячейка).
  3. Если корзина пуста — пара сохраняется. Если нет (коллизия) — Python использует открытую адресацию (пробирование) для поиска следующей свободной корзины.
    Коллизии: Ситуация, когда разные ключи имеют одинаковый хэш. Ухудшают производительность (поиск дольше).
    Борьба: Хорошая хэш-функция (встроенные типы имеют), динамическое увеличение размера таблицы, эффективный алгоритм пробирования.

13. Быстрый поиск: dict, set vs list, tuple

  • Быстрый поиск (по значению/ключу): dict и set. Основаны на хэш-таблицах, средняя сложность O(1).
  • Перебор (поиск по индексу или последовательный): list и tuple. Поиск по значению требует O(n). Но доступ по индексу — O(1).
  • Итог: Для проверки "принадлежности" (x in container) используйте set или dict ключи.

14. Передача аргументов в функцию

Python использует "call by object reference" (вызов по ссылке на объект).

  • Неизменяемые объекты (int, str, tuple): Функция получает ссылку, но не может изменить оригинальный объект. Создается новый.
  • Изменяемые объекты (list, dict): Функция получает ссылку и может изменить содержимое оригинального объекта.

15. Генератор vs Итератор

  • Итератор: Объект с методами __iter__ (возвращает себя) и __next__ (возвращает следующий элемент или вызывает StopIteration). Явное состояние.
  • Генератор: Частный, удобный случай итератора. Функция с ключевым словом yield. При вызове возвращает объект-генератор, который является итератором. Состояние (место выполнения) сохраняется автоматически между вызовами next().
  • Ключевое отличие: Генератор пишется как функция и управляет потоком "лениво", yield возвращает значение и "замораживает" состояние. Итератор требует явного описания класса с __next__.

16. List/Dict Comprehension

Краткий синтаксис для создания коллекций.

# List comprehension
squares = [x**2 for x in range(10) if x % 2 == 0] # [0, 4, 16, 36, 64]
# Dict comprehension
square_dict = {x: x**2 for x in range(5)} # {0:0, 1:1, 2:4, 3:9, 4:16}

17. Introspection, Name Mangling

  • Introspection (Интроспекция): Возможность программы исследовать тип и структуру объектов во время выполнения.
    • dir(obj) — список атрибутов.
    • type(obj), isinstance(obj, Class) — проверка типа.
    • hasattr(obj, 'attr'), getattr(obj, 'attr', default) — работа с атрибутами.
  • Name Mangling (Искажение имен): Механизм для "приватных" атрибутов (__attr). Имя внутри класса преобразуется в _ClassName__attr, чтобы избежать конфликтов в иерархии наследования.

18. Разница между _ и __

  • Одно подчеркивание _var: Условно защищенный атрибут. Сигнал: "внутреннее использование, не трогай снаружи". Также используется для хранения результата последней операции в интерпретаторе.
  • Два подчеркивания __var: Приватный атрибут (с искажением имени). Преобразуется в _ClassName__var. Защищает от случайного переопределения в подклассах.
  • Одно подчеркивание в конце var_: Используется, чтобы избежать конфликта с зарезервированными словами (например, class_).

19. GIL (Global Interpreter Lock)

GIL — это мьютекс (блокировка), который позволяет выполняться только одному потоку Python в один момент времени, даже на многоядерных процессорах.

  • Проблемы: Не позволяет использовать несколько ядер CPU для параллельного выполнения Python-кода в потоках. Потоки полезны только для I/O-задач (ожидание сети, диска).
  • Обход: Использование multiprocessing (отдельные процессы с своим GIL), asyncio для асинхронного I/O, вычисления на C-расширениях (которые могут отпускать GIL, например, numpy).

20. MRO (Method Resolution Order). MRO2 vs MRO3

MRO — порядок, в котором Python ищет методы в иерархии классов.

  • "Проблема ромба": Класс D наследуется от B и C, которые оба наследуются от A. Какой метод A будет вызван при D.method()?
  • Old-style (MRO2, Python 2.x): Использовал алгоритм поиска в глубину (DFS), что могло приводить к неинтуитивному порядку.
  • New-style (C3 Linearization, Python 3.x): Гарантирует, что подкласс всегда идет перед родителем, и сохраняет порядок в списке базовых классов. Доступен через ClassName.__mro__.

21. Old-style vs New-style classes

  • Old-style: Классы, не наследуемые от object (только в Python 2). Имеют устаревший MRO, нет свойств (property), дескрипторов.
  • New-style: Классы, наследуемые от object (явно или неявно в Python 3). Имеют современный MRO (C3), поддержку super(), дескрипторов, слотов.

22. Threading vs Multiprocessing

  • Threading: Многопоточность. Потоки разделяют память (легко общаться). В Python из-за GIL не дают выигрыша в CPU-задачах. Подходят для I/O-задач (много ожидания).
  • Multiprocessing: Многопроцессность. Процессы имеют отдельную память (общение сложнее, через IPC). Позволяют использовать несколько ядер CPU для вычислений, так как у каждого процесса свой GIL.
  • Выбор: CPU-bound задачи -> multiprocessing. I/O-bound задачи -> threading или, лучше, asyncio.

23. Asyncio

Asyncio — библиотека для написания асинхронного (неблокирующего) конкурентного кода с использованием синтаксиса async/await.

  • Особенность: Работает в одном потоке, используя кооперативную многозадачность. Когда корутина (async def) встречает await на операции ввода-вывода (сеть, файлы), она отдает управление циклу событий (event loop), который запускает другую корутину.
  • Преимущество: Эффективная обработка тысяч одновременных I/O-операций (веб-серверы, чаты) без накладных расходов на потоки/процессы.

24. Garbage Collector (GC)

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

  • В Python: Используется подсчет ссылок (основной механизм) + циклический сборщик (дополнительно).
    • Подсчет ссылок: Каждый объект имеет счетчик. При увеличении ссылок +1, при уменьшении -1. При 0 память освобождается мгновенно.
    • Циклический сборщик: Находит и удаляет изолированные циклы ссылок (где счетчики >0, но достичь объекты извне нельзя).
  • Плюсы: Простота для программиста, нет утечек памяти (не считая циклических ссылок с __del__).
  • Минусы: Накладные расходы на подсчет, возможные паузы от циклического сборщика (для реального времени).

25. Зачем нужен Celery?

Celery — это распределенная очередь задач (Distributed Task Queue).

  • Зачем: Для отложенного и фонового выполнения задач вне контекста основного приложения (чаще всего веб-приложения).
  • Основные применения:
    1. Фоновые задачи: Отправка email, обработка изображений, генерация отчетов.
    2. Периодические задачи (cron): Через celery beat.
    3. Распределенная обработка: Множество воркеров на разных машинах могут брать задачи из общей очереди (например, Redis, RabbitMQ).
    4. Масштабирование: Позволяет отделить тяжелые вычисления от основного сервиса.