Python

Последовательности

Что такое последовательность

Последовательностью в Python называется итерабельный объект, который поддерживает эффективный доступ к элементам с использованием целочисленных индексов через специальный метод __getitem__() и поддерживает метод __len__(), который возвращает длину последовательности. К основным встроенным типам последовательностей относятся list, tuple, range, str и bytes.

Последовательности также опционально могут реализовывать методы count(), index(), __contains__() и __reversed__() и другие.

Какие операции поддерживают большинство последовательностей

  • x in s, x not in s – находится ли элемент x в последовательности s (для строк и последовательностей байтов – является ли x подстрокой s)
  • s + t – конкатенация последовательностей s и t
  • s * n, n * s – конкатенация n нерекурсивных копий последовательности s
  • s[i] – i-й элемент последовательности s
  • s[:i] - срез всех элементов последовательности s до i (не включая i)
  • s[i:] - срез всех элементов последовательности s от i до последнего элемента
  • s[-i:] - срез последних i элементов последовательности s
  • s[::-1] - перевернуть последовательность
  • s[i:j] – срез последовательности s от i до j (не включая j)
  • s[i:j:k] – срез последовательности s от i до j с шагом k (не включая j)
  • len(s) – длина последовательности s
  • min(s) – минимальный элемент последовательности s
  • max(s) – максимальный элемент последовательности s
  • s.index(x[, i[, j]]) – индекс первого вхождения x (опционально – начиная с позиции i и до позиции j)
  • s.count(x) – общее количество вхождений x в s
  • sum(s) – сумма элементов последовательности

Неизменяемые последовательности обычно реализуют операцию hash(s) – хеш-значение объекта.

Большинство изменяемых последовательностей поддерживают следующие операции:

  • s[i] = x – элемент с индексом i заменяется на x
  • s[i:j] = t, s[i:j:k] = t – элементы с индексами от i до j (с шагом k) заменяются содержимым итерабельного объекта t
  • del s[i:j], del s[i:j:k] – удаление соответствующих элементов из последовательности
  • s.append(x) – добавление x в конец последовательности
  • s.clear() – удаление всех элементов последовательности
  • s.copy() – нерекурсивная копия последовательности
  • s.extend(t) – добавление всех элементов итерабельного объекта в конец последовательности
  • s.insert(i, x) – вставка элемента x по индексу i
  • s.pop(), s.pop(i) – возврат значения по индексу i (по умолчанию – последний элемент) и удаление его из последовательности
  • s.remove(x) – удаление первого вхождения x
  • s.reverse() – разворот последовательности в обратном порядке

Какие виды строк бывают в питоне

Зависит от версии Питона. Во второй ветке два типа: однобайтные строки и Юникод представлены классами str и unicode соответственно. В третьем Питоне есть один вид строк str, который представляет собой Юникод. Однобайтных строк нет, вместо них есть тип bytes, то есть цепочка байт.

Можно ли изменить отдельный символ внутри строки

Нет, строки неизменяемы. Операции замены, форматирования и конкатенации возвращают новую строку.

Как соединить список строк в одну. Как разбить строку на список строк

Чтобы соединить, нужен метод строки .join(). Чтобы разбить, метод .split().

Как кодировать и декодировать строки

Кодировать – перевести Юникод в байтовую строку. Вызвать метод .encode() у строки.

Декодировать – восстановить строку из цепочки байт. Вызвать метод .decode() у объекта str или bytes (версии Питона 2 и 3 соответственно).

В обоих случаях явно передавать кодировку, иначе будет использована та, что определена в системе по умолчанию. Быть готовым поймать исключения UnicodeEncodeError, UnicodeDecodeError.

Чем список отличается от кортежа

Списки – это изменяемые последовательности, обычно используемые для хранения однотипных данных (хотя Python не запрещает хранить в них данные разных типов). Представлены классом list.

Кортежи – это неизменяемые последовательности, обычно используемые, чтобы хранить разнотипные данные. Представлены классом tuple.

На уровне языка отличаются тем, что в кортеж нельзя добавить или убрать элемент. На уровне интерпретатора различий нет. Обе коллекции представлены массивом указателей на структуру PyObject.

Для списка определены функции, которые добавляют в такой массив новый элемент, удаляют имеющийся, соединяют два массива в один. Они вызываются методами списка .append(), .pop(), .sort() и т.д.

Что такое диапазон

Диапазоны – неизменяемые последовательности чисел, которые задаются началом, концом и шагом. Представлены классом range (в Python 2 – xrange; range в Python 2 – это функция, которая возвращает список). Параметры конструктора должны быть целыми числами (либо экземпляры класса int, либо любой объект с методом __index__) Поддерживает все общие для последовательностей операции, кроме конкатенации и повторения, а также, в версиях Python до 3.2, срезов и отрицательных индексов.

Как сделать список уникальным (без повторяющихся элементов)

Вариант со множеством. Не гарантирует порядок элементов. Порядок сохраняется только для маленьких списков.

list(set([1, 2, 2, 2, 3, 3, 1]))
>>> [1, 2, 3]

Вариант с OrderedDict. Гарантирует порядок элементов.

>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys([1, 2, 2, 2, 3, 3, 1]))
[1, 2, 3]

Вариант с циклом. Медленно, но гарантирует порядок. Подходит, если элементы нельзя помещать внутрь множества (например, словари).

res = []
for x in [1, 2, 2, 2, 3, 3, 1]:
    if x not in res:
        res.append(x)
>>> [1, 2, 3]

Есть кортеж из трех элементов. Назначить переменным a, b, c его значения

a, b, c = (1, 2, 3)

Как сравниваются последовательности

Две последовательности равны, если они имеют одинаковый тип, равную длину и соответствующие элементы обоих последовательностей равны.

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

Множества и отображения

Как понять хешируемый ли объект

Объект называется хешируемым, если он имеет хеш-значение (целое число), которое никогда не изменяется на протяжении его жизненного цикла и возвращается методом __hash__(), и может сравниваться с другими объектами (реализует метод __eq__()). Равные хешируемые объекты должны иметь равные хеш-значения. Все стандартные неизменяемые объекты хешируемые. Все стандартные изменяемые объекты не хешируемые.

Что такое множество

Множество – это неупорядоченная коллекция хешируемых объектов, которые не повторяются. В множествах нет понятия позиции элемента. Соответственно, они не поддерживают индексацию и срезы. Встроенные классы множеств: set (изменяемое множество), frozenset (неизменяемое множество).

Для чего применяются множества

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

Какие операции можно производить над множествами

  • set([iterable]), frozenset([iterable]) – создание множества (пустого или из элементов итерабельного объекта)
  • len(s) – количество элементов множества
  • x in s, x not in s – проверка нахождения элемента в множестве
  • s.isdisjoint(t) – проверка того, что данное множество не имеет общих элементов с заданным
  • s.issubset(t), s <= t – проверка того, что все элементы множества s являются элементами множества t
  • s < t – проверка того, что s <= t и s != t
  • s.isuperset(t), s >= t – проверка того, что все элементы множества t являются элементами множества s
  • s > t – проверка того, что s >= t и s != t
  • s.union(t, ...), s | t | ... – создание нового множества, которое является объединением данных множеств
  • s.intersection(t, ...), s & t & ... – создание нового множества, которое является пересечением данных множеств
  • s.difference(t, ...), s - t - ... – создание нового множества, которое является разницей данных множеств
  • s.symmetric_difference(t), s ^ t – создание нового множества, которое является симметрической разницей данных множеств (то есть, разница объединения и пересечения множеств)
  • s.copy() – неполная копия множества s

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

Операции над изменяемыми множествами:

  • s.update(t, ...), s |= t | ... – добавить в данное множество элементы из других множеств
  • s.intersection_update(t, ...), s &= t & ... – оставить в данном множестве только те элементы, которые есть и в других множествах
  • s.difference_update(t, ...), s -= t | ... – удалить из данного множества те элементы, которые есть в других множествах
  • s.symmetric_difference_update(t), s ^= t – оставить или добавить в s элементы, которые есть либо в s, либо в t, но не в обоих множествах
  • s.add(element) – добавить новый элемент в множество
  • s.remove(element) – удалить элемент из множества; если такого элемента нет, возникает исключение KeyError
  • s.discard(element) – удалить элемент из множества, если он в нём находится
  • s.pop() – удалить из множества и вернуть произвольный элемент; если множество пустое, возникает исключение KeyError
  • s.clear() – удалить все элементы множества.

Как происходит проверка множеств на равенство

Проверка множеств на равенство происходит поэлементно, независимо от типов множеств.

Что такое отображение

Отображение (mapping) – это объект-контейнер, который поддерживает произвольный доступ к элементам по ключам и описывает все методы, описанные в абстрактном базовом классе collections.Mapping (get(), items(), keys(), values()) или collections.MutableMapping (clear(), get(), items(), keys(), pop(), popitem(), setdefault(), update(), values()). К отображениям относятся классы dict, collections.defaultdict, collections.OrderedDict и collections.Counter.

Какие нюансы есть в использовании чисел как ключей

Числовые ключи в словарях подчиняются правилам сравнения чисел. Таким образом, int(1) и float(1.0) считаются одинаковым ключом. Однако из-за того, что значения типа float сохраняются приближенно, не рекомендуется использовать их в качестве ключей.

>>> {True: 'yes', 1: 'no', 1.0: 'maybe'}
{True: 'maybe'}

Какие операции можно производить над отображениями

  • len(d) – количество элементов.
  • d[key] – получение значения с ключом key. Если такой ключ не существует и отображение реализует специальный метод __missing__(self, key), то он вызывается. Если ключ не существует и метод __missing__ не определён, выбрасывается исключение KeyError.
  • d[key] = value – изменить значение или создать новую пару ключ-значение, если ключ не существует.
  • key in d, key not in d – проверка наличия ключа в отображении.
  • iter(d) – то же самое, что iter(d.keys()).
  • clear() – удалить все элементы словаря.
  • copy() – создать неполную копию словаря.
  • (метод класса) dict.fromkeys(sequence[, value]) – создаёт новый словарь с ключами из последовательности sequence и заданным значением (по умолчанию – None).
  • d.get(key[, default]) – безопасное получение значения по ключу (никогда не выбрасывает KeyError). Если ключ не найден, возвращается значение default (по-умолчанию – None).
  • d.items() – в Python 3 возвращает объект представления словаря, соответствующий парам (двухэлементным кортежам) вида (ключ, значение). В Python 2 возвращает соответствующий список, а метод iteritems() возвращает итератор. Аналогичный метод в Python 2.7 – viewitems().
  • d.keys() – в Python 3 возвращает объект представления словаря, соответствующий ключам словаря. В Python 2 возвращает соответствующий список, а метод iterkeys() возвращает итератор. Аналогичный метод в Python 2.7 – viewkeys().
  • d.pop(key[, default]) – если ключ key существует, удаляет элемент из словаря и возвращает его значение. Если ключ не существует и задано значение default, возвращается данное значение, иначе выбрасывается исключение KeyError.
  • d.popitem() – удаляет произвольную пару ключ-значение и возвращает её. Если словарь пустой, возникает исключение KeyError. Метод полезен для алгоритмов, которые обходят словарь, удаляя уже обработанные значения (например, определённые алгоритмы, связанные с теорией графов).
  • d.setdefault(key[, default]) – если ключ key существует, возвращает соответствующее значение. Иначе создаёт элемент с ключом key и значением default. default по умолчанию равен None.
  • d.update(mapping) – принимает либо другой словарь или отображение, либо итерабельный объект, состоящий из итерабельных объектов – пар ключ-значение, либо именованные аргументы. Добавляет соответствующие элементы в словарь, перезаписывая элементы с существующими ключами.
  • d.values() – в Python 3 возвращает объект представления словаря, соответствующий значениям. В Python 2 возвращает соответствующий список, а метод itervalues() возвращает итератор. Аналогичный метод в Python 2.7 – viewvalues().

