Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров19K
Всего голосов 16: ↑14 и ↓2+16
Комментарии36

Комментарии 36

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

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

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Всегда по ссылкам. Ваши примеры не эквивалентны, поскольку 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

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

Список представлен в памяти массивом. Вот фрагмент из 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;

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

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

Но если переменная 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

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

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

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

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

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

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

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

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

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

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

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

 По-разному происходит изменение мутабельного и немутабельного объекта.

Немного странное высказывание, т.к. у немутабельного объекта изменение не происходит никогда.

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

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

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

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

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

Boolean — один из двух целочисленных типов данных.
Т.н. код типа
count += (smth1 == smth2)
посчитает все True. Скобки здесь для читаемости.

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

На счёт строк не знаю, а вот integer от -5 до 256 включительно точно кэшируется.

Если не секрет, а а почему именно python?

Спасибо за ваш комментарий, по поводу строк - сам проводил эксперимент с большими и малыми строкам. is показывал True

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

Подскажите, как так вышло, что int - неизменяемый тип, но я могу написать вот такое:

a = 1
a += 1

И значение a при этом изменится.

Будет создан новый объект типа int со значением 2 и присвоена ссылка переменной a на этот объект


a = 1
print(id(a)) #11368768

a+=1
print(id(a)) #11368800

Ну у меня был вопрос скорее про синтаксис и терминологию. Странно выглядит, что переменная имеет неизменяемый тип, но я могу изменить её значение. Вот строки, например - действительно неизменяемы:

a = 'qwe'
a[1] = 'r'
print(a)
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    a[1] = 'e'
TypeError: 'str' object does not support item assignment
Ну так попробуйте проделать то же самое с числом:
num = 12
num[1] = 3

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

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

Тут наверно надо уточнить, что вместо args и kwargs можно написать что угодно, главное здесь - операторы * и **, принимающие позиционные и именованные аргументы.
Поэтому можно написать так:

a = [1, 2, 3]
print(*a)  # 1 2 3

b = {'a': '1', 'b': '2', 'c': '3'}
print(*[b])  # {'a': '1', 'b': '2', 'c': '3'}
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории