Подготовка к собеседованию на позицию Python-разработчика

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

Содержание статьи:

  1. Работа со списками.
  2. Отладка кода и тестирование.
  3. Массивы.
  4. Декораторы функций.
  5. Исключения.
  6. Итераторы.
  7. GIL.
  8. Передача аргументов.
  9. Вопросы вне определённых категорий.
  10. Дополнительно.
  11. Заключение.

Работа со списками

Лямбда-выражения, генераторы списков и выражения-генераторы

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

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

Лямбда-выражения с функциями map() и filter() и генераторы списков схожи, поэтому выбор одного из этих инструментов субъективен и зависит от случая. Но следует отметить, что генераторы списков выполняются несколько быстрее — вызов лямбда-функции создаёт новый стековый кадр.

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

В чём разница между списком и кортежем?

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

Самые продвинутые кандидаты скажут, что кортежи неоднородны и их использование аналогично использованию struct в языке программирования С. Списки же аналогичны привычным массивам.

Отладка кода и тестирование

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

Фундаментальный ответ на этот вопрос относится к использованию фреймворка Python — unittest.

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

Вас могут попросить описать ключевые элементы структуры unittest, а именно:

  • испытательный стенд (test fixture);
  • тестовый случай (test case);
  • набор тестов (test suite);
  • исполнитель тестов (test runner).

Mock — недавнее дополнение к модулю unittest, которое позволяет заменять часть тестируемой системы mock-объектами. Mock теперь часть стандартной библиотеки Python, доступной как unittest.mock в Python 3.3 и новее.

Как производится отладка программы на Python? Можно ли выполнить Python-код пошагово?

В Python есть встроенный модуль pdb, который определяет интерактивный отладчик для исходного кода программ Python.

python -m pdb mypyscript.py

Запуск pdb из интерпретатора:

>>> import pdb_script
>>> import pdb
>>> pdb.run('pdb_script.MyPyObj(6).go()')
> (1)()
(Pdb)

Из скриптового файла Python:

import pdb

class MyObj(object):
    count = 5
    def __init__(self):
        self.count= 9

    def go(self):
        for i in range(self.count):
            pdb.set_trace()
            print i
        return

if __name__ == '__main__':
    MyObj(5).go() 

Массивы

Рассмотрите два подхода ниже для инициализации массива и массивов. В чём разница между этими подходами и почему вам следует использовать только один из них?

# Инициализация массива -- метод 1
...
x = [[1,2,3,4]] * 3
x
[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]

# Инициализация массива -- метод 2
...
y = [[1,2,3,4] for _ in range(3)]
y
[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]

# Какой метод следует использовать и почему?

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

# Изменение массива X из предыдущего фрагмента кода:
x[0][3] = 99
x
[[1, 2, 3, 99], [1, 2, 3, 99], [1, 2, 3, 99]]
# Сомнительно, что такое вы хотели получить именно такой результат
...

# Изменение массива Y из предыдущего фрагмента кода:
y[0][3] = 99
y
[[1, 2, 3, 99], [1, 2, 3, 4], [1, 2, 3, 4]]
# А это уже то, что вполне следовало ожидать.
...

Что такое NumPy? Почему лучше использовать массивы NumPy вместо списков Python?

NumPy — пакет Python для научных вычислений, который способен работать с большими объёмами данных. Он включает в себя мощный N-мерный объект массива и набор продвинутых функций.

Также есть определённые причины, согласно которым лучше использовать массивы NumPy:

  • массивы NumPy более компактны, чем списки;
  • запись и чтение выполняются быстрее;
  • массивы более эффективны из-за увеличения функциональности списков.

Как можно создать массив NumPy в Python?

Существует два способа создания массивов NumPy:

Первый:

import numpy
numpy.array([])

Второй:

import numpy
numpy.empty(shape=(0,0))

Декораторы функций

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

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

Что такое @classmethod, @staticmethod, @property?