Что возвращает метод items

Объекты, возвращаемые методами items(), keys() и values() (viewitems(), viewkeys(), viewvalues() в Python 2.7) – это объекты представления словаря. Они предоставляют динамическое представление элементов словаря, то есть изменения данного словаря автоматически отображаются и на этих объектах.

Операции с представлениями словарей:

  • iter(dictview) – получение итератора по ключам, значениям или парам ключей и значений. Все представления словарей при итерировании возвращают элементы словаря в одинаковом порядке. При попытке изменить словарь во время итерирования может возникнуть исключение RuntimeError
  • len(dictview) – количество элементов в словаре.
  • x in dictview – проверка существования ключа, значения или пары ключ-значение в словаре.

Как отсортировать список словарей по определенному полю

Метод списка .sort() и встроенная функция sorted() принимают параметр key. Им должен быть вызываемый объект, который принимает очередной элемент (в нашем случае словарь) и возвращает значение-критерий сортировки.

Код ниже показывает, как отсортировать список людей по возрасту:

users = [{'age': 30}, {'age': 20}, {'age': 10}]
users.sort(key=lambda user: user['age'])
>>> [{'age': 10}, {'age': 20}, {'age': 30}]

Что может являться ключом словаря. Что не может. Почему

Ключом словаря может быть любой хешируемый неизменяемый объект: число, строка, datetime, функция и даже модуль. Такие объекты имеют метод __hash__(), который однозначно сопоставляет объект с некоторым числом. По этому числу словарь ищет значение для ключа.

Списки, словари и множества изменяемы и не имеют метода хеширования. При подстановке их в словарь возникнет ошибка.

Хеш кортежа вычисляется рекурсивно по всем элементам. Так, кортеж

(1, (True, (42, ('hello', )))) состоит только из неизменяемых элементов, поэтому может быть ключом. Однако, такой кортеж

(1, (True, (42, ({'hello': 'world'}, )))) содержит глубоко внутри словарь, поэтому хеш не может быть рассчитан.

Есть два списка – ключи и значения. Как составить из них словарь

keys = ['foo', 'bar', 'baz']
vals = [1, 2, 3]
dict(zip(keys, vals))
>>> {'baz': 3, 'foo': 1, 'bar': 2}

Функция zip отдает список пар N-ых элементов. Конструктор dict принимает список пар. Каждую пару он рассматривает как ключ и значение соответственно.

Как работает хэш-таблица

Хэш-таблица это разреженный массив (массив, в котором имеются незаполненные позиции). В стандартных англоязычных учебниках ячейки хэш-таблицы называются “bucket”. В хэш-таблице dict каждому элементу соотвествует ячейка, содержащая два поля: ссылку на ключ и ссылку на значение элемента. Поскольку размер всех ячеек одинаков, доступ к отдельной ячейке производится по смещению.

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

Для помещения элемента в хэш-таблицу нужно первым делом вычислить хэш-значение ключа элемента. Это делает встроенная функция hash().

Для выборки значения с помощью выражения my_dict[search_key] Python обращается к функции hash(search_key), чтобы получить хэш-значение search_key, и использует несколько младших битов полученного числа как смещение ячейки относительно начала хэш-таблицы (сколько именно битов зависит от текущего размера таблицы). Если найденная ячейка пуста, возбуждается исключение KeyError. В противном случае в найденной ячейке есть какой-то элемент - пара ключ:значение - и тогда Python проверяет, верно ли то, что search_key == found_key. Если да, то элемент найден и возвращается found_value. Если же search_key и found_key не совпали, то имеет место коллизия хэширования. Для разрешения коллизии алгоритм берет различные биты хэш-значения, производит над ними определенные действия и использует результат как смещение другой ячейки.

Что такое коллизия

Когда хеш-функция возвращает один и тот же ответ для разных данных.

Где будет быстрее поиск, а где перебор и почему: dict, list, set, tuple

Поиск будет быстрее в dict и set, потому что это хэш-таблицы, доступ к элементу которых выполняется за O(1). Для list и tuple поиск будет выполняться в среднем за O(n).

Исключение работает только для очень маленьких списков длиной до 5 элементов. В этом случае интерпретатору будет быстрей пробежаться по списку, чем считать хеш.

В Python 2 методы словаря keys, values, items возвращают список. Тоесть перед итерацией по словарю (или сету) интерпретатор сначала создает новый список, что занимает дополнительное время и память, но после создания это уже обыкновенный список. Тоесть в Python 2 итерация по словарям и сетам выполняется дольше, за счет создания нового списка и копирования в него элементов.

В Python 3 эти методы создают объект-представление. Это определенно происходит быстрее чем создание нового списка в Python2. Но итерирование по такому представлению должно происходить немного дольше, чем по списку из-за того что данные в словарях хранятся разреженно (редко, негусто). В подтверждение вышесказанного (Python 3):

>>> l = list(range(1000000))
>>> d = dict.fromkeys(l)
>>> s = set(l)
>>> def iter_list():
...     for i in l:
...         pass
...
>>> def iter_dict():
...     for i in d:
...         pass
...
>>> def iter_set():
...     for i in s:
...         pass
...
>>> timeit.timeit(iter_list, number=1000)
 6.727667486004066
>>> timeit.timeit(iter_dict, number=1000)
 9.293120226997416
>>> timeit.timeit(iter_set, number=1000)
 8.627948219014797

Функции

Что такое args, kwargs. В каких случаях они требуются

Выражения *args и **kwargs объявляют в сигнатуре функции. Они означают, что внутри функции будут доступны переменные с именами args и kwargs (без звездочек). Можно использовать другие имена, но это считается дурным тоном.

args – это кортеж, который накапливает позиционные аргументы. kwargs – словарь именованных аргументов, где ключ – имя параметра, значение – значение параметра.

Важно: если в функцию не передано никаких параметров, переменные будут соответственно равны пустому кортежу и пустому словарю, а не None.

Пожалуйста, не путайте кортеж со списком. Следующий вопрос объясняет, почему.

Почему использовать изменяемые объекты как параметры по-умолчанию плохо. Приведите пример плохого случая. Как исправить

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

В нашем примере bar равен пустому списку. Список – изменяемая коллекция, поэтому значение bar может изменяться от вызова к вызову. Пример:

def foo(bar=[]):
    bar.append(1)
    return bar
foo()
>>> [1]
foo()
[1, 1]
foo()
>>> [1, 1, 1]

Хорошим тоном считается указывать параметру пустое неизменяемое значение, например 0, None, '', False. В теле функции проверять на заполненность и создавать новую коллекцию:

def foo(bar=None):
    if bar is None:
        bar = []
    bar.append(1)
    return bar
foo()
>>> [1]
foo()
>>> [1]
foo()
>>> [1]

Сказанное выше актуально в т.ч. для множеств и словарей.

Можно ли передавать функцию в качестве аргумента другой функции

Можно, функция в Питоне объект первого порядка: допускает присваивание, передачу в функцию, удаление.

Можно ли объявлять функцию внутри другой функции. Где она будет видна

Можно. Такая функция будет видна только внутри первой функции.

Что такое лямбды. Каковы их особенности

Это анонимные функции. Они не резервируют имени в пространстве имен. Лямбды часто передают в функции map, reduce, filter.

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

Использовать точку с запятой для разделения операторов нельзя.

Допустимы ли следующие выражения

  • nope = lambda: pass
  • riser = lambda x: raise Exception(x)

Нет, при загрузке модуля выскочит исключение SyntaxError. В теле лямбды может быть только выражение. pass и raise являются операторами.

Как передаются значения аргументов в функцию или метод

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

В python отказались от подобного механизма, заменой служит механизм связывания (assignment) имени переменной с объектом, например при создании переменной: var = "john"

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

Что такое замыкание

Синтаксически это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней.

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

Что такое контейнер

Контейнер – это тип данных, который инкапсулирует в себе значения других типов. Списки, кортежи, множества, словари и т.д. являются контейнерами.

Что такое итерабельный объект

Итерабельный объект (в оригинальной терминологии – «iterable») – это объект, который может возвращать значения по одному за раз. Примеры: все контейнеры и последовательности (списки, строки и т.д.), файлы, а также экземпляры любых классов, в которых определён метод __iter__() или __getitem__(). Итерабельные объекты могут быть использованы внутри цикла for, а также во многих других случаях, когда ожидается последовательность (функции sum(), zip(), map() и т.д.).

Подробнее:

Рассмотрим итерируемый объект (Iterable). В стандартной библиотеке он объявлен как абстрактный класс collections.abc.Iterable:

class Iterable(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __iter__(self):
        while False:
            yield None

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterable:
            return _check_methods(C, "__iter__")
        return NotImplemented

У него есть абстрактный метод __iter__ который должен вернуть объект итератора. И метод __subclasshook__ который проверяет наличие у класса метод __iter__. Таким образом, получается, что итерируемый объект это любой объект который реализует метод __iter__

class SomeIterable1(collections.abc.Iterable):
    def __iter__(self):
        pass

class SomeIterable2:
    def __iter__(self):
        pass

print(isinstance(SomeIterable1(), collections.abc.Iterable))
# True
print(isinstance(SomeIterable2(), collections.abc.Iterable))
# True

Но есть один момент, это функция iter(). Именно эту функцией использует например цикл for для получения итератора. Функция iter() в первую очередь для получения итератора из объекта, вызывает его метод __iter__. Если метод не реализован, то она проверяет наличие метода __getitem__ и если он реализован, то на его основе создается итератор. __getitem__ должен принимать индекс с нуля. Если не реализован ни один из этих методов, тогда будет вызвано исключение TypeError.

from string import ascii_letters

class SomeIterable3:
    def __getitem__(self, key):
        return ascii_letters[key]

for item in SomeIterable3():
    print(item)

Что такое итератор

Итератор (iterator) – это объект, который представляет поток данных. Повторяемый вызов метода __next__() (next() в Python 2) итератора или передача его встроенной функции next() возвращает последующие элементы потока.

Если больше не осталось данных, выбрасывается исключение StopIteration. После этого итератор исчерпан и любые последующие вызовы его метода __next__() снова генерируют исключение StopIteration.

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

Подробнее:

Итераторы представлены абстрактным классом collections.abc.Iterator:

class Iterator(Iterable):

    __slots__ = ()

    @abstractmethod
    def __next__(self):
        'Return the next item from the iterator. When exhausted, raise StopIteration'
        raise StopIteration

    def __iter__(self):
        return self

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Iterator:
            return _check_methods(C, '__iter__', '__next__')
        return NotImplemented

__next__ Возвращает следующий доступный элемент и вызывает исключение StopIteration, когда элементов не осталось. __iter__ Возвращает self. Это позволяет использовать итератор там, где ожидается итерируемых объект, например for. __subclasshook__ Проверяет наличие у класса метода __iter__ и __next__

Что такое генератор

В зависимости от контекста, может означать либо функцию-генератор, либо итератор генератора (чаще всего, последнее). Методы __iter__ и __next__ у генераторов создаются автоматически.

С точки зрения реализации, генератор в Python — это языковая конструкция, которую можно реализовать двумя способами: как функция с ключевым словом yield или как генераторное выражение. В результате вызова функции или вычисления выражения, получаем объект-генератор типа types.GeneratorType. Канонический пример - генератор, порождающий последовательность чисел Фибоначчи, которая, будучи бесконечна, не смогла бы поместиться ни в одну коллекцию. Иногда термин применяется для самой генераторной функции, а не только объекта, возвращенного ей в качестве результата.

Так как в объекте-генераторе определены методы __next__ и __iter__, то есть реализован протокол итератора, с этой точки зрения, в Python любой генератор является итератором.

Когда выполнение функции-генераторы завершается (при помощи ключевого слова return или достижения конца функции), возникает исключение StopIteration.

Что такое генераторная функция

Генераторная функция - функция, в теле которой встречается ключевое слово yield. Будучи вызвана, такая функция возвращает объект-генератор (generator object) (итератор генератора (generator iterator)).

Что делает yield

yield замораживает состояние функции-генератора и возвращает текущее значение. После следующего вызова __next__() функция-генератор продолжает своё выполнение с того места, где она была приостановлена.

В чем отличие [x for x in y] от (x for x in y)

Первое выражение возвращает список (списковое включение), второе – генератор.

Что особенного в генераторе

Генератор хранит в памяти не все элементы, а только внутреннее состояние для вычисления очередного элемента. На каждом шаге можно вычислить только следующий элемент, но не предыдущий. Пройти генератор в цикле можно только один раз.

Как объявить генератор

  • использовать синтаксис (x for x in seq)
  • оператор yield в теле функции вместо return
  • встроенная функция iter, которая вызывает у объекта метод __iter__(). Этот метод должен возвращать генератор.

Как получить из генератора список

Передать его в конструктор списка: list(x for x in some_seq). Важно, что после этого по генератору уже нельзя будет итерироваться.

Что такое подгенератор

В Python 3 существуют так называемые подгенераторы (subgenerators). Если в функции-генераторе встречается пара ключевых слов yield from, после которых следует объект-генератор, то данный генератор делегирует доступ к подгенератору, пока он не завершится (не закончатся его значения), после чего продолжает своё исполнение.

На самом деле yield является выражением. Оно может принимать значения, которые отправляются в генератор. Если в генератор не отправляются значения, результат данного выражения равен None.

yield from также является выражением. Его результатом является то значение, которое подгенератор возвращает в исключении StopIteration (для этого значение возвращается при помощи ключевого слова return).

Какие методы есть у генераторов

  • __next__() – начинает или продолжает исполнение функции-генератора. Результат текущего yield-выражения будет равен None. Выполнение затем продолжается до следующего yield-выражения, которое передаёт значение туда, где был вызван __next__. Если генератор завершается без возврата значения при помощи yield, возникает исключение StopIteration. Метод обычно вызывается неявно, то есть циклом for или встроенной функцией next().
  • send(value) – продолжает выполнение и отправляет значение в функцию-генератор. Аргумент value становится значением текущего yield-выражения. Метод send() возвращает следующее значение, возвращённое генератором, или выбрасывает исключение StopIteration, если генератор завершается без возврата значения. Если send() используется для запуска генератора, то единственным допустимым значением является None, так как ещё не было выполнено ни одно yield-выражение, которому можно присвоить это значение.
  • throw(type[, value[, traceback]]) – выбрасывает исключение типа type в месте, где был приостановлен генератор, и возвращает следующее значение генератора (или выбрасывает StopIteration). Если генератор не обрабатывает данное исключение (или выбрасывает другое исключение), то оно выбрасывается в месте вызова.
  • close() – выбрасывает исключение GeneratorExit в месте, где был приостановлен генератор. Если генератор выбрасывает StopIteration (путём нормального завершения или по причине того, что он уже закрыт) или GeneratorExit (путём отсутствия обработки данного исключения), close просто возвращается к месту вызова. Если же генератор возвращает очередное значение, выбрасывается исключение RuntimeError. Метод close() ничего не делает, если генератор уже завершён.

Можно ли извлечь элемент генератора по индексу

Нет, будет ошибка. Генератор не поддерживает метод __getitem__.

Что возвращает итерация по словарю

Ключ. Порядок следования ключей не гарантируется (в 3.6 гарантируется неофициально, в 3.7 гарантируется). Для маленьких словарей порядок будет тот же, что и в объявлении. Для больших порядок зависит от расположения элементов в памяти. Особый класс OrderedDict учитывает порядок добавления ключей.

for key in {'foo': 1, 'bar': 2}:
    process_key(key)

Как итерировать словарь по парам ключ-значение

Метод словаря .items() возвращает генератор кортежей (key, value).

Что такое сопрограмма

Сопрограмма (англ. coroutine) — компонент программы, обобщающий понятие подпрограммы, который дополнительно поддерживает множество входных точек (а не одну, как подпрограмма) и остановку и продолжение выполнения с сохранением определённого положения. Расширенные возможности генераторов в Python (выражения yield и yield from, отправка значений в генераторы) используются для реализации сопрограмм. Сопрограммы полезны для реализации асинхронных неблокирующих операций и кооперативной многозадачности в одном потоке без использования функций обратного вызова (callback-функций) и написания асинхронного кода в синхронном стиле. Python 3.5 включает в себе поддержку сопрограмм на уровне языка. Для этого используются ключевые слова async и await.

Классы, объекты

Как получить список атрибутов объекта

Функция dir возвращает список строк – полей объекта. Поле __dict__ содержит словарь вида {поле -> значение}.

Что такое магические методы, для чего нужны

Магическими метода называют методы, имена которых начинаются и заканчиваются двойным подчеркиванием. Магические они потому, что почти никогда не вызываются явно. Их вызывают встроенные функции или синтаксические конструкции. Например, функция len() вызывает метод __len__() переданного объекта. Метод __add__(self, other) вызывается автоматически при сложении оператором +.

Перечислим некоторые магические методы:

  • __init__: инициализатор класса
  • __add__: сложение с другим объектом
  • __eq__: проверка на равенство с другим объектом
  • __iter__: возвращает итератор

Как в классе сослаться на родительский класс

Функция super принимает класс и экземпляр:

class NextClass(FirstClass):
    def __init__(self, x):
        super(NextClass, self).__init__()
        self.x = x

Возможно ли множественное наследование

Да, можно указать более одного родителя в классе потомка.

Что такое MRO

MRO – method resolution order, порядок разрешения методов. Алгоритм, по которому следует искать метод в случае, если у класса два и более родителей.

В классических классах поиск при наследовании по ссылкам на имена осуществляется в следующем порядке:

  1. Сначала экземпляр
  2. Затем его класс
  3. Далее все суперклассы его класса с обходом сначала с глубину, а затем слева направо

Используется первое обнаруженное вхождение. Такой порядок называется DFLR (Обход вглубину и слева направо).

При наследовании классов нового стиля применяется правило MRO (порядок разрешения методов), т.е линеаризованный обход дерева классов, причем вложенный элемент наследования становится доступным в атрибуте __mro__ данного класса. Такой алгорим называется C3-линеаризация. Наследование по правилу MRO осуществляется приблизительно в следующем порядке.

  1. Перечисление всех классов, наследуемых экземпляром, по правилу поиска DFLR для классических классов, причем класс включается в результат поиска столько раз, сколько он встречается при обходе.
  2. Просмотр в полученном списке дубликатов классов, из которых удаляются все, кроме последнего (крайнего справа) дубликата в списке.

Упорядочение по правилу MRO применяется при наследовании и вызове встроенной функции super(), которая всегда вызывает следующий по правилу MRO класс (относительно точки вызова).

Пример наследования в неромбовидных иерархаических деревьях:

class D:          attr = 3      #  D:3   E:2
class B(D)        pass          #   |     |
class E:          attr = 2      #   B    C:1
class C(E):       attr = 1      #    /   /
class A(B, C):    pass          #      A
X = A()                         #      |
print(X.attr)                   #      X

# DFLR = [X, A, B, D, C, E]
# MRO = [X, A, B, D, C, E, object]
# И в версии 3.х и в версии 2.х (всегда) выводит строку "3"

Пример наследования в ромбовидных иерархаических деревьях:

class D:          attr = 3      #  D:3   D:3
class B(D)        pass          #   |     |
class C(D):       attr = 1      #   B    C:1
class A(B, C):    pass          #    /   /
X = A()                         #      A
print(X.attr)                   #      |
                                #      X

# DFLR = [X, A, B, D, C, D]
# MRO = [X, A, B, C, D, object] (сохраняет только последний дубликат D)
# Выводит строку "1" в версии 3.х, строку "3" в версии 2.х  ("1" если D(object))

Что такое Diamond problem

При ромбовидном наследовании определить метод какого класса должен быть вызван

Что такое миксины

Миксин (mix-in, анг. “примесь”), паттерн проектирования в ООП, когда в цепочку наследования добавляется небольшой класс-помощник. Например, есть класс

class NowMixin(object):
    def now():
        return datetime.datetime.utcnow()

Тогда любой класс, наследованный с этим миксином, будет иметь метод now().

В названия миксинов принято добавлять слово Mixin, так как не существует никакого механизма для понимания полноценный это класс или миксин. Миксин технически является самым обычным классом.

Что такое контекстный менеджер. Как написать свой

В питоне есть оператор with. Размещенный внутри код выполняется с особенностью: до и после гарантированно срабатывают события входа в блок withи выхода из него. Объект, который определяет эту логику, называется контекстным менеджером.

События входа и выхода из блока определены методами __enter__ и __exit__. Первый срабатывает в тот момент, когда ход исполнения программы переходит внутрь with. Метод может вернуть значение. Оно будет доступно низлежащему внутри блока with коду.

__exit__ срабатывает в момент выхода из блока, в т.ч. и по причине исключения. В этом случае в метод будет передана тройка значений (exc_class, exc_instance, traceback).

Самый распространённый контекстный менеджер – класс, порожденный функцией open. Он гарантирует, что файл будет закрыт даже в том случае, если внутри блока возникнет ошибка.

Нужно стараться выходить из контекстного менеджера как можно быстрее, чтобы освобождать контекст и ресурсы.

with open('file.txt') as f:
    data = f.read()
process_data(data)

Пример реализации своего контекстного менеджера на основе класса:

class Printable:
    def __enter__(self):
        print('enter')

    def __exit__(self, type, value, traceback):
        print('exit')

Пример реализации своего контекстного менеджера с использованием встроенной библиотеки contextlib:

from contextlib import contextmanager

@contextmanager
def printable():
    print('enter')
    try:
      yield
    finally:
      print('exit')

Контекстные менеджеры также можно использовать для временной замены параметров, переменных окружения, транзакций БД.

Прокомментировать выражение

object() == object()

Всегда ложь, поскольку по умолчанию объекты сравниваются по полю id (адрес в памяти), если только не переопределен метод __eq__.

Что такое __slots__. Плюсы, минусы

Классы хранят поля и их значения в секретном словаре __dict__. Поскольку словарь – изменяемая структура, вы можете на лету добавлять и удалять из класса поля. Параметр __slots__ в классе жестко фиксирует набор полей класса. Слоты используются когда у класса может быть очень много полей, например, в некоторых ORM, либо когда критична производительность, потому что доступ к слоту срабатывает быстрее, чем поиск в словаре, или когда в процессе выполнения программы создаются миллионы экземпляров класса, применение __slots__ позволит сэкономить память.

Слоты активно используются в библиотеках requests и falcon.

Недостатотк: нельзя присвоить классу поле, которого нет в слотах. Не работают методы __getattr__ и __setattr__. Решение: включить в __slots__ элемент __dict__

В чем смысл параметров _value, __value

Поле класса с одним лидирующим подчеркиванием говорит о том, что параметр используется только внутри класса. При этом он доступен для обращения извне. Это ограничение доступа только на уровне соглашения.

class Foo(object):
    def __init__(self):
        self._bar = 42

Foo()._bar
>>> 42

Современные IDE вроде PyCharm подсвечивают обращение к полю с подчеркиванием, но ошибки в процессе исполнения не будет.

Поля с двойным подчеркиванием доступны внутри класса, но недоступны извне. Это достигается хитрым приемом: интерпретатор назначает таким полям имена вида _<ClassName>__<fieldName>. Описанный механизм называется name mangling или name decoration

class Foo(object):
    def __init__(self):
        self.__bar = 42

Foo().__bar
>>> AttributeError: 'Foo' object has no attribute '__bar'
Foo()._Foo__bar
>>> 42

Что такое __new__. И чем он отличается от __init__. В какой последовательности они выполняются

Основное различие между этими двумя методами состоит в том, что __new__ обрабатывает создание объекта, а __init__ обрабатывает его инициализацию.

__new__ вызывается автоматически при вызове имени класса (при создании экземпляра), тогда как __init__ вызывается каждый раз, когда экземпляр класса возвращается __new__, передавая возвращаемый экземпляр в __init__ в качестве параметра self, поэтому даже если вы сохранили экземпляр где-нибудь глобально/статически и возвращали его каждый раз из __new__, для него все-равно будет каждый раз вызываться __init__.

Из вышесказанного вытекает что сначала вызывается __new__, а потом __init__

Что такое и чем отличается old-style от new-style classes

Классы нового стиля (3.х доступны только они, в 2.х при наследовании от object) отличаются от классических (по умолчанию в 2.х) следующими особенностями:

  • Причиной создания new style classes послужила идея убрать отличие встроенных типов от определённых пользователем типов. Unifying types and classes in Python 2.2
  • Ромбовидные шаблоны множественного наследования имеют несколько иной порядок поиска. Поиск в них осуществляется скорее в ширину, чем в глубину, перед тем как начать обход снизу вверх (см. вопрос про MRO)
  • Классы теперь обозначают типы, а типы являются классами. Так, в результате вызова встроенной функции type(I) возвращается класс, из которого получается экземпляр, а не тип экземпляра, что, как правило, равнозначно выражению I.__class__. От класса type могут быть произведены подклассы для создания специальных классов. Все классы наследуют от встроенного класса object, предоставляющего по умолчанию небольшой набор методов

Что такое утиная типизация

Неявная типизация, латентная типизация или утиная типизация (англ. Duck typing) – вид динамической типизации, применяемой в некоторых языках программирования (Perl, Smalltalk, Python, Objective-C, Ruby, JavaScript, Groovy, ColdFusion, Boo, Lua, Go, C#), когда границы использования объекта определяются его текущим набором методов и свойств, в противоположность наследованию от определённого класса. То есть считается, что объект реализует интерфейс, если он содержит все методы этого интерфейса, независимо от связей в иерархии наследования и принадлежности к какому-либо конкретному классу.

Утиная типизация решает такие проблемы иерархической типизации, как:

  • невозможность явно указать (путём наследования) на совместимость интерфейса со всеми настоящими и будущими интерфейсами, с которыми он идейно совместим;
  • экспоненциальное увеличение числа связей в иерархии типов при хотя бы частичной попытке это сделать.

Модули, пакеты

Что такое модуль

Модуль – функционально законченный фрагмент программы, оформленный в виде отдельного файла с исходным кодом или поименованной непрерывной её части. Модули позволяют разбивать сложные задачи на более мелкие в соответствии с принципом модульности. Файл, который содержит исходный код на языке Python, является модулем. Модули могут объединяться в пакеты и, далее, в библиотеки.

Как можно получить имя модуля

Название модуля доступно в его глобальной переменной __name__. Если модуль не импортирован, а запущен как скрипт, то __name__ устанавливается в значение "__main__".

Что такое модульное программирование

Модульное программирование – это организация программы как совокупности небольших независимых блоков, называемых модулями, структура и поведение которых подчиняются определенным правилам. Использование модульного программирования позволяет упростить тестирование программы и обнаружение ошибок. Аппаратно-зависимые подзадачи могут быть строго отделены от других подзадач, что улучшает мобильность создаваемых программ.

Как Python ищет модули при импорте

При импортировании модулей интерпретатор Python ищет их в директориях и архивах, список которых доступен как для чтения, так и для модификации в виде переменной path встроенного модуля sys. По умолчанию sys.path состоит из директории с запускаемым скриптом, содержимого переменной окружения PYTHONPATH и стандартного расположения модулей, специфичного для конкретной платформы и интерпретатора.

Что такое пакет

Модули могут объединяться в пакеты. Пакеты служат как пространства имён для модулей и способ их структурирования. Любой пакет является модулем, но не каждый модуль является пакетом. Как правило, модули представляются в виде файлов, а пакеты – каталогов в файловой системе (но не всегда). Для того, чтобы каталог был пакетом, в нём должен находиться файл __init__.py. Он автоматически выполняется при импортировании соответствующего модуля и может содержать определённые действия для инициализации или быть пустым.

Что вы можете сказать о конструкции import package.item

При использовании оператора from package import item, item может быть пакетом, модулем или любым именем, описанным в пакете. При использовании оператора import package.item, item должен быть модулем или пакетом.

Исключения

Что такое обработка исключений

Обработка исключительных ситуаций или обработка исключений (англ. exception handling) — механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при выполнении программы и приводят к невозможности (бессмысленности) дальнейшей отработки программой её базового алгоритма.

Код на Python может сгенерировать исключение при помощи ключевого слова raise. После него указывается объект исключения. Также можно указать класс исключения, в таком случае будет автоматически вызван конструктор без параметров. raise может выбрасывать в качестве исключений только экземпляры класса BaseException и его наследников, а также (в Python 2) экземпляры классов старого типа.

Для чего могут применять конструкцию try finally без except

try:
    # some code
finally:
    # some code

Если в блоке try произойдет ошибка, то блок finally все-равно будет выполнен и внутри него можно будет сделать “cleanup”, например.

Как правильно по-разному обрабатывать исключения

Блоки except обрабатываются сверху вниз и управление передаётся не больше, чем одному обработчику. Поэтому при необходимости по-разному обрабатывать исключения, находящиеся в иерархии наследования, сначала нужно указывать обработчики менее общих исключений, а затем – более общих. Также именно поэтому bare except может быть только последним (иначе SyntaxError). Причём если сначала расположить обработчики более общих исключений, то обработчики менее общих будут просто проигнорированы.

Что будет если ошибку не обработает блок except

Если ни один из заданных блоков except не перехватывает возникнувшее исключение, то оно будет перехвачено ближайшим внешним блоков try/except, в котором есть соответствующий обработчик. Если же программа не перехватывает исключение вообще, то интерпретатор завершает выполнение программы и выводит информацию об исключении в стандартный поток ошибок sys.stderr. Из этого правила есть два исключения:

  • Если исключение возникло в деструкторе объекта, выполнение программы не завершается, а в стандартный поток ошибок выводится предупреждение “Exception ignored” с информацией об исключении.
  • При возникновении исключения SystemExit происходит только завершение программы без вывода информации об исключении на экран (не касается предыдущего пункта, в деструкторе поведение данного исключения будет таким же, как и остальных).

Что делать если нужно перехватить исключение, выполнить действия и опять возбудить это же исключение

Для того, чтобы в обработчике исключения выполнить определённые действия, а затем передать исключение дальше, на один уровень обработчиков выше (то есть, выбросить то же самое исключение ещё раз), используется ключевое слово raise без параметров.

try:
    1 / 0
except ZeroDivisionError:
  # some logic
  raise

Что такое сцепление исключений

В Python 3 при возбуждении исключения в блоке except, старое исключение сохраняется в атрибуте данных __context__ и если новое исключение не обработано, то будет выведена информация о том, что новое исключение возникло при обработке старого («During handling of the above exception, another exception occurred:»). Также, можно связывать исключения в одну цепь или заменять старые новыми. Для этого используется конструкция raise новое_исключение from старое_исключение либо raise новое_исключение from None. В первом случае указанное исключение сохраняется в атрибуте __cause__ и атрибут __suppress_context__ (который подавляет вывод исключения из __context__) устанавливается в True. Тогда, если новое исключение не обработано, будет выведена информация о том, что старое исключение является причиной нового («The above exception was the direct cause of the following exception:»). Во втором случае __suppress_context__ устанавливается в True и __cause__ в None. Тогда при выводе исключения оно, фактически, будет заменено новым (хотя старое исключение всё ещё хранится в __context__).

В Python 2 нет сцепления исключений. Любое исключение, выброшенное в блоке except, заменяет старое.

Зачем нужен блок else

Блок else выполняется, если в процессе выполнения блока try не возникло исключений. Он предназначен для того, чтобы отделить код, который может вызвать исключение, которое должно быть обработано в данном блоке try/except, от кода, который может вызвать исключение того же класса, которое должно быть перехвачено на уровне выше, и свести к минимуму количество операторов в блоке try.

Что можно передать в конструктор исключения

Исключения могут принимать в качестве параметра конструктора любые неименованные аргументы. Они помещаются в атрибуте данных args в виде кортежа (неизменяемого списка). Чаще всего используется один строковой параметр, который содержит сообщение об ошибке. Во всех исключениях определён метод __str__, который по умолчанию вызывает str(self.args). В Python 2 также имеется атрибут message, в который помещается args[0], если len(args) == 1.

Какие есть классы исключений

  • Базовые:
    • BaseException – базовый класс для всех исключений.
    • Exception – класс-наследник BaseException, базовый класс для для всех стандартных исключений, которые не указывают на обязательное завершение программы, и всех пользовательских исключений.
    • StandardError (Python 2) – базовый класс для всех встроенных исключений, кроме StopIteration, GeneratorExit, KeyboardInterrupt и SystemExit.
    • ArithmeticError – базовый класс для всех исключений, связанных с арифметическими операциями.
    • BufferError – базовый класс для исключений, связанных с операциями над буфером.
    • LookupError – базовый класс для исключений, связанных с неверным ключом или индексом коллекции.
    • EnvironmentError (Python 2) – базовый класс для исключений, связанных с ошибками, которые происходят вне интерпретатора Python. В Python 3 его роль выполняет OSError.
  • Некоторые из конкретных стандартных исключений:
    • AssertionError – провал условия в операторе assert.
    • AttributeError – ошибка обращения к атрибуту.
    • FloatingPointError – ошибка операции над числами с плавающей точкой.
    • ImportError – ошибка импортирования модуля или имени из модуля.
    • IndexError – неверный индекс последовательности (например, списка).
    • KeyboardInterrupt – завершение программы путём нажатия Ctrl+C в консоли.
    • MemoryError – нехватка памяти.
    • NameError – имя не найдено.
    • NotImplementedError – действие не реализовано. Предназначено, среди прочего, для создания абстрактных методов.
    • OSError – системная ошибка.
    • OverflowError – результат арифметической операции слишком большой, чтобы быть представлен.
    • RuntimeError – общая ошибка времени выполнения, которая не входит ни в одну из категорий.
    • SyntaxError – ошибка синтаксиса.
    • IndentationError – подкласс SyntaxError – неверный отступ.
    • TabError – подкласс IndentationError – смешанное использование символов табуляции и пробелов.
    • SystemError – некритичная внутренняя ошибка интерпретатора. При возникновении данного исключения следует оставить отчёт об ошибке на сайте bugs.python.org
    • SystemExit – исключение, которое генерируется функцией sys.exit(). Служит для завершения работы программы.
    • TypeError – ошибка несоответствия типов данных.
    • UnboundLocalError – подкласс NameError – обращение к несуществующей локальной переменной.
    • ValueError – генерируется, когда функции или операции передан объект корректного типа, но с некорректным значением, причём эту ситуацию нельзя описать более точным исключением, таким как IndexError.
    • ZeroDivisionError – деление на ноль.

В каких случаях можно обработать SyntaxError

Ошибка синтаксиса возникает, когда синтаксический анализатор Python сталкивается с участком кода, который не соответствует спецификации языка и не может быть интерпретирован. Поскольку, в случае синтаксической ошибки в главном модуле, она возникает до начала выполнения программы и не может быть перехвачена, учебник для начинающих в документации языка Python даже разделяет синтаксические ошибки и исключения. Однако SyntaxError – это тоже исключение, которое наследуется от Exception, и существуют ситуации, когда оно может возникнуть во время исполнения и быть обработано, а именно:

  • ошибка синтаксиса в импортируемом модуле;
  • ошибка синтаксиса в коде, который представляется строкой и передаётся функции eval или exec.

Можно ли создавать собственные исключения

Можно. Они должны быть наследниками класса Exception. Принято называть исключения так, что имя их класса заканчивается словом Error.

Для чего нужны предупреждения (warnings) и как создать собственное

Предупреждения обычно выводятся на экран в ситуациях, когда не гарантируется ошибочное поведение и программа, как правило, может продолжать работу, однако пользователя следует уведомить о чём-либо. Базовым классом для предупреждений является Warning, который наследуется от Exception. Базовым классом-наследником Warning для пользовательских предупреждений является UserWarning.

Для чего нужен модуль warning

В модуле warning собраны функции для работы с предупреждениями. Основной является функция warn, которая принимает один обязательный параметр message, который может быть либо строкой-сообщением, либо экземпляром класса или подкласса Warning (в таком случае параметр category устанавливается автоматически) и два опциональных параметра: category (по умолчанию – UserWarning) – класс предупреждения и stacklevel (по умолчанию – 1) – уровень вложенности функций, начиная с которого необходимо выводить содержимое стека вызовов (полезно, например, для функций-обёрток для вывода предупреждений, где следует задать stacklevel=2, чтобы предупреждение относилось к месту вызова данной функции, а не самой функции).

Декораторы

Что такое декораторы. Зачем нужны

Декоратор в широком смысле - паттерн проектирования, когда один объект изменяет поведение другого. В Питоне декоратор, как правило, это функция A, которая принимает функцию B и возвращает функцию C. При этом функция C задействует в себе функцию B.

Задекорировать функцию значит заменить ее на результат работы декоратора.

Что может быть декоратором. К чему может быть применен декоратор

Декоратором может быть любой вызываемый объект: функция, лямбда, класс, экземпляр класса. В последнем случае определите метод __call__.

Применять декоратор можно к любому объекту. Чаще всего к функциям, методам и классам. Декорирование встречается настолько часто, что под него выделен особый оператор @.

def auth_only(view):
    ...

@auth_only
def dashboard(request):
    ...

Если бы оператора декорирования не существовало, мы бы записали код выше так:

def auth_only(view):
    ...

def dashboard(request):
    ...

dashboard = auth_only(dashboard)

Что будет, если декоратор не возвращает ничего

Если в теле функции нет оператора return, вызов вернет None. Помним, результат декоратора замещает декорируемый объект. В нашем случае декоратор вернет None и функция, которую мы декорируем, тоже станет None. При попытке вызвать ее после декорирования получим ошибку “NoneType is not callable”.

В чем отличие @foobar от @foobar()

Первое – обычное декорирование функцией foobar.

Второй случай – декорирование функцией, которую вернет вызов foobar. По-другому это называется параметрический декоратор или фабрика декораторов. См. следующий вопрос.

Что такое фабрика декораторов

Это функция, которая возвращает декоратор. Например, вам нужен декоратор для проверки прав. Логика проверки одинакова, но прав может быть много. Чтобы не плодить копипасту, напишем фабрику декораторов.

from functools import wraps

def has_perm(perm):
    def decorator(view):
        @wraps(view)
        def wrapper(request):
            if perm in request.user.permissions:
                return view(request)
            else:
                return HTTPRedirect('/login')
        return wrapper
    return decorator

@has_perm('view_user')
def users(request):
    ...

Зачем нужен wraps

wraps - декоратор из стандартной поставки Python, модуль functools. Он назначает функции-врапперу те же поля __name__, __module__, __doc__, что и у исходной функции, которую вы декорируете. Это нужно для того, чтобы после декорирования функция-враппер в стектрейсах выглядела как декорируемая функция.

Метаклассы

Что такое метаклассы

Метакласс это «штука», которая создаёт классы.

Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты.

Что такое type. Как работает поиск метакласса при создании объекта

type это метакласс, который Питон внутренне использует для создания всех классов.

Когда вы пишете:

class Foo(Bar):
  pass

Питон делает следующее:

  • Есть ли у класса Foo атрибут __metaclass__?
  • Если да, создаёт в памяти объект-класс с именем Foo, используя то, что указано в __metaclass__.
  • Если Питон не находит __metaclass__, он ищет __metaclass__ в родительском классе Bar и попробует сделать то же самое.
  • Если же __metaclass__ не находится ни в одном из родителей, Питон будет искать __metaclass__ на уровне модуля.
  • И если он не может найти вообще ни одного __metaclass__, он использует type для создания объекта-класса.

Как работают метаклассы

  • перехватить создание класса
  • изменить класс
  • вернуть модифицированный

Зачем вообще использовать метаклассы

Основное применение метаклассов это создание API. Типичный пример — Django ORM.

Она позволяет написать что-то в таком духе:

class Person(models.Model):
  name = models.CharField(max_length=30)
  age = models.IntegerField()

Однако если вы выполните следующий код:

guy = Person(name='bob', age='35')
print guy.age

вы получите не IntegerField, а int, причём значение может быть получено прямо из базы данных.

Это возможно, потому что models.Model определяет __metaclass__, который сотворит некую магию и превратит класс Person, который мы только что определили простым выражением в сложную привязку к базе данных.

Django делает что-то сложное выглядящее простым, выставляя наружу простой API и используя метаклассы, воссоздающие код из API и незаметно делающие всю работу.

Ввод-Вывод

Что такое файловый объект

Файловый объект – объект, предоставляющий файл-ориентированный API (методы read(), write() и т.д.) для доступа к ресурсу. В зависимости от способа создания, файловый объект может предоставлять доступ к реальному файлу на диске или другому виду устройства хранения или передачи данных (стандартные потоки ввода/вывода, буферы в памяти, сокеты и т.д.). Файловые объекты также называют потоками. Файловые объекты являются контекстными менеджерами.

Какие есть виды файловых объектов

На уровне типов данных в Python 2 нет отличия между текстовыми и бинарными файлами. При открытии можно указать текстовый либо бинарный режим, но это влияет только на преобразования концов строк при выполнении под ОС Windows, а под Unix-системами, где преобразования концов строк не требуются, не влияет ни на что.

В Python 3 существует три вида файловых объектов: текстовые файлы (text files), «обычные» (небуферизированные) бинарные файлы (raw binary files) и буферизированные бинарные файлы (buffered binary files). Разные виды потоков представляются соответствующими классами модуля io.

Модуль io был обратно портирован в последние версии Python 2, поэтому в Python 2 также при желании можно использовать систему ввода-вывода, аналогичную Python 3.

В чем отличие текстовых и бинарных файлов

Текстовые файлы записывают и считывают данные типа str и автоматически выполняют преобразования кодировок и концов строк. Бинарные файлы записывают и считывают данные типов bytes и bytearray и не производят никаких манипуляций с данными: всё записывается и считывается в таком же виде, как и сохраняется.

Как пользоваться функцией open

Сигнатура функции в Python 2: open(file, mode='r', buffering=-1).

Сигнатура функции в Python 3 (и в Python 2 при использовании функции io.open): open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None).

Основные параметры:

  • file – имя файла или файловый дескриптор;
  • mode – режим открытия файла;
  • encoding – кодировка файла;
  • buffering – использовать ли буферизацию: отрицательное число (по умолчанию, указывать явно не нужно) – стандартное значение для данного вида файлового объекта, 0 – отключить буферизацию, 1 – построчная буферизация (для текстовых файлов), другое значение – включить буферизацию и задать соответствующий размер буфера.

Обязательным параметром является только первый. Чаще всего функция open() используется с двумя параметрами.

mode может начинаться с символов «r» (чтение), «w» (запись, очищает файл, если уже существует), «x» (исключительное создание, неуспешно, если файл уже существует), «a» (добавление, запись в конец файла). Также параметр mode может иметь вторую букву для определения типа файла: «t» для текстового (по умолчанию) и «b» для бинарного. Также можно добавить символ «+» для открытия в режиме чтения и записи одновременно. Порядок последних двух символов не имеет значения: «rb+» и «r+b» задают один и тот же режим.

Для чего необходимо закрывать файлы

После окончания работы с файлом следует обязательно его закрыть при помощи метода close(), особенно если он был открыт для записи. При использовании буферизированного вывода данные, которые записываются в файл, не попадают в него сразу, а записываются в буфер. Содержимое буфера записывается в файл при его заполнении или вызове методов flush() или close(). Кроме того, если файл открыт для записи, он будет заблокирован для открытия для записи другими процессами до момента закрытия. Все открытые файлы автоматически закрываются при удалении соответствующих файловых объектов из памяти сборщиком мусора интерпретатора Python и при завершении работы самого интерпретатора, однако следует держать файлы открытыми минимально требуемое время.

Что делают методы tell и seek

Метод tell() возвращает текущую позицию считывания/записи в файле. Метод seek(offset, whence) устанавливает её. Параметр offset задаёт отступ, а whence – точку, от которой данный отступ считается: io.SEEK_SET(0) – начало файла, io.SEEK_CUR(1) – текущая позиция, io.SEEK_END(2) – конец файла.

Что делают StringIO и BytesIO

Классы io.StringIO и io.BytesIO, представляют собой потоки для считывания и записи в строки или байтовые строки в памяти. Они могут быть использованы для того, чтобы использовать строки и байтовые строки в качестве текстовых и бинарных файлов.

Являются ли файловые объекты контекстными менеджерами

Да, являются

Что такое сериализация

Сериализация – это процесс сохранения объектов в двоичном или строковом виде для хранения, передачи и восстановления. Обратный процесс называется десериализацией. Термины-синонимы маршалинг/анмаршалинг

json.dumps / json.dump , json.loads / json.load

Функция dumps модуля json сохраняет JSON-представление объекта в строку. Функция dump – в текстовый файл. Функция loads модуля json загружает объект из строки. Функция load – из текстового файла.

Что делать если нужно сериализовать данные, которые не поддерживаются стандартным модулем json

Можно использовать pickle или расширить классы JSONEncoder и JSONDecoder.

pickle.dumps / pickle.dump, pickle.loads / pickle.load

Функции dump, dumps, load и loads модуля pickle аналогичны по своему предназначению соответствующим функциям модуля JSON, но работают с байтовыми строками и бинарными файлами.

Опциональный параметр protocol данных функций задаёт версию протокола. Последнюю версию протокола можно получить как константу pickle.HIGHEST_PROTOCOL, текущую версию по умолчанию – pickle.DEFAULT_PROTOCOL.

На момент написания данного текста существует пять версий протокола:

  • 0 и 1 – это устаревшие версии, которые использовались в Python 2.2 и ниже;
  • 2 – это основная версия протокола для Python 2;
  • 3 – версия протокола, которая появилась в Python 3, стандартный протокол в Python 3 на текущий момент, не может быть десериализован в Python 2;
  • 4 – версия протокола, появившаяся в Python 3.4, поддерживает очень большие по объёму памяти объекты, поддерживает большее количество типов объектов, добавлены некоторые оптимизации.
  • 5 - версия протокола, появившаяся в Python 3.8. Он добавляет поддержку данных out-of-band и ускорение для in-band данных. PEP 574 более подробно описывает изменения.

Тестирование

Пирамида тестирования

Рис. Пирамида тестирования

Что такое mocking

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

Что делать, если тестируемая функция использует удалённое подключение к внешним сервисам, которое иногда видает ошибку таймаута, 404 и им подобные

Если мы говорим про юнит тесты, то они не должны вызывать внешние ресурсы, то есть делать http запросы и тд. Следовательно нужно либо замокать http-клиент, который использует функция для вызова сервиса, либо, что обычно является лучшим решением, передавать то что вызывает этот сервис в функцию зависимостью (если конечно мы не тестируем сам клиент для вызова сервиса).

Что делать, если тестируемая функция занимает много времени на выполнение повторяющихся операций внутри неё

Например, внутри цикл от 1 до 1000000, где что-то считывается, записывается, рассчитывается.

Допустим у этой функции не проблем с декомпозицией - это функция, которая выполняет одно действие и разбивать ее на несколько других не имеет никакого смысла. В таком случае я бы:

  • сделал бы возможным заменить из теста верхнюю границу цикла (через параметр или мокая константу, настройку и т.д.)
  • если функция вызывает для расчетов другую ресурсоёмкую функцию, стороннюю или из своей кодовой базы, то возможно замокал бы ее и проверил что она вызывается с нужными параметрами
  • по возможности подготовил бы для теста такой входящий набор данных, при котором она выполялась быстро

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

Какие вы знаете виды тестов

Unit-тесты

Что проверяется? Модульные тесты проверяют, правильно ли работает каждый отдельный модуль (юнит) вашего кода. В идеале при планировании и написании модульных тестов нужно изолировать функционал, который нельзя разделить на более мелкие составляющие, и протестировать его.

Модульные тесты не должны проверять внешние зависимости или взаимодействия. Вам определенно нужно сымитировать (mock out) api-вызовы. Борцы за чистоту модульных тестов будут также настаивать на имитации вызовов базы данных, чтобы убедиться, что ваш код, получая корректный input из внешних источников, ведет себя правильно. Модульные тесты должны быть быстрыми, иначе они значительно замедляют разработку.

Когда их запускать? Вы должны писать и запускать модульные тесты параллельно со своим кодом.

Интеграционные тесты (Integration tests)

Этот термин употребляют чаще к тестам, покрывающим непосредственно публичный API сервиса. Фокус устремлен на проверку взаимодействия разных систем по принципу “сервис-клиент”.

Что проверяется? Интеграционные тесты проверяют взаимодействие между двумя (или больше, чем двумя) отдельными юнитами вашего кода.

Ваше приложение состоит из отдельных модулей, выполняющих определенные маленькие функции. Каждый из них может хорошо работать в изолированном состоянии, но ломаться в связке с другими.

Интеграционные тесты также проверяют интеграцию вашего кода с внешними зависимостями, вроде соединений с базой данных или сторонними API.

Когда их запускать? Интеграционные тесты это следующий шаг после модульных тестов.

Что, если тесты провалены? Провал интеграционных тестов означает, что две (или больше) функции вашего приложения не работают вместе. Это могут быть два написанных вами модуля, которые приходят в противоречие из-за какой-то сложной бизнес-логики. Также провал может случиться из-за того, что изменилась структура ответа стороннего API. Провал тестов может быть предупреждением о плохой обработке ошибок в случае сбоя подключения к базе данных.

Функциональное тестирование

  • Функциональное тестирование может быть определено как тестирование отдельных функций модулей.
  • Это относится к тестированию программного продукта на индивидуальном уровне, чтобы проверить его функциональность.
  • Оно сильно отличается от модульного или интеграционного тестирования; вы не можете написать бесчисленное множество тест-кейсов для функционального тестирования, поскольку оно является более сложным, чем модульное.
  • Инструменты функционального тестирования стремятся проверить функциональные возможности (работоспособность) программного обеспечения. Тестовые примеры используются для проверки ожидаемых и неожиданных результатов тестирования программного обеспечения.
  • Этот тип тестирования проводится больше с точки зрения пользователя. То есть, он рассматривает ожидание пользователя в выбранном типе ввода данных.
  • Selenium является одним из наиболее распространенных инструментов, используемых для функционального тестирования.

Системный тест (System test, Service test)

Автоматизированные тесты, проверяющие работу всей интегрированной системы. По сути, они представляют собой предельный случай интеграционных тестов. Системные тесты не проверяют бизнес-правила напрямую. Вместо этого они проверяют, что компоненты системы правильно связаны друг с другом, а взаимодействие между ними проходит по исходному плану. Тесты производительности и пропускной способности обычно относятся к этой категории.

Системное — это тестирование программы в целом. Для небольших проектов это, как правило, ручное тестирование — запустил, пощелкал, убедился, что (не) работает. Можно автоматизировать. К автоматизации есть два подхода.

Эти тесты пишутся системными архитекторами и ведущими специалистами с технической стороны. Как правило, они пишутся на том же языке и в той же среде, что и интеграционные тесты пользовательского интерфейса. Системные тесты выполняются относительно редко (в зависимости от продолжительности их выполнения), но чем чаще – тем лучше.

Системные тесты покрывают 10 % системы. Это объясняется тем, что они предназначены для проверки правильности не поведения системы, а ее конструкции. Правильность поведения нижележащего кода и компонентов уже была проверена на нижних уровнях пирамиды.

Проверка работоспособности (Smoke test, Sanity check)

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

Дымовое тестирование - пришло из сферы проверки оборудования, если, после подачи питания, появляется дым и запах гари, то оборудование неисправно.

Также дымовыми тестами можно сопровождать рефакторинг legacy кода, потому что написание полноценных юниттестов может быть сильно затратно по времени.

Регрессионное тестирование (Regression testing)

Это может быть любой вид теста из описанных выше, который пишется после того, как была обнаружена проблема. Тест должен эмулировать в точности шаги для воспроизведения проблемы. Наличие такого теста после исправления проблемы дает гарантию, что точно такой же баг, больше не появится в системе.

Что проверяется? Регрессионные тесты проверяют набор сценариев, которые раньше работали и должны быть относительно стабильными.

Прочее

  • Приемочное тестирование
  • Проверка на уязвимости (Penetration test, Pentest)
  • Нагрузочное тестирование (Load testing)
  • Тестирование производительности (Performance testing)
  • Фаззинг тест (Fuzzing test, Fuzztest, Random test)

Чем интеграционное тестирование отличается от функционального

Интеграционное и функциональное тестирования - это две фазы в процессе тестирования программного обеспечения. Первое проводится после модульного тестирования, а второе - метод тестирования черного ящика.

Функциональное тестирование также упоминается как тестирование Е2Е для тестирования браузера.

Рис. Интеграционное тестирование против функционального тестирования

Функциональное программирование

Что такое функциональное программирование

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

Как у Python с поддержкой функционального программирования

Python частично поддерживает парадигму функционального программирования и позволяет писать код в функциональном стиле. Кроме того, в нём присутствуют определённые возможности, характерные для функциональных языков или впервые появившиеся в функциональных языках (списковые включения, лямбда-функции, функции высшего порядка и т.д.).

Что такое объект первого класса

Объектами первого класса (англ. first-class object, first-class entity, first-class citizen) в контексте конкретного языка программирования называются сущности, которые могут быть переданы как параметр, возвращены из функции, присвоены переменной.

Объект называют «объектом первого класса», если он:

  • может быть сохранен в переменной или структурах данных;
  • может быть передан в функцию как аргумент;
  • может быть возвращен из функции как результат;
  • может быть создан во время выполнения программы;
  • внутренне самоидентифицируем (независим от именования).

Термин «объект» используется здесь в общем смысле, и не ограничивается объектами языка программирования. В Python, как и в функциональных языках, функции являются объектами первого класса.

Что такое функция высшего порядка

Функция высшего порядка – функция, принимающая в качестве аргументов другие функции или возвращающая другую функцию в качестве результата. Основная идея состоит в том, что функции имеют тот же статус, что и другие объекты данных.

Что такое каррирование

Карринг — это преобразование функции от многих аргументов в набор функций, каждая из которых является функцией от одного аргумента. Мы можем передать часть аргументов в функцию и получить обратно функцию, ожидающую остальные аргументы. Это преобразование было введено М. Шейнфинкелем и Г. Фреге и получило свое название в честь Х. Карри.

Создадим простую функцию greet, которая принимает в качестве аргументов приветствие и имя:

def greet(greeting, name):
    print(greeting + ', ' + name)

greet('Hello', 'German')

Небольшое улучшение позволит нам создать новую функцию для любого типа приветствия и передать этой новой функции имя:

def greet_curried(greeting):
    def greet(name):
        print(greeting + ', ' + name)
    return greet

greet_hello = greet_curried('Hello')

greet_hello('German')
greet_hello('Ivan')

Или напрямую greet_curried

greet_curried('Hi')('Roma')

А дальше можно сделать это с любым количеством аргументов:

def greet_deeply_curried(greeting):
    def w_separator(separator):
        def w_emphasis(emphasis):
            def w_name(name):
                print(greeting + separator + name + emphasis)
            return w_name
        return w_emphasis
    return w_separator

greet = greet_deeply_curried("Hello")("...")(".")
greet('German')
greet('Ivan')

Опишите функции map, reduce, filter модуля functools

Функция map применяет функцию к каждому элементу последовательности. В Python 2 возвращает список, в Python 3 – объект-итератор.

Функция filter оставляет лишь те элементы последовательности, для которых заданная функция истинна. В Python 2 возвращает список, в Python 3 – объект-итератор.

Функция reduce (в Python 2 встроенная, в Python 3 находится в модуле functools) принимает функцию от двух аргументов, последовательность и опциональное начальное значение и вычисляет свёртку (fold) последовательности как результат последовательного применения данной функции к текущему значению (так называемому аккумулятору) и следующему элементу последовательности.

Какие еще вы знаете функции из модуля functools

Модуль functools содержит большое количество стандартных функций высшего порядка. Среди них особенно полезны:

  • lru_cache – декоратор, который кеширует значения функций, которые не меняют свой результат при неизменных аргументах; полезен для кеширования данных, мемоизации (сохранения результатов для возврата без вычисления функции) значений рекурсивных функций (например, такого типа, как функция вычисления n-го числа Фибоначчи) и т.д.;
  • partial – частичное применение функции (вызов функции с меньшим количеством аргументов, чем она ожидает, и получение функции, которая принимает оставшиеся параметры).

Какие вы функции знаете из модуля itertools

Модуль itertools содержит функции для работы с итераторами и создания итераторов. Некоторые из его функций:

  • product – декартово произведение итераторов (для избегания вложенных циклов for);
  • permutations – генерация перестановок;
  • combinations – генерация сочетаний;
  • combinations_with_replacement – генерация размещений;
  • chain – соединение нескольких итераторов в один;
  • takewhile – получение значений последовательности, пока значение функции-предиката для её элементов истинно;
  • dropwhile – получение значений последовательности начиная с элемента, для которого значение функции-предиката перестанет быть истинно.

Для чего нужен модуль operator

Модуль operator содержит функции, которые соответствуют стандартным операторам. Таким образом, вместо lambda x, y: x + y можно использовать уже готовую функцию operator.add и т.д.

GIL, потоки, процессы

Что такое GIL. Какие у него есть проблемы

В любой момент может выполняться только один поток Python. Глобальная блокировка интерпретатора — GIL — тщательно контролирует выполнение тредов. GIL гарантирует каждому потоку эксклюзивный доступ к переменным интерпретатора (и соответствующие вызовы C-расширений работают правильно).

Принцип работы прост. Потоки удерживают GIL, пока выполняются. Однако они освобождают его при блокировании для операций ввода-вывода. Каждый раз, когда поток вынужден ждать, другие, готовые к выполнению, потоки используют свой шанс запуститься.

Когда поток начинает работу, он выполняет захват GIL. Спустя какое-то время планировщик процессов решает, что текущий поток поработал достаточно, и передает управление следующему потоку. Поток №2 видит, что GIL захвачен, так что он не продолжает работу, а погружает себя в сон, уступая процессор потоку №1.

Но поток не может удерживать GIL бесконечно. До Python 3.3 GIL переключался каждые 100 инструкций машинного кода. В поздних версиях GIL может быть удержан потоком не дольше 5 мс. GIL так-же освобождается, если поток совершает системный вызов, работает с диском или сетью.

Проблема в том, что из-за GIL далеко не все задачи могут быть решены в тредах. Напротив, их использование чаще всего снижает быстродействие программы (при CPU-bound задачах). С использованием тредов требуется следить за доступом к общим ресурсам: словарям, файлам, соединением к БД.

  • GIL упрощает интеграцию non thread safe библиотек на С. Благодаря GIL у нас так много быстрых модулей и биндингов почти ко всему.
  • Библиотекам на C доступен механизм управления GIL. Так например NumPy отпускает его на долгих операциях.

По сути, GIL в питоне делает бесполезной идею применять потоки для параллелизма в вычислительных задачах. Они будут работать последовательно даже на многопроцессорной системе. На CPU Bound задачах программа не ускорится, а только замедлится, так как теперь потокам придется делить пополам процессорное время. При этом I/O операции GIL не замедлит, так как перед системным вызовом поток отпускает GIL.

Работали ли Вы с asyncio. В чём его особенность

Представим, что мы пишем HTTP или WebSocket сервер, который каждое подключение обрабатывает в отдельном потоке.

Здесь вполне можно создать 100, может даже 500 потоков, чтобы обработать нужное количество одновременных соединений. Для коротких запросов это даже будет работать и позволит выдержать нагрузку в 5000 RPS на самом дешевом инстансе в DO за пять баксов — вполне неплохо. Если у вас меньше, возможно здесь и не нужны никакие AsyncIO/Tornado/Twisted.

Но что, если их количество стремится к бесконечности? Скажем, это большой чат с кучей каналов, где количество одновременных участников не ограничено. В такой ситуации создать столько потоков, чтобы хватило каждому пользователю я бы уже не рискнул. И вот почему:

Как говорилось выше, пока GIL захвачен одним потоком, другие работать не будут. Планировщик операционной системы, при этом, о GIL ничего не знает и все равно будет отдавать процессор заблокированный потокам. Такой поток, конечно, увидит что GIL захвачен и сразу же уснет, но на переключение контекста процессора будет тратиться драгоценное время.

Переключение контекста — вообще дорогая для процессора операция, которая требует сброса регистров, кэша и таблицы отображения страниц памяти. Чем больше потоков запущено, тем больше процессор совершает холостых переключений на потоки, заблокированные GIL, прежде чем дойдет до того самого, который этот GIL удерживает. Не очень-то эффективно.

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

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

Как и в случае с тредами, асинхронщина бесполезна для вычислений. Тут ситуация даже хуже, так как зависший на вычислениях поток рано или поздно GIL отпустит, а вот блокирующий код в сопрограмме заблокирует весь поток, до тех пор, пока не исполнится весь. В отличии от нативных тредов, у сопрограмм отсутствует прерывание по таймеру. Передача управления следующей сопрограмме происходит вручную, при явном вызове конструкции await (или yield, если используются generator-based корутины). Поэтому важно следить, чтобы в асинхронных программах не было блокирующего кода и использовались только асинхронные вызовы, а все вычисления происходили в отдельных процессах.

Потоки будут проще, если у вас типичное веб-приложение, которое не зависит от внешних сервисов, и относительно конечное количество клиентов, для которых время ответа будет предсказуемо-коротким.

AsyncIO подойдет, если приложение большую часть времени тратит на чтение/запись данных, а не их обработку. Например, у вас много медленных запросов — вебсокеты, long polling или есть медленные внешние синхронные бекенды, запросы к которым неизвестно когда завершатся.

Что такое async/await, для чего они нужны и как их использовать

Ключевое слово async идет до def, чтобы показать, что метод является асинхронным. Ключевое слово await показывает, что вы ожидаете завершения сопрограммы.

import asyncio
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

async def call_url(url):
    async with aiohttp.ClientSession() as session:
        print('Starting {}'.format(url))
        async with session.get(url) as response:
            data = await response.text()
            print('{}: {} bytes: {}'.format(url, len(data), data))
            return data

futures = [call_url(url) for url in urls]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*futures))

