Факты и мифы об именах и значениях в питоне

Статья, написанная известным специалистом Недом Батчелдером (Ned Batchelder) на основе выступления на PyCon 2015 в Монреале, Канада, по поводу двух фундаментальных концепций питона: именах (names) и значениях (values). Текст статьи применим к Python 2 и Python 3. Дата написания: 29.03.2015.

Источник: Facts and myths about Python names and values

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

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

Сегодня мы поговорим про имена (names), значения (values), операцию присвоения (assignment statement) и изменяемость (mutability).

Имя - это ссылка на значение

3

Как и во многих других языках программирования, операция присвоения в питоне создает связь между символическим именем, которое находится с левой стороны от оператора, и значением, которое находится с правой стороны. Мы говорим, что в питоне имя отсылает к значению:
x = 23

Здесь имя «х» отсылает к значению «23». Если мы вызовем имя «х» еще раз, то получим «23».
print(x) # выводим «23»

Другими словами, «х» связано с «23». На самом деле, не так важно, каким конкретно образом имя отсылает к значению. Если есть опыт в языке Си, можно считать его указателем, а если это не понятно, то не нужно беспокоиться.

Буду объяснять, что происходит, с помощью слайдов. Прямоугольная и похожая на тег фигура серого цвета обозначает имя. Стрелка показывает на его значение. На этом слайде видно, что имя «х» отсылает к целому числу «23».

Одно значение можно связывать с большим количеством имен

4

Нет такого правила, чтобы у значения было только одно имя. С помощью операции присвоения в питоне можно создать связь между значением и вторым (третьим и т.д.) именем.

x = 23
y = x

Теперь «х» и «y» оба отсылают к одному значению. Ни «х», ни «y» не являются «настоящими» именами. Они в равном положении и связаны со значением абсолютно одинаковым образом.

Имена присваиваются повторно независимо друг от друга

5

Два имени отсылают к одному и тому же значению, никакая магия не соединяет эти имена между собой. То есть, присвоение одному из них нового значения не изменяет второе:
x = 23
y = x
x = 12

Если написать «y = x», это не означает, что они остаются одинаковыми навсегда. Если присвоить новое значение «х», то оно бросит «y» в одиночестве. Можете представить себе, какой бы поднялся хаос, если бы это было не так. Еще важно отметить, что в питоне нет механизма для создания связи между именами. То есть, не получится сделать «y» постоянным псевдонимом для «х», и неважно, каким образом делается повторное присвоение к имени «х».

Значения живут пока есть связь

6

Питон отслеживает, сколько ссылок ведет к каждому значению, и автоматически очищает значения, не связанные ни с какими именами. Это называется сборкой мусора (garbage collection), то есть не нужно отдельно заниматься удалением значений. Они пропадают сами, когда становятся не нужны.

Механизм этого отслеживания в питоне - техническая деталь процесса выполнения. Но если слышали термин «подсчет ссылок» (reference counting), это важная часть процесса. Иногда очищение значения называют утилизацией (reclaiming).

Присвоение не копирует информацию

7

Это важный момент насчет присвоения. Если у значения больше одного имени, то легко запутаться и подумать, что перед нами два имени и два значения:
x = 23
y = x
# «Теперь у меня два значения: «x» и «y»!»
# НЕТ: имен - два, но значение - одно.

Если присвоение определенного значения определенному имени не копирует информацию, то нового значения никогда не создается. Операция присвоения лишь связывает имя (которое слева) со значением (которое справа). В данном случае у нас лишь одно целое число «23», «х» и «y» оба ссылаются на него, как и было показано на предыдущих слайдах.

Становится интереснее, если использовать более сложное значение, например список:
nums = [1, 2, 3]

Если присвоить списку «nums» еще одно имя, то у нас будет два имени со ссылками на один и тот же список:
nums = [1, 2, 3]
other = nums

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

Изменения можно увидеть через каждое имя

8

Теперь пересоздаем наш список и присваиваем ему оба имени «nums» и «other». Потом присваиваем другое значение имени «nums». Когда мы выводим «other», то окажется, что к нему добавлен пункт «4». Многих это удивляет.

Поскольку список всегда был один, изменение распространяется на оба имени. Присвоение на «other» не скопировало список, а операция добавления (append) не скопировала список перед добавлением нового значения. Список - один, и если его изменить через одно имя, то изменение становится видимым через все имена.

Изменяемые псевдонимы!
- изменяемое значение
- присваиваем больше одного имени
- значение изменяется
- изменение отражается на всех именах!

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

Перед нами маленький образец кода, и на нем видно, что у одного списка два имени. Но после вызова функции (function call) мы увидим, что два имени могут сильно различаться. Поэтому новое значение отнюдь не должно поражать.

Здесь я первый раз сказал «изменяемый» (mutable). Это значит, что можно изменить содержание значения. В нашем образце кода имя «nums» ссылается на один и тот же объект на всех четырех строках кода. Однако значение объекта изменилось.

Неизменяемые значения не могут иметь псевдонимов

10

В питоне не все значения могут изменяться. Числа (numbers), строки (strings) и кортежи (tuples) не могут. нет таких операций, которые могли бы изменить для них содержание значения. Можно только создать новый объект на базе старого.

В нашем примере кода на три строки имя «х» ссылается на строку «hello». При этом «y» тоже ссылается на нее. На последней строке оператор «+» не расширяет существующую строку, а создает новую через объединение (concatenating) строк «hello» и «there».

Со старой строкой ничего не случилось. Со строками по-другому не бывает, потому что они не изменяются. Методы для строк (string methods) не могут изменить строку, они все возвращают новые строки.

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

Неочевидность изменения

11

В разговоре об этих концепциях трудности создает, в том числе, слово «замена» (change). Неформально выражаясь, мы говорим, что добавление «1» к «х» как бы заменяет имя «х»:
x = x + 1

Еще мы говорим, что добавление нового значения к имени «num» как бы заменяет это имя:
num.append(7) # список «num» как бы заменен

Однако эти две операции очень сильно различаются. Первая создает новую ссылку для имени «х». При выполнении «х+1» создается новый объект, а потом операция присвоения связывает «х» с этим объектом. Если добавлять новые значения к списку «num», то он изменяется. При этом имя «num» продолжает ссылаться на тот же объект в его новом, измененном, состоянии. Значение объекта изменилось по содержанию.

Слова «пересоздать ссылку» (rebinding) и «изменять» (mutating) неудобно использовать все время. Однако, когда мы вчитываемся в участок кода, чтобы понять, что он делает, эти слова очень помогают различать два разных вида замены.

Разумеется, можно пересоздать ссылку между именем и списком. Это еще один способ превратить «nums» в список, последним элементом которого является «7»:
nums = nums + [7]

Как и с целыми числами, оператор «+» создает новый список. После этого для имени «nums» пересоздается ссылка на список. С другой стороны, изменить число невозможно. Они неизменяемые, в буквальном смысле.

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

Среди распространенных ошибочных представлений насчет питона - мнение о том, что операция присвоения работает по-разному для изменяемых и неизменяемых значений. Это не так. Присвоение осуществляется просто: между именем с левой стороны оператора и значением с правой стороны оператора создается ссылка.

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

Базовые механизмы питона нередко оказываются проще, чем о них думают. Присвоение всегда выполняет одну и ту же операцию, независимо от того, какого рода значение находится с правой стороны оператора.

Варианты присвоения

13

В питоне есть другие способы осуществить присвоение помимо обычного знака равенства. Например, для чисел и списков можно использовать оператор +=.

На принципиальном уровне это две одинаковые строки:
x += y
x = x + y

Однако в питоне способ использования оператора += зависит от значения «х». Следующие две строки по факту одинаковые:
x += y
x = x.__iadd__(y)

Включая педантичность на полную мощность, нужно сказать, что они аналогичны строке «x = type(x).__iadd__(x, y)», потому что метод нельзя вызывать по самому объекту, только по классу. Результат использования оператора += зависит от типа объекта, которому присвоено имя «х», потому что именно значение определяет способ выполнения для метода __iadd__.

Для чисел оператор += выполняется ровно как и ожидается. Но списки приготовили еще один сюрприз. Строка «nums = nums+more» пересоздает ссылку между именем «nums» и новым списком, созданным путем объединения списков «nums» и «more». При этом строка «nums += more» на самом деле изменяет содержание списка «nums». То есть, выполняется операция изменения (mutating operation).

Причина в том, что для списков метод __iadd__ действует следующим образом (но это в языке Си, не в питоне):
class List:
    def __iadd__(self, other):
        self.extend(other)
        return self

С точки зрения выполнения строка «nums += more» дает тот же результат, что и следующая строка кода:
nums = nums.__iadd__(more)

которая, благодаря реализации метода __iadd__, аналогична следующей строке кода:
nums.extend(more)
nums = nums

Поэтому в данном случае выполняется операция по пересозданию ссылки. Первым этапом выполняется изменение, а пересоздание ссылки является холостой командой.

Мораль в том, что нужно понимать поведение базовых элементов при их использовании!

Иногда ссылки - это не просто имена

14

Во всех примерах, показанных на данный момент, имена выступали ссылками на значения, но в качестве ссылок можно использовать и другие объекты. В питоне есть ряд сложных структур данных, каждая из которых содержит ссылки на значения: элементы списков (list elements), ключи (keys) и значения (values) в словарях, атрибуты объектов (object attributes) и т.д. Их всех можно использовать с левой стороны во время операции присвоения. К ним применима вся специфика, о которой я говорил выше. Все, что можно поставить с левой стороны оператора присвоения, является ссылкой. Каждый раз, когда я говорю про «имя», его синонимом является «ссылка».

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

nums123nums = [1, 2, 3]

ris1

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

nums123nums = [1, 2, 3]

ris2

Много что можно назвать ссылкой

15

Примеры других присвоений. Все, что слева - ссылка:
my_obj.attr = 23
my_dict[key] = 24
my_list[index] = 25
my_obj.attr[key][index].attr = «etc, etc»

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

Обратите внимание, что строка «i = x» выполняет операцию присвоения к имени «i», а строка «i[0] = x» ничего не присваивает имени «i». Присвоение выполняется для первого элемента в значении имени «i». Очень важно правильно понимать, к чему конкретно выполняется присвоение. Если имя появляется где-то с левой стороны оператора присвоения, это не означает, что для него пересоздается ссылка.

Много что является присвоением

16

Многие объекты можно использовать в качестве ссылок. Точно так же в питоне есть много операций, которые представляют собой присвоение. В каждой из следующих строк выполняется присвоение для имени «Х»:
X = ...
for X in ...
[... for X in ...]
(... for X in ...)
{... for X in ...}
class X(...):
def X(...):
def fn(X): ... ; fn(12)
with ... as X:
except ... as X:
import X
from ... import X
import ... as X
from ... import ... as X

Я вовсе не хочу сказать, что эти конструкции выполняются аналогично присвоению. Я хочу сказать, что они и есть присвоение: каждый раз результатом становится создание ссылки между именем «Х» и значением. Все, что я говорил про операцию присвоения, точно так же применимо к этим конструкциям.

В большинстве этих конструкций имя «Х» определяется в том же контексте, что и сама конструкция. Но не во всех, особенно это касается генераторов (comprehensions). Нюансы реализации немного различаются в Python 2 и Python 3. Но они все являются самыми настоящими присвоениями, поэтому все факты касаемо присвоений применимы и к ним.

Циклы «for loop»

17

Циклы «for loop» - интересный пример. Если написать вот такую строку кода:
for x in sequence:
    something(x)

он выполняется примерно так:
x = sequence[0]
something(x)
x = sequence[1]
something(x)
# and so on...

Реальный механизм получения значений на базе последовательности (sequence) запутаннее простой индексации, которую я использовал в примерах. Но суть в том, что каждый элемент последовательности присваивается имени «х» точно так же, как это выполняется при обычной операции присвоения. Повторюсь, все правила операции присвоения и метода ее выполнения применимы к такому присвоению.

Циклы «for loop»

18

Допустим, у нас есть список чисел, и мы хотим все эти числа умножить на 10. Поэтому если в нашем списке первые элементы - [1, 2, 3], это значит, что мы хотим превратить список в [10, 20, 30]. Простой вариант:

nums = [1, 2, 3]
for x in nums: # x = nums[0] ...
    x = x * 10
print(nums) # [1, 2, 3] :(

Не работает. Чтобы понять почему, нужно вспомнить, что во время первой итерации «х» - это другое имя для элемента nums[0]. Выше мы узнали, что если два имени ссылаются на одно значение, то после повторного присвоения одного из них второе не изменится вслед за ним. В данном случае мы выполняем повторное присвоение для «х» (это строка «x = x * 10»), поэтому теперь «х» обозначает 10. При этом nums[0] по-прежнему ссылается на старое значение 1.

Наш цикл никогда не меняет исходный список, потому что мы попросту продолжаем присваивать имя «х» снова и снова. Лучше всего избегать изменения списков и создавать новые списки:

nums = [ 10*x for x in nums ]

Аргументы функций - это присвоение

19

Возможно, аргументы функций являются самыми важными объектами из всех, которые внешне не похожи на присвоение. Но они тоже выполняют присвоение. Когда мы создаем функцию, то определяем ее формальные параметры, аналогично «х» в данном примере:
def func(x):
    print(x)

При вызове функции (function call) мы задаем реальные значения для аргументов:
num = 17
func(num)
print(num)

В данном случае «num» представляет собой значение для параметра «х». При вызове функции мы видим точно такое же выполнение, как если бы написали код «x = num». Реальное значение присваивается этому параметру.