Декораторы @classmethod, @staticmethod и @property используются для функций, определённых внутри классов. Они ведут себя так:

class MyClass(object):
    def __init__(self):
        self._some_property = "properties классные"
        self._some_other_property = "Очень классные"
    def normal_method(*args,**kwargs):
        print("вызван normal_method({0},{1})".format(args,kwargs))
    @classmethod
    def class_method(*args,**kwargs):
        print("вызван class_method({0},{1})".format(args,kwargs))
    @staticmethod
    def static_method(*args,**kwargs):
        print("вызван static_method({0},{1})".format(args,kwargs))
    @property
    def some_property(self,*args,**kwargs):
        print("вызвано some_property getter({0},{1},{2})".format(self,args,kwargs))
        return self._some_property
    @some_property.setter
    def some_property(self,*args,**kwargs):
        print("вызвано some_property setter({0},{1},{2})".format(self,args,kwargs))
        self._some_property = args[0]
    @property
    def some_other_property(self,*args,**kwargs):
        print("вызвано some_other_property getter({0},{1},{2})".format(self,args,kwargs))
        return self._some_other_property

o = MyClass()
# Недекорированные методы работают подобно обычным, они принимают текущий экземпляр (self) как первый аргумент

o.normal_method 
# >

o.normal_method() 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>,),{})

o.normal_method(1,2,x=3,y=4) 
# normal_method((<__main__.MyClass instance at 0x7fdd2537ea28>, 1, 2),{'y': 4, 'x': 3})

# Методы класса всегда принимают класс как первый аргумент

o.class_method
# >

o.class_method()
# class_method((,),{})

o.class_method(1,2,x=3,y=4)
# class_method((, 1, 2),{'y': 4, 'x': 3})

# Статичные методы не имеют аргументов кроме тех, которые вы им назначаете при вызове

o.static_method
# 

o.static_method()
# static_method((),{})

o.static_method(1,2,x=3,y=4)
# static_method((1, 2),{'y': 4, 'x': 3})

# Декораторы properties реализуются с помощью геттеров и сеттеров. Вызывать их открыто — ошибка
# Атрибуты "только для чтения" могут быть определены геттером без сеттера (как, например some_other_property)

o.some_property
# Вызывает some_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'properties замечательны'

o.some_property()
# Вызывает some_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "", line 1, in 
# TypeError: 'str' object is not callable

o.some_other_property
# Вызывает some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# 'Великолепие'

# o.some_other_property()
# Вызывает some_other_property getter(<__main__.MyClass instance at 0x7fb2b70877e8>,(),{})
# Traceback (most recent call last):
#   File "", line 1, in 
# TypeError: 'str' object is not callable

o.some_property = "Само великолепие"
# Вызывает some_property setter(<__main__.MyClass object at 0x7fb2b7077890>,(' само великолепие',),{})

o.some_property
# Вызывает some_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})
# 'Само великолепие'

o.some_other_property = "великолепно"
# Traceback (most recent call last):
#   File "", line 1, in 
# AttributeError: can't set attribute

o.some_other_property
# Вызывает some_other_property getter(<__main__.MyClass object at 0x7fb2b7077890>,(),{})
# 'Великолепно'

Создайте логируемый декоратор

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

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

import logging

def log(func):
    """
    Логируем какая функция вызывается
    """
    
    def wrap_log(*args, **kwargs):
        name = func.__name__
        logger = logging.getLogger(name)
        logger.setLevel(logging.INFO)
    
        # Открываем файл логов для записи
        fh = logging.FileHandler("%s.log" % name)
        fmt = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
        formatter = logging.Formatter(fmt)
        fh.setFormatter(formatter)
        logger.addHandler(fh)
        
        logger.info("Вызов функции: %s" % name)
        result = func(*args, **kwargs)
        logger.info("Результат: %s" % result)
        return func
    
    return wrap_log


@log
def double_function(a):
    """
    Умножаем полученный параметр
    """
    return a*2


if __name__ == "__main__":
    value = double_function(2)

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

Исключения

Что такое исключения?

Исключительные ситуации, или исключения (exceptions) — это ошибки, обнаруженные при исполнении кода. Например, к чему приведет попытка чтения несуществующего файла? А вдруг файл был случайно удален, пока программа работала? Такие ситуации обрабатываются при помощи исключений.

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

Простейший пример исключения — деление на ноль:

100 / 0

Traceback (most recent call last):
  File "", line 1, in
>>>    100 / 0
ZeroDivisionError: division by zero

В этом случае интерпретатор сообщил об исключении ZeroDivisionError — делении на ноль.

Иерархия исключений. Какие системные исключения вам знакомы?

Исключение, которое вы не видите при выполнении кода — BaseException — это базовое исключение, которое наследует остальные.

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

  1. Системные исключения и ошибки.
  2. Пользовательские исключения.

К системным относятся:

  • SystemExit — исключение, порождаемое функцией sys.exit при выходе из программы;
  • KeyboardInterrupt — возникает при прерывании программы пользователем (обычно сочетанием клавиш Ctrl + C).
  • GeneratorExit — возникает при вызове метода close объекта generator.

Больше о работе исключений вы можете узнать в официальном руководстве.

Итераторы

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

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

В чём разница между итератором и генератором?

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

def squares(start, stop):
    for i in range(start, stop):
        yield i * i
generator = squares(a, b)

GIL

Концепция GIL заключается в том, что в каждый момент времени только один поток может исполняться процессором. Это сделано для того, чтобы между потоками не было борьбы за отдельные переменные. Исполняемый поток получает доступ ко всему окружению. Такая особенность реализации потоков в Python значительно упрощает работу с потоками и дает определенную потокобезопасность (thread safety).

Передача аргументов

Как передаются неизменяемые объекты?

Неизменяемые объекты передаются «по значению». Такие объекты, как целые числа и строки, передаются в виде ссылок на объекты, а не в виде копий объектов.

Как передаются изменяемые объекты?

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

Пример:

>>> def f(a):   # Имени a присваивается переданный объект 
...     a = 99  # Изменяется только локальная переменная
...
>>> b = 88
>>> f(b)# Первоначально имена a и b ссылаются на одно и то же число 88
>>> print(b)    # Переменная b не изменилась
88

В этом фрагменте в момент вызова функции f(b) переменной a присваивается объект 88, но переменная a существует только внутри вызванной функции. Изменение переменной a внутри функции не оказывает влияния на окружение, откуда была вызвана функция, – просто в момент вызова создается совершенно новый объект a.

Что будет выведено после второго вызова append() в коде ниже?

>>> def append(list=[]):
...    # добавление длины списка в список
...    list.append(len(list))
...    return list
...
>>> append(['a','b'])
['a', 'b', 2]
>>>
>>> append() # вызов без аргумента использует значение list по умолчанию []
[0]
>>>
>>> append() # Но что произойдёт при повторном вызове append без аргумента?

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

>>> append() # при первом вызове без аргумента используется значение по умолчанию []
[0]
>>> append() # но затем...
[0, 1]
>>> append() # последовательные вызовы расширяют список по умолчанию
[0, 1, 2]
>>> append() # и так продолжается...
[0, 1, 2, 3]

Как можно изменить применение метода append в предыдущем вопросе, чтобы избежать нежелательного поведения, описанного там?

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

>>> def append(list=None):
...    if list is None:
            list = []
        # Увеличивает длину списка
...    list.append(len(list))
        return list
...
>>> append()
[0]
>>> append()
[0]

Вопросы вне определённых категорий

Как можно поменять местами значения двух переменных внутри строки в Python?

Рассмотрим простой пример:

>>> x = 'X'
>>> y = 'Y'

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

>>> tmp = x
>>> x = y
>>> y = tmp
>>> x, y
('Y', 'X')

Но в Python существует возможность сделать это с помощью одной строки кода следующим образом:

>>> x,y = y,x
>>> x,y
('Y', 'X')

Что будет выведено последним оператором ниже?

>>> flist = []
>>> for i in range(3):
...    flist.append(lambda: i)
...
>>> [f() for f in flist] # что будет выведено?

В любом замыкании в Python переменные связываются по имени. Таким образом, в приведённой выше строке кода будет выведено следующее:

[2, 2, 2]

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

>>> flist = []
>>> for i in range(3):
...    flist.append(lambda i = i : i)
...
>>> [f() for f in flist]
[0, 1, 2]

Для чего служит ключевое слово «self»?

Ключевое слово self — переменная, которая относится к экземпляру объекта. Когда создаётся класс, явная ссылка на объект того же типа класса отсутствует. Поэтому, чтобы ссылаться на текущий класс или объект, в Python используется ключевое слово self.

class User:
    def __init__(self):
        self.name = 'Ivan Ivanov'
        self.age = 16
 
user_obj = User()
user_obj.name    # self.name содержит 'Ivan Ivanov' в качестве значения

Для чего служит ключевое слово «yield»?

Ключевое слово yield может обратить любую функцию в генератор. Оно работает подобно оператору return, с той разницей, что ключевое слово будет возвращать объект-генератор. Также функция может совершать несколько вызовов ключевого слова yield.

def testgen(index):
  weekdays = ['sun','mon','tue','wed','thu','fri','sat']
  yield weekdays[index]
  yield weekdays[index+1]

day = testgen(0)
print next(day), next(day)

#output: sun mon

Что такое __init__.py? Как импортировать класс из другого каталога?

__init__.py в основном используется для инициализации пакетов Python.

Файл __init__.py в каталоге lstm_m указывает интерпретатору Python, что этот каталог должен обрабатываться как пакет Python.

Как импортировать класс из другого каталога?

Обычно __init__.py является пустым файлом. А если нам нужно использовать lstm.py в файле run.py, то его нужно импортировать следующим образом:

from lstm_m import lstm

Кроме того, внутри папки модуля должен быть файл __init__.py, предназначенный для импорта.

Какие встроенные типы существуют в Python?

Существуют изменяемые и неизменяемые встроенные типы Python.

Изменяемые:

  • списки;
  • множества;
  • словари.

Неизменяемые:

  • строки;
  • кортежи;
  • числа.

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

Что такое docstring в Python?

Строка документации в Python (docstring) — способ документирования функций, модулей и классов. Стандарты оформления — на официальном сайте.

Как можно конвертировать число в строку?

Для преобразования числа в строку, как правило, используют встроенную функцию str(), хотя есть и другие способы, такие как "{0:d}".format(число) и "%d"%число. Если вы хотите преобразовать десятичное число в восьмеричное (oct — octal) или шестнадцатеричное (hex — hexadecimal), используйте встроенную функцию oct() или hex() соответственно.

В чём разница между Xrange и range?

Функция xrange() возвращает объект xrange, в то время как range() возвращает список и использует то же количество памяти, независимо от размера функции.

Как увидеть методы или атрибуты объекта?

Команда dir(x) возвращает отсортированный список имен атрибутов для любого переданного в нее объекта. Если ни один объект не указан, dir() возвращает имена в текущей области видимости.

Дополнительно

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

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

Заключение

Вопросы и советы, представленные в этой статье, могут быть очень ценными вспомогательными средствами для подготовки соискателей к собеседованиям. Мы надеемся, разработчики найдут их полезными для самостоятельного тестирования своих знаний перед встречей с работодателем. Но не следует забывать, что все представленные выше вопросы — один из нескольких инструментов отбора кандидатов на должность в составе определённой стратегии. Готовьтесь внимательно и тщательно. Удачи!

При подготовке использовались материалы: The Vital Guide to Python Interviewing, Must Have Python Interview Questions, 15 Essential Python Interview Questions, Python Interview Questions and Answers