Программа состоит из метода async. Во время выполнения он возвращает сопрограмму, которая затем находится в ожидании.

async/await нужен для того, чтобы не блокировать поток выполнения на время ожидания какого-нибудь асинхронного события. Конструкция async/await превращает по сути процедуру в корутину (сопрограмму): она прекращает своё выполнение на время await, дожидается асинхронного события, и возобновляет работу.

В не-async-варианте ожидание получается блокирующим, или нужно вручную делать трюки: запускать операцию и подписываться на её окончание. Async делает код более простым, линейным.

Как в питоне реализуется многопоточность. Какими модулями

Многопоточность достигается модулем Threading. Это нативные Posix-треды. Такие треды исполняются операционной системой, а не виртуальной машиной.

В чем отличие тредов от мультипроцессинга

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

Какие задачи хорошо параллелятся, какие плохо

Хорошо параллелятся задачи, которые порождают долгий IO. Когда тред упирается в ожидание сокета или диска, интерпретатор бросает этот тред и стартует следующий. Это значит, не будет простоя из-за ожидания. Наоборот, если ходить в сеть в одном треде (в цикле), то каждый раз придется ждать ответа.

Однако, если затем в треде обрабатывает полученные данные, то выполнятся будет только он один. Это не только не даст прироста в скорости, но и замедлит программу из-за переключения на другие треды.

