Pull to refresh

«Что я получу, если смешаю корень златоцветника и настойку полыни?» или 10 вопросов для Junior Python-разработчика

Reading time7 min
Views19K

Стать Python-разработчиком после PHP оказалось сложнее, чем подняться на Оштен (гора Кавказского хребта, 2804 метра). Нет, подняться на Оштен вполне посильная задача, нужна небольшая подготовка. Вот и я думал, что три года опыта коммерческой разработки на PHP мне дадут крылья. Это должна была быть "небольшая подготовка", чтобы сразу оказаться на середине горы или хотя бы у её подножия. Все оказалась слегка не так. Больше половины компаний не отвечают. Возможно, увидели, что у меня нет Python в разделе с опытом работы. Остается только догадываться. Кто-то не сообщает об оценке тестового задания. Каков мой настрой? Что ж, его качает, но все же просто дайте мне точку опоры (пару лет опыта на Python), и я переверну Землю!

Если ты сейчас решаешь похожий вопрос – советую продолжать учиться, практиковаться и договариваться о новых собеседованиях. Чтобы не растерять знания по дороге, я сделал для себя небольшую базу знаний с ответами на основные вопросы, которые касаются Python. Я на них часто отвечаю при первой беседе с компанией и на техническом интервью. Для подготовки этой базы я изучал публичные собеседования на должность Junior Python-разработчика и опыт технических интервьюеров. Помогли и собственные карандашные записи со встреч. Ты спросишь: "Зачем нужны эти знания, ты можешь мне сказать?" А я отвечу как дневник одного злого волшебника: "Нет. Но я могу показать". На одном собеседовании технический специалист сказал, что я знаю о Python больше, чем некоторые программисты уровня Middle. Жаль, что тогда мне не хватило знаний о базах данных.

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

Какие основные типы данных есть в Python?

В Python основные типы данных делятся на две группы: неизменяемые (immutable) и изменяемые (mutable). К неизменяемым типам данных относятся:

  • NoneType

  • bool (True или False)

  • int (так же float, long, complex)

  • str

  • tuple – кортеж

  • frozenset – неизменяемое множество (содержит уникальные значения)

К изменяемым типам данных относятся:

  • list – список

  • dict – словарь

  • set – множество (содержит уникальные значения)

Значения 0 и False, а так же 1 и True считаются эквивалентными, поэтому они объединяются при создании множества (set или frozenset).

# tuple (кортеж)
newTuple = (1, 3.14, "Harry", True)				

# frozenset (неизменяемое множество)
newFrz = frozenset([1, 3.14, "Harry", True, 3.14])
print(type(newFrz), newFrz)
# <class 'frozenset'>  frozenset({1, 3.14, 'Harry'})

# list (список)
newList = [1, 3.14, "Harry", True]

# dict (словарь)
newDict = {
  True: 1, 
  "pi": 3.14, 
  "name": "Harry", 
  1: True
}

# set (множество)
newSet = set([1, 3.14, "Harry", True, 3.14, 1])
print(type(newSet), newSet)
# <class 'set'>  {1, "Harry", 3.14}

Чем отличаются операторы == и is?

В Python все является объектом (экземпляром какого-либо класса). А переменная – это просто имя, которому сопоставлено некоторое значение. Оператор == проверяет равенство значений.

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

list1 = [1, 3.14, "Harry", True]
list2 = [1, 3.14, "Harry", True]
print(id(list1), id(list2), list1 == list2, list1 is list2)
# 937664  937984  True  False 

list3 = list1
print(id(list1), id(list3), list1 == list3, list1 is list3)
# 937664  937664  True  True

В комментариях к статье мне указали, что только в частном случае (а не всегда), переменные с одинаковым значением могут быть идентичными. Пример с переменными типа float:

a = 3.14
b = 3.14
print(id(a), id(b), a == b, a is b)
# 170944  170944  True  True

Почему a is b возвращает True? Python (CPython, если быть точнее) в целях производительности кэширует некоторые строки и числа, поэтому возможны такие казусы.

Ниже приведен пример класса, любой экземпляр которого всегда равен (==) всему, чему угодно. В то же время, экземпляр этого класса не является (is) другим экземпляром этого же класса и ничем другим кроме самого себя.

class AlwaysEqual(object):
  def __eq__(self, other):
    return True

instance = AlwaysEqual()
print(instance == 42)      # True
print(instance is 42)      # False
print(instance is AlwaysEqual())      # False
print(instance is instancе)      # True

instancе2 = instance
print(instancе2 is instance)     # True

Спасибо комментаторам этой статьи и ребятам, которые ответили на вопрос на Хабр Q&A.


Как в Python передаются аргументы в функцию (изменяемые и неизменяемые)?

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

def some_function(some_arg: list = []):
  some_arg.append(1)
  return some_arg