При каждом вызове функции создается стековый фрейм. Он представляет собой контейнер для локальных имен этой функции. Имя «х» входит в локальный контекст функции. При этом семантика присвоения такая же.

Когда мы находится внутри функции «func», у значения (17) два имени: «num» в рамках вызова и «х» в рамках функции.

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

20

Попробуем написать полезную функцию. Мы хотим добавить значение в список два раза. Напишем тремя разными способами. Два из них - рабочие (но выполняются по-разному), а один - совершенно бесполезный. Будет полезно понять, почему эти три варианта действуют по-разному.

Первая версия функции «append_twice»:
def append_twice(a_list, val):
    a_list.append(val)
    a_list.append(val)

Тут все очень просто, выполняется именно то, что предполагается.

Выполняем вот так:
nums = [1, 2, 3]
append_twice(nums, 7)
print(nums) # [1, 2, 3, 7, 7]

Когда мы вызываем функцию «append_twice», то передаем ей список «nums». Этот список присваивается параметру «a_list». Поэтому теперь у списка два имени: «nums» при вызове и «a_list» в функции «append_twice». Потом мы два раза добавляем «val» в список. Выполняется операция добавления в список «a_list». Это тот же самый список, что и «nums» при вызове функции. Поэтому мы выполняем прямое изменение в списке из вызова функции.

После выполнения функции фрейм уничтожается, то есть локальное имя «a_list» удаляется. Но на список ссылалось не только это имя, поэтому его удаление не утилизирует сам список.

Затем мы смотрим список «nums» и видим, что он и правда изменился.

21

Теперь попробуем еще один вариант:
def append_twice_bad(a_list, val):
    a_list = a_list + [val, val]
    return

nums = [1, 2, 3]
append_twice_bad(nums, 7)
print(nums) # [1, 2, 3]

Здесь другой подход: внутри функции мы расширяем (extend) список, добавляя в исходный список два значения. Но эта функция не работает, совсем. Как и раньше, мы передаем список «nums» в функцию, чтобы оба списка «a_list» и «nums» ссылались на исходный список. Но потом мы создаем новый список и присваиваем ему имя «a_list».

После выполнения функции фрейм уничтожается вместе с именем «a_list». На новый расширенный список ссылалось только имя «a_list», поэтому он тоже утилизируется. Вся работа пропала!

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

22

Здесь у нас почти такая же функция. Но, создав новый список «a_list», мы его возвращаем:
def append_twice_good(a_list, val):
    a_list = a_list + [val, val]
    return a_list

nums = [1, 2, 3]
nums = append_twice_good(nums, 7)
print(nums) # [1, 2, 3, 7, 7]

Потом в программе мы не просто вызываем функцию «append_twice_good», мы повторно присваиваем ее возвращенное значение списку «nums». Функция создала новый список, и после его возвращения мы можем поступить с ним по своему усмотрению. В данном случае мы хотим, чтобы у нового списка было имя «nums». Поэтому мы просто присваиваем списку это имя. Старое значение утилизируется, а новое остается.

23

У нас есть три версии функции, можно сравнить. Первая изменяет содержание списка. Вторая создает новый список, но ничего не может с ним сделать. Третья создает новый список и возвращает его, поэтому можем его использовать.

24

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

25

Для имен не предусмотрено типизации, а для значений - контекста. Когда мы говорим, что у функции есть локальная переменная (local variable), мы имеем в виду, что имя включено в контекст функции. Нельзя использовать это имя вне функции. Когда функция возвращает значение, имя уничтожается. Но мы уже видели, что если к значению этого имени ведут и другие ссылки, то оно сохранится после выполнения функции. Локальным является имя, а не значение.

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

Другие вопросы

26

Здесь еще несколько вопросов, на глубокое освещение которых у меня не было времени.

«В питоне нет переменных». Честно говоря, изначально я начал готовить данное выступление именно потому, что встречал людей, которые пытались объяснить питон, говоря, что в нем нет переменных. Очевидно, что это не так. Они пытались сказать, что переменные питона работают не так, как в языке Си, пусть даже выбранную ими формулировку трудно понять.

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

Слава богу, эта мантра теряет популярность!

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

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

Двухмерные списки: простой способ создания вложенных списков (например, шахматной доски 8х8) не работает. Получится список из восьми нулей, при этом у наружного списка будет восемь ссылок на этот список, а не восемь отдельных списков с нулями. Второй вариант на слайде - правильный.

Механизмы питона - очень простые, часто они проще, чем мы думаем. Если их понимать, то программы не будут преподносить плохих сюрпризов.