Короткий ответ: хорошо ложатся на треды задачи по работе с сетью. Например, выкачать сто урлов. Полученные данные обрабатывайте вне тредов.

Нужно посчитать 100 уравнений. Делать это в тредах или нет

Нет, потому что в этой задаче нет ввода-вывода. Интерпретатор только будет тратить лишнее время на переключение тредов. Сложные математические задачи лучше выносить в отдельные процессы, либо использовать фреймворк для распределенных задач Celery, либо подключать как C-библиотеки.

Треды в Питоне — это нативные треды или нет

Да, это нативные Posix-совместимые треды, которые исполняются на уровне операционной системы.

Что такое гринлеты. Общее понятие. Примеры реализаций

Greenlet == Green thread == Зеленые треды == легковесные треды внутри виртуальной машины. Могут называться корутинами, сопроцессами, акторами и т.д. в зависимости от платформы. Операционная система не видит их. С точки зрения ОС запущен один процесс виртуальной машины, а что внутри нее – неизвестно. Такими тредами управляет сама вируальная машина: порождает, исполняет, согласует доступ к ресурсам.

Примеры: корутины в языках Go и Lua, легковесные процессы в Erlang, модуль greenlet для Python. Модуль gevent использует гринлеты

Какие варианты реализации шаблона Singleton на питоне