print(some_function())      # [1]
print(some_function())      # [1, 1]
print(some_function())      # [1, 1, 1]

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

Но если переменная some_arg имеет, например, тип int, то каждый раз при вызове функции аргументу some_arg будет присваиваться 0. Поэтому, вызвав новую функцию три раза, результат будем следующим:

def some_function(some_arg: int = 0):
  some_arg = some_arg + 1
  return some_arg

print(some_function())      # 1
print(some_function())      # 1
print(some_function())      # 1

Стоит разобрать примеры с передачей переменных разных типов в функцию.

Пример со списком:

def foo(value):
  print("a in function", id(value), value)
  value[0] = 1
  print("a in function", id(value), value)
  
a = [1000]
print("a", id(a), a)
foo(a)
print("a", id(a), a)

# a 60795240 [1000]
# a in function 60795240 [1000]
# a in function 60795240 [1]
# a 60795240 [1]

Видно, что при изменение значения a[0] внутри функции, значение a[0] изменилось и вне ее. Адрес памяти один и тот же. Значит в функцию a передается по ссылке.

Другой пример уже с числом:

def foo(value):
  print("a in function", id(value), value)
  value = 1
  print("a in function", id(value), value)
  
a = 1000
print("a", id(a), a)
foo(a)
print("a", id(a), a)

# a 61223712 1000
# a in function 61223712 1000
# a in function 1721690624 1
# a 61223712 1000

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

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

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


Что такое *args и **kwargs? Чем представлены?

*args – аргумент, который принимает в себя неограниченное количество позиционных аргументов функции. В Python *args представлен как кортеж (tuple). Пример функции с *args:

def some_function(*some_args):
  for i, x in enumerate(some_args):
    print(f'[{i}] = {x}')

some_function(10, 25, 33)

# [0] = 10
# [1] = 25
# [2] = 33

**kwargs – аргумент, который принимает в себя неограниченное количество аргументов функции с помощью ключевых слов. В Python **kwargs представлен как словарь (dict). Пример функции с **kwargs:

def some_function2(**some_args):
  for i, x in some_args.items():
    print(f'[{i}] = {x}')

some_function2(one=10, two=25, three=33)

# [one] = 10
# [two] = 25
# [three] = 33

Что такое аннотации типов? Зачем они нужны?

Аннотация типов – это подсказка о типе данных к переменной или к аргументу функции. Пример:

price: int = 5
title: str

def some_function(x: str, y: int) -> str:    
	return f'x = {x}, y = {y}'

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

Аннотации типов выполняются не в runtime.


Что происходит при операции a = b?

При присваивании b значения a, переменная b всегда будет ссылаться на тот же адрес памяти, что и a.

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

Изучить подробнее разницу между созданием объекта и изменением объекта можно с помощью функции id(object). Но помните про то, что Python сохраняет некоторые значения в кэш.


Что такое тернарный оператор? Как записывается?

Тернарный оператор – это обычная конструкция if, которая для удобства читаемости и лаконичности синтаксиса записана в одну строку. Пример кода:

result = "A больше B" if a > b else "A не больше B"

Как оценивается сложность алгоритмов? Что такое нотация Big O?

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

  • O(1) – константная

  • O(log2(n)) – логарифмическая

  • O(n) – линейная

  • O(n * log(n)) – квазилинейная

  • O(n^2) – квадратичная

  • O(n!) – факториальная


Какая сложность основных операций в списке (list) и словаре (dict)?

В списке:

Средний случай

Худший случай

Append

O(1)

O(1)

Pop last

O(1)

O(1)

Pop intermediate

O(n)

O(n)

Insert

O(n)

O(n)

Get Item

O(1)

O(1)

Set Item

O(1)

O(1)

Delete Item

O(n)

O(n)

x in s

O(n)

min(s), max(s)

O(n)

В словаре:

Средний случай

Худший случай

Get Item

О(1)

О(n)

Set Item

О(1)

О(n)

Delete Item

О(1)

О(n)

k in d

О(1)

О(n)

Сложность этих и других операций указана в документации.


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

На Хабре я нашел хорошую статью, где как раз описывается внутреннее устройство list в Python. Он хранится как массив. Об этом говорится и в документации. Спасибо комментаторам этой статьи, что помогли разобраться.

В рамках этой статьи я описал ответы на несколько основных вопросов, которые мне задают на собеседованиях на должность Junior Python-разработчика. Это первая часть материала. К тому же, это моя первая статья на Хабре. Проба пера. Надеюсь, что материал оказался для вас полезным и интересным. Я планирую продолжить его и описать еще часть ответов на основные вопросы.

Желаю вам всего доброго и мирного неба над головой.

Шалость удалась!

Tags:
Hubs:
Total votes 16: ↑14 and ↓2+16
Comments36

Articles