Вопросы на собеседовании Python-разработчика

Разбор 11 часто задаваемых вопросов на собеседовании для начинающих, а также для продвинутых Python-разработчиков

Вопрос № 1

Что будет в результате выполнения данного кода? Объясните ваш ответ.

def extendList(val, list=[]):
    list.append(val)
    return list

list1 = extendList(10)
list2 = extendList(123, [])
list3 = extendList('a')

print (f"list1 = {list1}")
print (f"list2 = {list2}")
print (f"list3 = {list3}")

Как бы вы изменили функцию extendList, чтобы добиться более предсказуемого поведения?

Посмотреть ответ

Результат будет следующим:

list1 = [10, 'a']
list2 = [123]
list3 = [10, 'a']

Многие могут ошибочно предположить, что list1 будет равен [10], а list3 будет равен ['a'], думая, что всякий раз при вызове функции extendList аргумент list будет равен значению по умолчанию [].

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

Таким образом, list1 и list3 имеют дело с одним и тем же списком по умолчанию, в то время как list2 использует пустой список, переданный ему во втором аргументе.

Чтобы каждый раз при вызове функции extendList без второго аргумента она бы использовала пустой список по умолчанию, определение этой функции можно было бы изменить следующим образом:

def extendList(val, list=None):
    if list is None:
        list = []
    list.append(val)
    return list

В таком исполнении результат работы функции будет следующим:

list1 = [10]
list2 = [123]
list3 = ['a']

Вопрос № 2

Что будет в результате выполнения данного кода? Объясните ваш ответ.

def multipliers():
    return [lambda x : i * x for i in range(4)]
    
print ([m(2) for m in multipliers()])

Как бы вы изменили функцию multipliers, чтобы добиться более предсказуемого поведения?

Посмотреть ответ

Результатом выполнения данного кода будет [6, 6, 6, 6], а не [0, 2, 4, 6].

Это объясняется тем, что замыкания в Python работают по принципу позднего связывания. Это означает, что значения переменных, используемых в замыканиях, ищутся во время вызова внутренней функции. Поэтому, когда вызывается любая из функций, возвращаемых multipliers(), значение i ищется исключительно в области видимости этой функции в данный момент. А значение i, вне зависимости от того, какая из функций вызывается, после завершения цикла for всегда равно 3. Таким образом, каждая возвращаемая функция умножает значение, которое ей передано, на 3, а поскольку в приведенном выше коде передается значение 2 , все они возвращают значение 6 (то есть 3 x 2).

Между тем, существует довольно распространенное заблуждение, что это как-то связано с лямбда-функциями (например, так сказано в книге «Автостопом по Python»). Но это не так. Функции, созданные с помощью лямбда-выражений, ни в коем случае не являются какими-то особыми, и в работе ничем не отличаются от функций, созданных с использованием обычной функции def.

Ниже приведены возможные способы обойти данную проблему.

Одно из возможных решений — это использовать генераторы Python:

def multipliers():
    for i in range(4): yield lambda x : i * x 

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

def multipliers():
    return [lambda x, i=i : i * x for i in range(4)]

Еще один вариант — использовать функцию functools.partial:

from functools import partial
from operator import mul

def multipliers():
    return [partial(mul, i) for i in range(4)]

И, наконец, самый простой вариант — просто заменить квадратные скобки на круглые:

def multipliers():
    return (lambda x : i * x for i in range(4))

Вопрос № 3

Что будет в результате выполнения данного кода? Объясните ваш ответ.

class Parent(object):
    x = 1

class Child1(Parent):
    pass

class Child2(Parent):
    pass

print(Parent.x, Child1.x, Child2.x)
Child1.x = 2
print(Parent.x, Child1.x, Child2.x)
Parent.x = 3
print(Parent.x, Child1.x, Child2.x)
Посмотреть ответ

Результатом выполнения данного кода будет:

1 1 1
1 2 1
3 2 3

Многих удивляет, что последняя строка вывода — это 3 2 3, а не 3 2 1. Почему изменение значения Parent.x также меняет значение Child2.x, но в то же время не меняет значение Child1.x?

