[1, 2, 3, 4]. Присваивание b = a не копирует список, а создаёт вторую ссылку на тот же объект; append меняет объект, и через a видно то же самое. Независимая копия это list(a) или copy.deepcopy для вложенных структур.
== сравнивает значения, is проверяет, что это один и тот же объект в памяти. Два пустых списка: [] == [] даёт True, [] is [] даёт False, это разные объекты. Два экземпляра класса без __eq__: и ==, и is дадут False, потому что == по умолчанию сравнивает по идентичности; чтобы сравнивать по содержимому, нужно определить __eq__ (и __hash__, если объекты будут ключами). is используется для None, True, False; с числами и строками он иногда даёт True из-за кэша маленьких целых и интернирования, полагаться на это нельзя.
Изменяемые: list, dict, set, bytearray, экземпляры классов. Неизменяемые: int, float, complex, bool, str, bytes, tuple, frozenset, range, None. Кортеж неизменяем, но элементы внутри него могут быть изменяемыми. set хранит уникальные хешируемые элементы без порядка и проверяет принадлежность за O(1); list хранит любые элементы с порядком и дублями, поиск за O(n).
Динамическая и строгая: тип привязан к объекту, а не к имени, и неявных приведений между несовместимыми типами нет ("1" + 1 это TypeError). Аннотации типов не проверяются во время выполнения, их читают редактор, mypy и люди.
Да, сколько угодно: выполнится первый достигнутый, остальной код после него не запускается. Так пишут ранний выход (if not rows: return []). Если ни один return не достигнут, функция возвращает None. Несколько значений возвращаются одним return a, b как кортеж.
Singleton гарантирует, что у класса один экземпляр, и даёт к нему глобальную точку доступа: в Python через переопределение __new__, метакласс или декоратор класса; на практике чаще всего модуль с объектом, созданным при импорте (модули и так загружаются один раз). Отличие от глобальной переменной: экземпляр создаётся лениво при первом обращении, класс контролирует создание и может нести поведение и состояние (соединение с базой, конфиг), а глобальная переменная это просто имя, которое кто угодно может перепривязать. Другие паттерны, которые реально встречаются в пайплайнах: фабрика (создание коннектора по типу источника), стратегия (взаимозаменяемые алгоритмы обработки), адаптер (единый интерфейс к разным API), декоратор.
Основной механизм это счётчик ссылок: объект уничтожается сразу, когда счётчик стал нулём. Циклы ссылок счётчик не ловит, их находит сборщик gc: периодически обходит объекты-контейнеры по трём поколениям и освобождает недостижимые циклы. Запускается автоматически по порогам выделений, вручную через gc.collect().
Все объекты живут в куче, имена и контейнеры хранят ссылки. Для маленьких объектов CPython использует свой аллокатор pymalloc с аренами, поэтому освобождённая память может не вернуться операционной системе. Локальные имена живут в кадре функции и исчезают при return; память освобождается, когда на объект не остаётся ссылок.
Global Interpreter Lock: мьютекс в CPython, который позволяет только одному потоку выполнять байт-код в каждый момент времени. Поэтому потоки не ускоряют вычисления на CPU: два потока считают не быстрее одного. Потоки помогают, когда задача ждёт ввода-вывода (сеть, диск, база): во время ожидания GIL отпускается. Для CPU-нагрузки берут процессы (multiprocessing), библиотеки на C, которые отпускают GIL (NumPy, pandas в части операций), или выносят расчёт в базу и Spark. С Python 3.13 есть экспериментальная сборка без GIL.
Список хранит все элементы сразу, итератор и генератор отдают по одному по запросу и хранят только своё состояние. for line in file держит в памяти одну строку, file.readlines() весь файл. Генератор (x * 2 for x in rows) не создаёт промежуточного списка. Цена ленивости: обойти можно один раз, а ошибка возникает не при создании, а при чтении.
Типичный случай в DE: выкачать тысячи страниц из API или много файлов из S3, где всё время уходит на ожидание сети. asyncio с aiohttp делает это в одном потоке через событийный цикл: пока один запрос ждёт ответа, отправляется следующий; ThreadPoolExecutor даёт то же самое для библиотек без async-версии (requests, драйверы баз). Для обработки самих данных ни то ни другое не помогает из-за GIL: там процессы, Spark или SQL. Честный ответ «применял для параллельной выкачки, обработку делал в базе» звучит лучше, чем выдуманный опыт.
Ключом может быть любой хешируемый объект: тот, у которого есть __hash__, и хеш не меняется за время жизни. Словарь кладёт ключ в ячейку по его хешу; если бы ключ изменился, хеш стал бы другим, и словарь искал бы его не в той ячейке. Поэтому встроенные изменяемые типы (list, dict, set) хеша не имеют, а int, str, tuple из хешируемых, frozenset, bytes, None и функции имеют. Экземпляр MyClass хешируем по умолчанию (по адресу), значит может быть ключом; чтобы два экземпляра с одинаковыми полями считались одним ключом, нужны согласованные __eq__ и __hash__, например через @dataclass(frozen=True).
Словарь и множество это хеш-таблицы: от ключа считается hash(), по нему вычисляется индекс ячейки, при коллизии проверяются соседние по открытой адресации. В среднем поиск, вставка и удаление O(1), в худшем случае (все хеши совпали) O(n). Поиск в списке 5 in nums перебирает элементы по одному: O(n), в среднем половина списка. Поэтому «есть ли ключ среди ста тысяч» делают через set, а не через список.
Список это массив ссылок с запасом по ёмкости. append дописывает в конец: амортизированно O(1), редкое переразмещение массива при переполнении. insert(i, x) сдвигает все элементы правее позиции: O(n), для insert(0, x) сдвигается весь список. Если нужно добавлять в начало, берут collections.deque с O(1) с обоих концов.
Оба это массив ссылок: доступ по индексу O(1), поиск и in O(n), у списка append амортизированно O(1), вставка в начало O(n). Кортеж неизменяем и может быть ключом словаря. Для проверки принадлежности на больших объёмах нужен set с O(1).
def log(*args, **kwargs): args внутри функции это кортеж позиционных аргументов, kwargs словарь именованных. Вызов log(1, "a", table="orders", rows=10) даст args == (1, "a") и kwargs == {"table": "orders", "rows": 10}. Именованные передаются как key=value при вызове или распаковкой готового словаря log(**params); позиционные распаковываются из последовательности log(*values). Имена args и kwargs это соглашение, звёздочки это синтаксис.
По присваиванию: параметр становится ещё одной ссылкой на переданный объект. Изменение объекта на месте (row["x"] = 1, lst.append) видно снаружи, перепривязка параметра (row = {}) нет. Для неизменяемых типов это выглядит как передача по значению, для изменяемых как по ссылке, но механизм один.
Анонимная функция из одного выражения. Уместна как короткий ключ сортировки (sorted(rows, key=lambda r: r["dt"])) или условие в filter, используемые один раз. Несколько инструкций, присваивание и try в ней невозможны; как только логика растёт или нужна повторно, это def с именем. Присваивать lambda имени считается плохим стилем.
Значение по умолчанию вычисляется один раз при определении функции и живёт в объекте функции. def f(x, bucket=[]) даёт один список на все вызовы: элементы накапливаются между вызовами. Идиома: bucket=None и if bucket is None: bucket = [] внутри.
Функция, которая принимает функцию и возвращает новую, обычно обёртку с действиями до и после вызова исходной. @dec над def f эквивалентно f = dec(f). Несколько декораторов применяются снизу вверх (ближайший к функции первым), а при вызове выполняются сверху вниз: @a @b def f это a(b(f)). functools.wraps сохраняет имя и docstring исходной функции.
Объект для конструкции with: при входе вызывается __enter__, при выходе из блока, в том числе по исключению, __exit__. Так гарантируется освобождение ресурса: with open(...) закрывает файл, with conn: коммитит или откатывает транзакцию, with lock: снимает блокировку. Свой пишут классом с __enter__/__exit__ или функцией-генератором с декоратором @contextlib.contextmanager: код до yield это вход, после yield выход, try/finally вокруг yield даёт очистку при ошибке. В пайплайнах контекстный менеджер это стандартный способ не оставить соединение или временный файл открытыми после падения.
Функция с yield при вызове возвращает генератор: итератор, который выполняет тело лениво, по одному элементу на запрос, и сохраняет состояние между запросами. return завершает функцию и уничтожает кадр, yield отдаёт значение и ставит функцию на паузу. Для инженера данных это способ обрабатывать файл больше памяти: читать, преобразовывать и писать по одному элементу, собирая конвейер из маленьких тестируемых шагов.
Итерируемый объект это тот, у которого __iter__ возвращает итератор. Итератор это объект с __next__, который отдаёт элементы по одному и возбуждает StopIteration в конце; обходится только один раз. Цикл for вызывает iter, а потом next до конца. Файл, map, zip, csv.reader возвращают итераторы, список и словарь итерируемы, но не итераторы.
Первый параметр метода, в который Python подставляет экземпляр при вызове obj.method(): obj.method() это Class.method(obj). Через него метод читает и пишет атрибуты именно этого объекта. Имя это соглашение, подстановка это механизм языка.
Доступ к методам следующего класса в порядке MRO, обычно родителя. Используется, чтобы дополнить унаследованный метод, а не переписать его целиком; в __init__ дочернего класса вызов super().__init__() обязателен, иначе атрибуты родителя не будут созданы.
Инкапсуляция, наследование, полиморфизм, абстракция. В Python инкапсуляция держится на соглашении об именах с подчёркиванием, полиморфизм реализуется утиной типизацией (важно наличие метода, а не тип), абстракция через abc или просто через договорённость об интерфейсе. В коде загрузок из четырёх реально работают полиморфизм и абстракция интерфейсов (источник с методом read(), у каждого своя реализация).
Конструкция try / except / else / finally: в try код, который может упасть; except Тип as e обрабатывает указанные типы (сверху вниз, первый подходящий); else выполняется, только если исключения не было; finally всегда, даже при return или непойманном исключении, и нужен для освобождения ресурсов. Исключения это классы с иерархией: BaseException → Exception → ValueError, TypeError, KeyError, OSError (и его потомки FileNotFoundError, ConnectionError), RuntimeError; KeyboardInterrupt и SystemExit наследуют от BaseException, поэтому голый except: ловит и их. Свои исключения наследуют от Exception или от ближайшего по смыслу встроенного; raise ... from e сохраняет цепочку. Правило: ловить самое узкое, что умеете обработать, остальное пусть падает с traceback.
Возбудить исключение из кода задачи: любое непойманное исключение в PythonOperator или @task переводит задачу в failed и запускает retry по настройкам. Для явного смысла есть AirflowFailException (упасть без повторов) и AirflowSkipException (пометить как пропущенную). Главное не ловить исключение и не возвращать None вместо него: тогда задача будет зелёной при сломанных данных.
Два указателя: сравнивать текущие элементы списков, забирать меньший и сдвигать его указатель, в конце дописать остаток того списка, который не кончился. Это шаг merge sort, сложность O(n + m) и O(n + m) памяти под результат. sorted(a + b) даст O((n + m) log(n + m)), хотя в CPython Timsort на двух отсортированных кусках работает почти линейно; на собеседовании ждут именно два указателя. Готовое решение в стандартной библиотеке: heapq.merge(a, b), оно ленивое и подходит для потоков.
Два указателя с краёв: наибольший по модулю элемент всегда на одном из концов, его квадрат идёт в результат последним. Заполнять результат с конца, сдвигая левый или правый указатель, O(n). Наивное sorted(x * x for x in nums) это O(n log n), и интервьюер обычно просит улучшить.
collections.Counter(values).most_common(1)[0][0] за O(n). Руками: словарь счётчиков в один проход, затем ключ с максимальным значением. Уточнить у интервьюера, что делать при нескольких модах: вернуть любую, все или первую встреченную. Решение через max(set(values), key=values.count) короткое, но O(n × k).
Стек: открывающая скобка кладётся в стек, закрывающая должна совпасть с вершиной, иначе последовательность некорректна; в конце стек должен быть пуст. Для нескольких видов скобок словарь пар {")": "(", "]": "[", "}": "{"}. O(n) по времени и памяти. Случаи, которые ждут на собеседовании: пустая строка (корректна), только закрывающая, незакрытая в конце, перекрёст ([)].
Один проход с подсчётом серии: пока следующий символ равен текущему, увеличивать счётчик, при смене символа записать символ и, если счётчик больше 1, число. Можно через itertools.groupby: "".join(ch + (str(n) if n > 1 else "") for ch, n in ((k, len(list(g))) for k, g in groupby(s))). Граничные случаи: пустая строка, один символ, серия длиной 1 без числа по условию.
Один проход со словарём счётчиков: элемент попадает в результат, если его счётчик меньше N, после чего счётчик увеличивается. O(n) времени, O(k) памяти по числу уникальных значений. Это та же схема, что дедупликация с сохранением порядка (N = 1).
6 - 2. Множественное присваивание сначала вычисляет правую часть в кортеж ("6", "2"), потом распаковывает в имена, поэтому обмен происходит без временной переменной. Значения остаются строками, но print разделяет аргументы пробелом, и на выводе это не видно.
(96, 130). filter возвращает ленивый итератор с элементами, для которых функция истинна; tuple() его материализует. Без tuple() напечатался бы объект <filter ...>.
10. Строка итерируема по символам, map(int, ...) превращает каждый символ в число, sum складывает: 1 + 2 + 3 + 4.
5. set оставляет уникальные значения, key=values.count для каждого считает, сколько раз оно встречается в исходном списке, max выбирает самое частое: это мода списка. Сложность O(n × k), где k число уникальных значений, потому что count каждый раз проходит весь список; для больших данных берут collections.Counter(values).most_common(1) за O(n).
[[1, 2], [3, 4]]. items[1] это ссылка на внутренний список, append меняет его на месте, внешний список видит изменение, потому что хранит ту же ссылку.
Ответ с задачей для каждой звучит убедительнее списка: psycopg для загрузок в Postgres и свой драйвер для ClickHouse, pandas для разведки и отчётов, pyarrow для Parquet, PySpark когда данные на кластере, SQLAlchemy как универсальный коннектор, requests/aiohttp для API, boto3 для S3. Главное, что проверяют: понимаете ли вы, где pandas заканчивается и начинается база или Spark.
Зависимости фиксируются в репозитории (requirements.txt с версиями, pyproject.toml с uv или poetry и lock-файлом), а не ставятся руками на сервере. Деплой собирает окружение из этого файла: образ Docker с pip install -r requirements.txt или uv sync, для Airflow пересборка образа воркеров или добавление пакета в образ через CI. Так окружение воспроизводимо, а pip install на проде вручную это первое, что ломает повторяемость.
Разделитель (запятая, точка с запятой, табуляция), наличие заголовка, символ кавычек и экранирования, кодировка (UTF-8, cp1251, BOM), формат дат и чисел (десятичный разделитель, тысячи), признак NULL (пустая строка, \N, NULL), переводы строк внутри полей, размер файла (читать потоком, не целиком). Всё это задаётся явно в csv.reader/pandas.read_csv, а не угадывается, и типы приводятся на входе с ошибкой на плохом значении.