Декоратор:

def singleton(class_):
    instances = {}
    def getinstance(*args, **kwargs):
        if class_ not in instances:
            instances[class_] = class_(*args, **kwargs)
        return instances[class_]
    return getinstance

@singleton
class MyClass(BaseClass):
    pass

Достоиства:

Недостатки:

Base class:

class Singleton(object):
    _instance = None
    def __new__(class_, *args, **kwargs):
        if not isinstance(class_._instance, class_):
            class_._instance = object.__new__(class_, *args, **kwargs)
        return class_._instance

class MyClass(Singleton, BaseClass):
    pass

Достоиства:

Недостатки:

Метаклассы:

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

#Python2
class MyClass(BaseClass):
    __metaclass__ = Singleton

#Python3
class MyClass(BaseClass, metaclass=Singleton):
    pass

Достоиства:

Недостатки:

Модуль:

Достоиства:

Недостатки:

Какие вы знаете инструменты для проверки кодстайл

Pycodestyle — простая консольная утилита для анализа кода Python, а именно для проверки кода на соответствие PEP8. Один из старейших анализаторов кода, до 2016 года носил название pep8, но был переименован по просьбе создателя языка Python Гвидо ван Россума.

Pydocstyle проверяет наличие docstring у модулей, классов, функций и их соответствие официальному соглашению PEP257.

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

Vulture — небольшая утилита для поиска “мертвого” кода в программах Python. Она использует модуль ast стандартной библиотеки и создает абстрактные синтаксические деревья для всех файлов исходного кода в проекте. Далее осуществляется поиск всех объектов, которые были определены, но не используются. Vulture полезно применять для очистки и нахождения ошибок в больших базовых кодах.

Flake8 — обвязка к входящим в нее утилитам — pyflakes, pycodestyle, mccabe. Flake8 имеет схожий с pylint основной функционал. Однако она имеет ряд отличий и особенностей:

Prospector — это инструмент для анализа кода Python. Объединяет функциональность других инструментов анализа Python, таких как pylint, pep8, mccabe, Pyflakes, Dodgy, pydocstyle (экспериментально, возможны ошибки). Дополнительно можно подключить mypy, pyroma, vulture. Главной особенностью prospector является наличие предустановленных профилей, которые содержат настройки входящих в него утилит, призванных подавить наиболее придирчивые предупреждения и оставить только важные сообщения.

Pylama — инструмент аудита кода для Python и JavaScript. Служит оберткой на такими утилитами как: pydocstyle, pycodestyle, pyflakes, mccabe, pylint, radon (инструмент для сбора и вычисления различных метрик из исходного кода). Для работы с работы с JavaScript кодом используется gjslint.