Ответ заключается в том, что в Python переменные класса обрабатываются компилятором как словари. Если имя переменной не найдено в словаре текущего класса, то поиск продолжается вверх по иерархии (то есть, по всем родительским классам) до тех пор, пока не будет найдено имя указанной переменной. Если имя указанной переменной нигде не будет найдено, то возникает ошибка AttributeError.

Следовательно, присвоение x = 1 в родительском классе делает переменную класса x (со значением 1) ссылочной в данном классе и во всех его дочерних классах также. Вот почему первый оператор print выдает 1 1 1.

Соответственно, если какой-либо из его дочерних классов переопределяет это значение (например, когда мы выполняем инструкцию Child1.x = 2), то значение изменяется только в этом дочернем классе. Вот почему второй оператор print выдает 1 2 1.

И наконец, если значение изменяется в родительском классе Parent (например, когда мы выполняем инструкцию Parent.x = 3), это изменение отражается также на всех его дочерних классах, которые еще не переопределили это значение (которое в этом случае будет Child2). Вот почему третий оператор вывода выводит 3 2 3.

Вопрос № 4

Что будет в результате выполнения данного кода, написанного на Python 2? Объясните ваш ответ.

def div1(x,y):
    print "%s/%s = %s" % (x, y, x/y)
    
def div2(x,y):
    print "%s//%s = %s" % (x, y, x//y)

div1(5,2)
div1(5.,2)
div2(5,2)
div2(5.,2.)

Также скажите, что изменится при выполнении данного кода в Python 3 (разумеется, предположив, что синтаксис оператора print будет изменен соответствующим образом?

Посмотреть ответ

В Python 2 результат выполнения данного кода будет следующим:

5/2 = 2
5.0/2 = 2.5
5//2 = 2
5.0//2.0 = 2.0

По умолчанию Python 2 автоматически выполняет целочисленное деление, если оба операнда являются целыми числами. В результате 5/2 дает 2, а 5/22,5.

Заметим, что вы можете переопределить такое поведение в Python 2, добавив следующую строку:

from __future__ import division

Также заметим, что оператор «//» будет всегда производить целочисленное деление, вне зависимости от типа операндов. Вот почему 5.0 // 2.0 дает 2.0 даже в Python 2.

Python 3, однако, ведет себя иначе. Он не выполняет целочисленное деление, если оба операнда являются целыми числами. Поэтому в Python 3 результат будет следующим:

5/2 = 2.5
5.0/2 = 2.5
5//2 = 2
5.0//2.0 = 2.0

Вопрос № 5

Что будет в результате выполнения данного кода?

list = ['a', 'b', 'c', 'd', 'e']
print list[10:]
Посмотреть ответ

Данный код выдаст в виде результата пустой список [], а ошибка IndexError не возникнет.

Как известно, попытка доступа к элементу списка с использованием индекса, превышающего число элементов (например, операция list[10] в списке выше), приводит к ошибке IndexError. Однако, попытка получить доступ к срезу списка с начальным индексом, превышающем количество элементов в списке, не приведет к IndexError и просто вернет пустой список.

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

Вопрос № 6

Рассмотрим следующий фрагмент кода:

list = [ [ ] ] * 5
list  # Результат?
list[0].append(10)
list  # Результат?
list[1].append(20)
list  # Результат?
list.append(30)
list  # Результат?

Какой будет результат выполнения строк 2, 4 , 6 и 8? Объясните ваш ответ.

Посмотреть ответ

Результат будет следующим:

[[], [], [], [], []]
[[10], [10], [10], [10], [10]]
[[10, 20], [10, 20], [10, 20], [10, 20], [10, 20]]
[[10, 20], [10, 20], [10, 20], [10, 20], [10, 20], 30]

И вот почему.

Первая строка интуитивно понятна и проста для понимания. Выражение list = [ [ ] ] * 5 просто создает список состоящий из 5 пустых списков.

Но ключевым моментом для понимания всей этой задачи является то, что выражение list = [ [ ] ] * 5 не создает список, состоящий из пяти различных списков, а создает список, состоящий из пяти ссылок на один и тот же пустой список. Осознавая это, нам не составит труда понять все остальные результаты.

Выражение list[0].append(10) добавляет 10 в первый список. Но так как все пять списков ссылаются на один и тот же список, ответ будет [[10], [10], [10], [10], [10]].

Сходным образом, выражение list[1].append(20) добавляет 20 во второй список, но опять это изменение затрагивает все пять внутренних списков. Результат будет вот такой: [[10, 20], [10, 20], [10, 20], [10, 20], [10, 20]].

А вот выражение list.append(30), напротив, добавляет новый элемент во внешний список, и ответ будет следующим: [[10, 20], [10, 20], [10, 20], [10, 20], [10, 20], 30].

Вопрос № 7

Дан список из N чисел. Используя единичное представление списков, создайте новый список, элементы которого удовлетворяли бы следующим условиям:

  1. все числа должны быть четные;
  2. из первоначального списка должны быть взяты только элементы с четными индексами.

Например, если элемент list[2] является четным, то он должен быть включен в новый список, так как у него еще и четный индекс. Если же элемент list[3] также является четным, то он, несмотря на это, в результирующий список не попадает, так как его индекс является нечетным.

Посмотреть ответ

Простейшее решение данной задачи выглядит следующим образом:

[x for x in list[::2] if x%2 == 0]

Например, пусть дан следующий список:

list = [ 1 , 3 , 5 , 8 , 10 , 13 , 18 , 36 , 78 ]

Тогда наше преставление списков вычислит следующий список:

[10, 18, 78]

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

Вопрос № 8

Дан следующий подкласс словаря (класс, наследующий от класса dict):

class DefaultDict(dict):
  def __missing__(self, key):
    return []

Будет ли работать код, приведенный ниже? Почему да или почему нет?

d = DefaultDict()
d['florp'] = 127
Посмотреть ответ

На самом деле, данный код будет работать с объектом стандартного словаря в Python 2 или 3 — это совершенно нормально. Однако, созданный подкласс с данным кодом правильно работать не сможет. Потому что метод __missing__ возвращает значение, но не изменяет сам словарь:

d = DefaultDict()
print d
{}
print d['foo']
[]
print d
{}

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

Вот обновленный пример кода, который будет обновлять словарь и возвращать правильные значения:

class DefaultDict(dict):
    def __missing__(self, key):
        newval = []
        self[key] = newval
        return newval

Начиная с версии 2.5 такой объект доступен в стандартной библиотеке Python.

Вопрос № 9

Как бы вы организовали юнит-тестирование следующего кода?

async def logs(cont, name):
    conn = aiohttp.UnixConnector(path="/var/run/docker.sock")
    async with aiohttp.ClientSession(connector=conn) as session:
        async with session.get(f"http://xx/containers/{cont}/logs?follow=1&stdout=1") as resp:
            async for line in resp.content:
                print(name, line)
Посмотреть ответ

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

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

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

keep_running = True

async def logs(cont, name):
    conn = aiohttp.UnixConnector(path="/var/run/docker.sock")
    async with aiohttp.ClientSession(connector=conn) as session:
        async with session.get(f"http://xx/containers/{cont}/logs?follow=1&stdout=1") as resp:
            async for line in resp.content:
                if not keep_running:
                    break
                print(name, line)

Здесь любой из асинхронных операторов может иметь побочный эффект изменения глобальной переменной keep_running.

Вопрос № 10

Каким образом вы бы могли вывести на экран все функции в модуле?

Посмотреть ответ

Для перечисления функций в модуле используется метод dir(). Вот пример кода:

import some_module
print dir(some_module)

Вопрос № 11

Дан список, состоящий из целых чисел. Напишите функцию, выводящую на экран наименьшее целое число, которое отсутствует в данном списке и не может быть получено суммированием его элементов (двух и более). Например, для списка a = [1,2,5,7] таким числом будет 4, а для списка a = [1,2,2,5,7] — 18.

Посмотреть ответ
import  itertools
sum_list = []
stuff = [1,2, 5, 7]
for L in range(0, len(stuff)+1):
    for subset in itertools.combinations(stuff, L):
        sum_list.append(sum(subset))

new_list = list(set(sum_list))
new_list.sort()
for each in range(0,new_list[-1]+2):
    if each not in new_list:
        print(each)
        break