Стать 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-разработчика. Это первая часть материала. К тому же, это моя первая статья на Хабре. Проба пера. Надеюсь, что материал оказался для вас полезным и интересным. Я планирую продолжить его и описать еще часть ответов на основные вопросы.

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

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

Комментарии (26)


  1. ya_boiko Автор
    05.03.2022 14:33

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


    1. ya_boiko Автор
      05.03.2022 15:33

      Поправили, сейчас отступы верные


  1. anonymous
    00.00.0000 00:00


  1. anonymous
    00.00.0000 00:00


    1. ya_boiko Автор
      05.03.2022 15:14

      Да, здесь у меня небольшая опечатка, исправил
      Спасибо!


  1. ya_boiko Автор
    05.03.2022 15:53

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

    В приведенном примере неслучайно используется изменяемый тип данных. Для неизменяемых типов переменные с одинаковым значением будут иметь одинаковый адрес памяти.

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

    Предложение-комментарий, которое я приведу сразу ниже, относится к предыдущему блока кода, где указан пример со списками:

    В приведенном примере неслучайно используется изменяемый тип данных.

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

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


    1. 16bc
      06.03.2022 04:25

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


      1. ya_boiko Автор
        06.03.2022 04:35

        Спасибо за вопрос. Мой комментарий выше не совсем верный, я внес исправления в ответ на вопрос в статье.

        Какая сложность присвоения закешированного значения я, к сожалению, точно не могу сказать. Думаю, что кэш в Python - это хеш-таблица. Поэтому, возможно, сложность O(1).


  1. philosoph
    05.03.2022 17:36
    +2

    "Для неизменяемых типов переменные с одинаковым значением будут иметь одинаковый адрес памяти"

    Это ошибочное утверждение. Оно работает только на тех значениях, которые закэшированы интерпретатором (Ну механизм кэширования для меня не вполне ясен, но это не суть) Поэтому is будет показывать тождество объектов лишь на небольшом количестве числовых значений. Попробуйте присвоить переменным одно и то же, но большое число, или одну и ту же строку. == будет показывать равенство, а is - вернёт false.


    1. ya_boiko Автор
      05.03.2022 18:00

      Спасибо, это важное для меня замечание. Я попробую и приведу свой пример


  1. suhanoves
    05.03.2022 18:36

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


    1. ya_boiko Автор
      05.03.2022 19:23

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

      Под поиском я понимаю поиск конкретного значения, по таблице из документации вижу, что есть две операции - get item и x in s. Правильнее будет вместо одной операции "поиск" указать эти две операции, как считаете?


  1. AMDmi3
    05.03.2022 18:55
    +3

    >Для неизменяемых типов переменные с одинаковым значением будут иметь одинаковый адрес памяти

    Не будут, хотя это и возможно в частных случаях.

    >Изменяемые аргументы передаются по ссылкам, а неизменяемые – по значениям

    Всегда по ссылкам. Ваши примеры не эквивалентны, поскольку some_arg.append() - изменение, `some_arg = some_arg + 1` - создание нового объекта и сохранение ссылки на него в переменную.

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

    Так всё-таки, когда?

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

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

    >Спор касался способа хранения в памяти списка (list).

    Странно спорить об однозначно определённый вещах. Не так явно как могло быть, но всё же задокументированных https://docs.python.org/3.9/glossary.html#term-list


    1. ya_boiko Автор
      06.03.2022 04:41

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


  1. 32bit_me
    05.03.2022 21:34
    +1

    Список представлен в памяти массивом. Вот фрагмент из CPython (listobject.h):

    typedef struct {

    PyObject_VAR_HEAD

    /* Vector of pointers to list elements. list[0] is ob_item[0], etc. */

    PyObject *ob_item;

    ...

    Py_ssize_t allocated;

    } PyListObject;


    1. ya_boiko Автор
      06.03.2022 02:36

      Да, нашел перевод статьи https://habr.com/ru/post/273045/. В ней как раз описывается как работает list "изнутри". Это действительно массив, я оказался прав в том споре, спасибо!


  1. pomponchik
    06.03.2022 00:04

    None — это не тип данных, а объект NoneType.


    1. ya_boiko Автор
      06.03.2022 00:52

      Спасибо! Да, действительно. Исправляю недочет

      print(type(None))
      # <class 'NoneType'>


  1. EzikBro
    06.03.2022 00:52

    Но если переменная some_arg имеет, например, тип int (неизменяемый тип), то в функцию передается именно значение переменной, а не ссылка на неё.

    def foo(value):
        print(id(value))
        value += 1
        print(id(value))
       
    a = 1000
    print(id(a)) # 1588669723440
    foo(a)       # 1588669723440 1588669722640
    print(id(a)) # 1588669723440

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


    1. ya_boiko Автор
      06.03.2022 00:58

      Да, действительно. Круто, что вы нашли такой яркий пример, спасибо!


    1. ya_boiko Автор
      06.03.2022 01:43

      Тоже нашел несколько примеров. Давайте разберемся. Пример со списком:

      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 передается в функцию, значение аргумента внутри функции изменяется и меняется адрес памяти, а вне функции остается тот же адрес памяти и то же значение, что и при объявлении.

      Думаю, что именно в этом и разница. Можно утверждать, что изменяемые передаются по ссылкам, а неизменяемые - по значениям.

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


      1. bDrwx
        07.03.2022 14:46

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

        и изменяемые, и неизменяемые передаются по ссылкам.

        Понял только тогда, когда начал писать комментарий. Вы утверждаете, что все параметры передаются в функцию по ссылке, а вот их дальнейшая обработка отличается. При изменении мутабельного объекта происходит его модификация, т.е. id(mutable) остается неизменным, а при из изменении immutable создается новый объект в памяти, те меняется id. Что в принципе очень логично, операции над неизменными объектами приводят к порождению новых немутабельных объектов. А вот про дизайн языка и причины по которым в python встречаются изменяемые и не изменяемые структуры данных было бы интересно почитать.

        Спасибо за статью!


    1. ya_boiko Автор
      06.03.2022 04:38

      Посидел, разобрался, подумал. Конечно, вы правы, я соглашусь с вами


  1. worldmind
    06.03.2022 15:20

    Переходил с перла на питон, тоже было трудно найти работу, мои заметки https://habr.com/ru/post/426277/


  1. lookid
    06.03.2022 15:28

    -- Подзубрю и пойду кодить, кек. Зачем знать, только зубрить ответы!

    -- Ой, ой! А почему Эльбрус не Интел? А где русский Тесла? Ой, ой! В ЕС и США не учатся тоже! Я видел! Я знаю!


  1. noviqohabr
    06.03.2022 20:26

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