Переменной в Python называют имя, связанное с объектом.
Объектом в Python называют отдельный фрагмент данных, лежащий в оперативной памяти компьютера. Этот фрагмент данных состоит из типа объекта и значения объекта.
Как хранятся данные объектов
Оперативная память компьютера это место, в котором хранятся данные, с которыми компьютер активно работает в данный момент. Сейчас мы сосредоточимся на том, что находится в опреативной памяти между выполнением инструкций из нашей программы на Python.
Давайте попробуем понять, какие данные окажутся в памяти после выполнения вот такой инструкции:
a = "Hello World"Представим, что перед нами лист бумаги, на котором написано всё то, что компьтер сохраняет в оперативную память. Как уже было сказано, в оперативной памяти хранится то, с чем компьютер работает в данный момент. Ну, кажется логичным, что там должна быть записана строка с текстом Hello World. Это действительно так, компьютер загрузит в оперативную память эту строку. Давайте отразим это на бумаге:
Эта аналогия с листом бумаги очень грубая, но её достаточно чтобы понимать, как работает программа на Python.
Python с данными работает как с объектами, и наша строка не является исключением. В оперативной памяти выделилось место под наш объект, записались его тип и значение. Поскольку это строка, тип объекта будет str.
Но что дальше? Просто записать данные в оперативную память немного бессмысленно, поскольку после завершения программы эти данные просто сотрутся. Что можно сделать полезного с ними? Например, можно их напечатать в консоль. Например вот так...
a = "Hello World"
print(a) # Hello WorldДавайте разбираться, что именно при этом происходит. Тут важно понимать, что Python воспринимает этот код не как что-то цельное, а как набор из двух отдельных инструкций, выполняемых по отдельности. Для первой инструкции мы уже поняли, что интерпретатор запишет в память строку "Hello World". Но что он сделает дальше, при выполнении второй инструкции? Он находится в такой ситуации:
Инструкция, которую он должен выполнить:
print(a)Данные в оперативной памяти, с которыми он может работать при её выполнении:
Как он должен понять, что такое a? Он с такими вводными это никак не сможет понять. В опретивной памяти должна быть ещё информация о том, что обозначает имя a. На самом деле после выполнения инструкции
a = "Hello World"В опреативной памяти окажется следующее:
И исходя уже из этого, Python поймёт, что обозначает имя a. И соответсвенно напечатает объект, с которым это имя связано.
Получается, что смотря на данные в опративной памяти, теперь мы можем точно сказать, что произойдёт при выполнении нашей второй инструкции. Именно так Python и определяет, что ему надо сделать.
Про ссылку на объект, которая изображена на диаграмме точкой рядом с именем a, можно думать как про адрес дома. Адрес дома ведь это тоже по сути просто некоторые данные, и мы, люди, понимаем, как можно используя эти данные определить, где дом находится. Ссылка это тоже некоторые данные, находящиеся в оперативной памяти (например число). Но нам, людям, сложно будет понять, где находится память, даже если мы знаем эти данные. Вот например число 141651. Как определить, где находится объект в памяти, зная это число? Мы не знаем, но Python знает как это сделать. Мы же просто для понимания будем рисовать стрелочку на место в памяти, Python сам поймёт как найти объект, который лежит в этом месте.
Рассмотрим другой пример. Есть такой код на Python:
b = [1, "Hello world"]Тут есть уже понятная нам вещь: появляется имя, за именем скрывается ссылка, то есть адрес на какое-то место в оперативной памяти. Но что будет в этом месте? Как нам это отобразить на бумаге?
Можно было бы подумать, что в этом месте будут данные единицы, данные строки Hello world. Но это не совсем так.
Дело в том, что при выполнении этой строки будет задействовано не одно место в памяти, а сразу три. А если считать имя, то даже четыре. В понимании Python тут есть три объекта:
Один из этих объектов является нашим списком. Список хранит информацию о том, где находятся другие два объекта. Иначе говоря, список хранит ссылки на свои элементы, а не сами элементы.
Вообще, когда в фрагменте кода вы видите сразу несколько кусочков, выполнив каждый из которых в принципе можно получить объект, обычно Python работает с таким кодом создавая много отдельных объектов, а не помещая всё в один. Вот пример небольшого кода с большим количеством самостоятельных кусочков, каждый из которых будет соответствовать отдельному объекту в памяти:
a = {"b": 1, "c": [2]}Работа с данными объектов
Тип уже созданного объекта никак нельзя изменить. Если вы создали список, вы не можете его изменить так, чтобы он стал строкой, числом, или чем-то ещё. Физически конечно вы можете перезаписать данные о типе объекта в оперативной памяти, но через обычный Python код эту возможность сознательно ограничили. Но значение некоторых объектов можно изменить. Например, список поддерживает добавление новых элементов: то есть можно изменить его значение, добавив туда данные о месте нахождения ещё одного объекта:
lst = []lst.append(1)Однако значение некоторых объектов никак нельзя изменить, и нужно запомнить, какие объекты являются неизменяемыми. Начнём с того, что нельзя изменить объекты вот с этими типами: int, float, str, bool.
Так, если вы пишете код:
a = 1
a = a + 1Смысл его такой:
На первой строке мы получили объект типа int со значением один и теперь имя a обозначает ссылку на этот объект.
На второй строке, во-первых, в результате вычисления выражения a + 1 мы получили новый объект, с типом int и значением два. Во-вторых, мы изменили ссылку имени a: теперь оно указывает не на объект 1, а на объект 2. При этом мы никак не изменяли сами объекты.
То же самое относится к любым операциям со строками: данные объекта типа str не могут быть изменены.
a = "Hello"
b = a + " World!" # объект по ссылке `a` остался неизменнымВажно понимать, что управление переменными и объектами происходит очень небольшим количеством способов — небольшим количеством механизмов языка. Сейчас мы рассмотрели
- Получение объектов через вычисление выражений (примеры таких выражений:
1,a + 1) - Изменение уже известных объектов в ходе вычисления выражений (например
some_list.append(1)) - Создание или изменение ссылки переменной через присваивание (например
a = 1,a = 1 + 2,lst = [4],a = some_list)
Обратите внимание, что каждый из механизмов происходит в соответствии с конкретным текстом в программе, и приводит к конкретным изменениям в оперативной памяти. А то, что находится в оперативной памяти, напрямую определяет дальнейший ход выполнения программы.
В целом, эти механизмы будут нас сопровождать при разработке на Python постоянно, потому важно уметь бегло считывать использование этих механизмов при чтении кода: видеть их в коде и понимать, как именно они отражаются на состоянии программы. Глядя на код, вы должны видеть какие из них в нём присутствуют, и понимать последствия их работы для оперативной памяти:
a = 1 + 1Оптимизации использования оперативной памяти
Рассмотрим такой код.
str_1 = "Hello"
str_2 = "Hello"
lst_1 = []
lst_2 = []
print(str_1, str_2, lst_1, lst_2)Логично будет предположить, что после выполнения этого кода, в оперативной памяти мы увидим следующее
Но давайте подумаем, есть ли смысл в том, чтобы держать в памяти все 4 объекта? Тут же есть два дубля: в памяти два раза лежит одно и то же. Можно сохранить в памяти всего два объекта, и результат выполнения не поменяется. Допустим, интерпретатор Python тоже так решил, и вот в этих строках
str_1 = "Hello"
str_2 = "Hello"
lst_1 = []
lst_2 = []
print(str_1, str_2, lst_1, lst_2)Не создавал объект, а возвращал уже имеющийся, точно такой же объект. В таком случае, получится так
Но что, если мы захотим добавить в один из списков элемент?
str_1 = "Hello"
str_2 = "Hello"
lst_1 = []
lst_2 = []
lst_1.append(str_1)
print(str_1, str_2, lst_1, lst_2)В таком случае получилось следующее
И в выводе у нас будет
Hello Hello ['Hello'] ['Hello']Что, понятное дело, не то, чего мы ожидаем. Мы хотим, чтобы изменился только один список; мы ожидаем, что имена указывают на разные места в памяти, и хотим изменить только одно из них. Поэтому так оптимизировать хранение списков у нас не получится.
Но что насчёт строк? А со строками дело обстоит интереснее. Они неизменяемы. Вы никаким образом не сможете изменить то, что находится внутри объекта с типом str. Поэтому для них такая оптимизация будет работать. По этой причине Python оставляет за собой возможность вернуть вам тот же самый объект. Поэтому вот этот код
str_1 = "Hello"
str_2 = "Hello"
lst_1 = []
lst_2 = []
lst_1.append(str_1)
print(str_1, str_2, lst_1, lst_2)Может привести как к такому состоянию
Так и к такому
И, в целом, нам пока не так важно, какой из вариантов произойдёт. Просто запомните, что для некоторых неизменяемых объектов реализация Python может переиспользовать уже существующий объект.
Задания
Задание 1
name = "Tom"
age = 20
message = name + " is " + str(age)
age = age + 1
name = name + " Smith"
final_message = name + " is " + str(age)Попробуйте самостоятельно подробно объяснить, что происходит на каждой строке:
- Какие объекты вычисляются (с какими типами и значениями)?
- Какие имена начинают ссылаться на какой объект?
- На какие объекты ссылаются имена name, age, message и final_message после каждой строки?
- Изменяется ли какой-нибудь уже существующий объект?
Задание 2
Напишите несколько программ на Python.
Ниже приведены картинки, на которых изображено состояние программы. Вы должны для каждой картинки написать хотя бы две программы, которые приводят именно к такому состоянию.
Пример
Тут мы видим, что есть два имени. Первое имя ссылается на объект типа list, в значении имеющий две ссылки: на объект с типом int и значением 1, и на объект с типом int и значением 2. Второе имя ссылается на тот же самый объект типа list.
К примеру, вот такой код приведёт к нужной нам ситуации:
items = [1, 2]
alias = itemsНо важно понимать, что к состоянию программы, приведённому на картинке, приведут ещё куча других вариантов кода. И потому можно сказать, что результат выполнения этих вариантов будет идентичным. Вот некоторые из них:
items = [1] # есть список, есть имя которое на него ссылается
items.append(2) # изменили список, теперь он такой как нужен
alias = items # теперь на него ссылается второе нужное имяitems = [1] # то же самое
alias = items # сразу сделали второе имя
alias.append(2) # теперь список такой как надоitems = list() # сделали сначала пустой список и первое нужное имя на него указывает
alias = items # теперь на него ссылается второе нужное имя
items.extend([1, 2]) # изменили список: там теперь ссылки на все нужные объектыПример, где лаконичность намного хуже. Но тем не менее его результат точно такой же как у остальных:
[1, 2] # сделали нужный список
# но никакое имя на него не указывает, так что он сразу
# удалился, не оказав влияния на финальное состояние
items = 1
# используем имя, ссылающееся на единицу, чтобы сделать
# список в котором есть ссылка на ту же единицу
items = [items]
alias = items
# используем ссылки на единицу которые у нас есть чтобы
# получить двойку. и изменяем список: добавляем в него
# ссылку на эту двойку.
alias.append(alias[0] + alias[0])Выполнение каждого из этих фрагментов кода приводит к тому, что состояние программы становится таким, как изображено на картинке выше.
Указание: представляйте в уме или рисуйте промежуточные состояния кода который вы пишете. Финальное состояние должно быть в точности таким какое нарисовано, но промежуточные могут быть очень разными, как в этом примере.
Примечания:
- Вы можете двигать блоки на картинках.
- Вы можете навести мышку на начало или конец стрелки, чтобы понять, куда именно она указывает.
- Вы можете нажать
Full pageрядом с диаграммой чтобы развернуть её на всю страницу.