autopep8 модифицирует код, не совместимый с PEP8. Проверка соответствия соглашениям осуществляется с помощью утилиты pycodestyle. В autopep8 есть поддержка многопоточности, рекурсивного обхода каталогов, возможность сохранения настроек в файле, задание диапазона строк для исправления, фильтрация ошибок и непосредственное изменение проверяемого файла.

yapf похож на autopep8, но использует другой подход, который основан на «clang-format», разработанном Дэниелом Джаспером. Отформатированный yapf код, будет не только соблюдать принятые соглашения, но и выглядеть так, словно был написан хорошим программистом. Вторым важным отличием является возможность задавать стили. Для этого воспользуйтесь ключом —style и в качестве аргумента передайте файл с настройками или одно из предопределенных значений (pep8, google, chromium, facebook).

black — это бескомпромиссный форматировщик, который работает быстро и экономит время и умственную энергию программистов для более важных вопросов.

Что такое list/dict comprehension

Выражение заключенное в квадратные/фигурные скобки, в котором используются ключевые слова for и in для построения списка/словаря путем обработки и фильтрации элементов из одного или нескольких итерируемых объектов. Списковое включение работает энергично.

Энергичный - итерируемый объект, который сразу строит все свои элементы. В Python включения - энергичные операции. Противоположность - ленивый.

Какая разница между одинарным и двойным подчеркиванием

Есть 5 кейсов использования подчеркивая в Python:

  1. Для хранения значения последнего выражения в REPL
  2. Игнорирования значения
  3. Для опеределения специального значения функции или переменной
    • одинарное в начале или конце названия
    • двойное в начале
    • двойное в начале и конце
  4. Для использования в качестве функции локализации
  5. Для разделения символов числа (1_00 == 100)

Отличие copy() от deepcopy()

Глубокая копия deepcopy() создает новую и отдельную копию всего объекта или списка со своим уникальным адресом памяти. Это означает, что любые изменения, внесенные вами в новую копию объекта или списка, не будут отражаться в исходной. Этот процесс происходит следующим образом: сначала создается новый список или объект, а затем рекурсивно копируются все элементы из исходного в новый.

Поверхностное копирование copy() также создает отдельный новый объект или список, но вместо копирования дочерних элементов в новый объект оно просто копирует ссылки на их адреса памяти. Следовательно, если вы сделаете изменение в исходном объекте, оно будет отражено в скопированном объекте, и наоборот. Короче говоря, обе копии зависят друг от друга.

Что такое garbage collector. В чём его плюсы и минусы

GC (generational garbage collector) - это сборщик мусора, создавался он в первую очередь для обнаружения и удаления циклических ссылок. gc является встроенным в python модулем и при необходимости его можно выключить и запускать вручную (или не запускать). Чтобы понимать для чего был создан GC нужно понимать как в Python работает менеджер памяти и как эта память высвобождается.

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

Как только один из маленьких объектов удаляется — память из под него не переходит операционной системе, Python оставляет её для новых объектов с таким же размером. Если в одном из выделенных блоков памяти не осталось объектов, то Python может высвободить его операционной системе. Как правило, высвобождение блоков случается когда скрипт создает множество временных объектов.

Таким образом, если долгоживущий Python процесс с течением времени начинает потреблять больше памяти, то это совсем не означает, что в вашем коде есть проблема с утечкой памяти.

Стандартный интерпретатор питона (CPython) использует сразу два алгоритма сборки мусора, подсчет ссылок и generational garbage collector (далее GC), более известный как стандартный модуль gc из Python.

Алгоритм подсчета ссылок очень простой и эффективный, но у него есть один большой недостаток. Он не умеет определять циклические ссылки. Именно из-за этого, в питоне существует дополнительный сборщик, именуемый поколенческий GC, который следит за объектами с потенциальными циклическими ссылками.

В Python, алгоритм подсчета ссылок является фундаментальным и не может быть отключен, тогда как GC опционален и может быть отключен.

В отличие от алгоритма подсчета ссылок, циклический GC не работает в режиме реального времени и запускается периодически. Каждый запуск сборщика создаёт микропаузы в работе кода, поэтому CPython (стандартный интерпретатор) использует различные эвристики, для определения частоты запуска сборщика мусора.

Циклический сборщик мусора разделяет все объекты на 3 поколения (генерации). Новые объекты попадают в первое поколение. Если новый объект выживает после процесса сборки мусора, то он перемещается в следующее поколение. Чем выше поколение, тем реже оно сканируется на мусор. Так-как новые объекты зачастую имеют очень маленький срок жизни (являются временными), то имеет смысл опрашивать их чаще, чем те, которые уже прошли через несколько этапов сборки мусора.

В каждой генерации есть специальный счетчик и порог срабатывания, при достижении которых срабатывает процесс сборки мусора. Каждый счетчик хранит количество аллокаций минус количество деаллокаций в данной генерации. Как только в Python создается какой либо контейнерный объект, он проверяет эти счетчики. Если условия срабатывают, то начинается процесс сборки мусора.

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

Стандартные пороги срабатывания для поколений установлены на 700, 10 и 10 соответственно, но вы всегда можете их изменить с помощью функций gc.get_threshold и gc.set_threshold.

Что такое интроспекция

Интроспекция — это способность программы исследовать тип или свойства объекта во время работы программы. Вы можете поинтересоваться, каков тип объекта, является ли он экземпляром класса. Некоторые языки даже позволяют узнать иерархию наследования объекта. Возможность интроспекции есть в таких языках, как Ruby, Java, PHP, Python, C++ и других. В целом, инстроспекция — это очень простое и очень мощное явление. Вот несколько примеров использования инстроспекции:

// Java

if(obj instanceof Person){
   Person p = (Person)obj;
   p.walk();
}
//PHP

if ($obj instanceof Person) {
   // делаем что угодно
}

В Python самой распространённой формой интроспекции является использование метода dir для вывода списка атрибутов объекта:

# Python

class foo(object):
  def __init__(self, val):
    self.x = val
  def bar(self):
    return self.x

...

dir(foo(5))
=> ['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'bar', 'x']

Что такое рефлексия

Интроспекция позволяет вам изучать атрибуты объекта во время выполнения программы, а рефлексия — манипулировать ими. Рефлексия — это способность компьютерной программы изучать и модифицировать свою структуру и поведение (значения, мета-данные, свойства и функции) во время выполнения. Простым языком: она позволяет вам вызывать методы объектов, создавать новые объекты, модифицировать их, даже не зная имён интерфейсов, полей, методов во время компиляции. Из-за такой природы рефлексии её труднее реализовать в статически типизированных языках, поскольку ошибки типизации возникают во время компиляции, а не исполнения программы (подробнее об этом здесь). Тем не менее, она возможна, ведь такие языки, как Java, C# и другие допускают использование как интроспекции, так и рефлексии (но не C++, он позволяет использовать лишь интроспекцию).

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

Пример:

# Без рефлексии
Foo().hello()

# С рефлексией
getattr(globals()['Foo'](), 'hello')()