Вступление:

Многие люди выбирают Python в качестве своего первого языка программирования, потому что он имеет приятный синтаксис и автоматически заботится о многих деталях, о которых вам нужно беспокоиться на языках более низкого уровня, например объем памяти. Тем не менее, под капотом интерпретатор Python управляет своей памятью несколько сложнее, чем, например, для аналогичной программы, написанной на C. Хотя вы можете быть довольны тем, что интерпретатор (обычно CPython, реализация Python на языке C) позаботится обо всем этом за вас, неизбежно наступит время, когда вам придется копнуть немного глубже, чтобы устранить ошибку или понять особенности вашей программы. поведение. Фактически, понимание объектов и их возможностей приведет к более эффективному и чистому коду.

Идентификатор и тип:

Python - это язык объектно-ориентированного программирования (ООП) со многими функциями, которые поддерживают парадигму ООП. В отличие от процедурного языка, такого как C, Python позволяет создавать объекты и классы, которым можно присвоить определенные атрибуты, такие как методы и свойства. Не вдаваясь в подробности, объекты позволяют избежать некоторых ловушек процедурных языков, таких как дублирование кода, и обеспечивают дополнительные проверки того, как можно использовать код с помощью инкапсуляции и полиморфизма. В отличие от некоторых языков ООП, Python имеет динамическую типизацию, что означает, что вам не нужно объявлять, какие данные (например, целые числа, массивы и т. Д.) Может принимать переменная перед ее использованием. Кроме того, вы можете переключать типы в любое время. Это возможно потому, что все в Python одного типа.

Пусть это осядет на секунду.

Что же отличает int от str? Это правда, что они имеют разные свойства и способы использования, однако в реализации (я буду использовать CPython в качестве ориентира, поскольку он является каноническим) все объекты Python являются экземплярами структуры PyObject. Хотя int действительно отличается от str, объекты передаются как PyObject указатели и только приводятся к их окончательному типу, когда это необходимо.

Система управления памятью, используемая CPython, аналогична системе, используемой обычными программами C. Когда создается новый объект, интерпретатор использует malloc для запроса памяти у ОС. Каждый объект находится где-то в памяти (в куче), местоположение которой можно определить с помощью встроенной функции id(). Вызов id с именем переменной в качестве аргумента вернет адрес объекта, на который ссылается имя переменной:

>>> a = 'word'
>>> id(a)
140441823802288
>>> hex(id(a)) # the more familiar hex representation of the address
'0x7fbb2904f3b0'

Адрес объекта - это место, где его данные хранятся в памяти. Поскольку все объекты минимально обладают всеми атрибутами PyObject, я могу переназначить a другому типу без каких-либо проблем:

>>> a = [1, 2, 3]
>>> hex(id(a))
'0x7fbb29030148'

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

Тип объекта можно запросить с помощью функции type(). Вызов type для объекта вернет класс, экземпляром которого является этот объект. Поскольку в ООП все объекты принадлежат классу, вы можете использовать type для всех объектов, даже для самих классов:

>>> type(a)
<class 'list'>
>>> type(list)
<class 'type'>

A - экземпляр для list, поэтому его класс - list. list сам по себе является классом, а не экземпляром, но, тем не менее, это объект, производный от класса type, который является самым основным объектом в Python. Все объекты наследуются от type в какой-то момент своей иерархии классов.

Хотя тип относится к классу объекта, сами переменные не имеют типа. Вместо этого вы можете рассматривать переменные как метки, применяемые к объектам. Отчасти поэтому нет проблем с переназначением a на list после того, как он был назначен на str. Это потому, что он просто добавляет a к списку имен, которые можно использовать для ссылки на конкретный экземпляр list. Поскольку имя переменной - это просто идентификатор, его можно использовать для ссылки на любой объект.

Имена переменных относятся к объектам «по ссылке». Это означает, что они всегда являются указателем на ячейку памяти переменной. Единственные данные, которые напрямую хранятся в имени переменной, - это адрес, возвращаемый id. Вот почему можно делать такие вещи:

>>> a
[1, 2, 3]
>>> b = a
>>> b
[1, 2, 3]
>>> b.append(4)
>>> a
[1, 2, 3, 4]

Присваивая a b, мы действительно назначаем id(a) b. Поскольку list объекты могут быть изменены на месте, эффект вызова b.append такой же, как если бы он был вызван с использованием a.

Изменяемые объекты:

Хотя все объекты наследуются от одного и того же базового класса и носят одну и ту же «шляпу» в реализации CPython, каждый из них может иметь определенные методы и поведение. Одно из наиболее важных отличий - их изменчивость. Некоторые объекты могут изменять свои значения в течение их срока службы. Например, к экземпляру list можно добавлять или изменять данные в любом месте списка без необходимости делать копию и переносить все в новое место в памяти. Изменяемые объекты поддерживают операции присваивания на месте, как в приведенном выше примере.

Несмотря на то, что все переменные являются ссылкой на свои объекты, не все объекты изменяемы. Типы изменяемых объектов в Python - это list, dict, set и почти любой пользовательский класс. Все эти объекты поддерживают операции на месте.

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

>>> a = [1, 2]
>>> a.word = "word"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'word'
>>> class A(list):
...     pass
...
>>> a = A([1, 2])
>>> a
[1, 2]
>>> a.word = "word"
>>> a.word
'word'

Создав новый класс A, который наследуется от list, мы можем вернуть возможность добавлять новые атрибуты к объектам типа A, поскольку механизм, используемый для предотвращения этого, не наследуется.

Неизменяемые объекты:

В отличие от изменяемых объектов, неизменяемые объекты нельзя изменить на месте. Такое поведение требуется по множеству причин, например, к скорости и более безопасному манипулированию данными. Примеры неизменяемых объектов: tuple, int, float, frozenset и str. Данные, хранящиеся в этих объектах, задаются при создании. Однако то, что их нельзя изменить, не означает, что вы не можете переназначать имена переменных новым экземплярам этих объектов. Например:

>>> a = (1, 2)
>>> id(a)
140441783708296
>>> a[0] = 9
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> a = (9, 2)
>>> id(a)
140441783708040

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

Некоторые типы имеют как изменяемые, так и неизменяемые аналоги. Например, tuple - неизменная версия list, а frozenset - неизменная версия set. Причина наличия обоих заключается в том, что часто вы хотите передавать аргументы функции, не беспокоясь об изменении исходных данных.

В отличие от tuple и frozenset, у объектов int и float нет изменяемых аналогов. Для этого есть несколько причин, но интуитивно понятно, что такие атомарные типы, как целые числа, нельзя переназначать. Основная причина того, что ints и floats не изменяемы, сводится к единому объектному интерфейсу. В Python все является объектом. У четных чисел есть методы и атрибуты, что позволяет выполнять такие операции, как перегрузка операторов и использование специализированных методов. Кроме того, поскольку Python поддерживает произвольно большие целые числа, объем пространства, зарезервированного для каждого экземпляра, сведен к минимуму (до определенного размера), поэтому можно использовать как очень большие, так и маленькие значения, не тратя много места.

Если вы не создаете приложение, которое выполняет много вычислений с большими числами, большую часть времени вы будете использовать числа либо для подсчета, либо для перебора других объектов, таких как списки. Учитывая эту склонность к подсчету, обычно начиная с 0, при запуске CPython предварительно загружает / выделяет место для чисел от -5 до 256, поскольку они наиболее часто используются. Затем при присвоении числа имени переменной, если число находится в этом диапазоне, переменная просто становится ссылкой на уже существующий объект:

>>> id(1)
10055552
>>> a = 1
>>> id(a)
10055552
>>> b = 1
>>> id(b)
10055552
>>> a = 257
>>> id(a)
140441784021520
>>> b = 257
>>> id(b)
140441797668816
>>> id(257)
140441784021648

Как видите, a и b относятся к одному и тому же объекту, который представляет число 1. Однако после 256 объект создается динамически и не остается неизменным при каждом использовании. Кроме того, эти объекты хранятся в большом массиве 32-байтовых структур C, поэтому они быстро доступны:

>>> id(10) - id(9) == id(9) - id(8) == id(8) - id(7) == 32
True

Факт остается фактом: переменные хранят ссылку на объект, поэтому, если вы создаете новый int, например 257, а затем назначаете его новой переменной, вы создаете две ссылки на один и тот же объект:

>>> a = 257
>>> id(a)
140441784021520
>>> b = a
>>> id(b)
140441784021520
>>> b += 1
>>> b
258
>>> id(b)
140441784021648

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

Строки во многом похожи на int объекты. Во-первых, строка - это последовательность целых чисел. ASCII - это представление символов от 1 до 127, поэтому в своей основе строки ASCII представляют собой массив 1-байтовых целых чисел. Строки Python используют Unicode, поэтому они требуют большего хранилища для каждого символа, но в принципе работают одинаково. Аналогично стратегии оптимизации, используемой для целых чисел, str объекты меньше определенной длины хранятся в таблице постоянных строк. Если две строки необходимо сравнить и соответствовать критериям для хранения (это технически называется интернирование), их можно сравнить по адресу, а не посимвольно, поскольку интерпретатор знает, как сначала взглянуть на таблицу постоянных строк. Таким образом, могут произойти такие вещи:

>>> s1 = "A random word"
>>> s2 = "A random word"
>>> s1 is s2
False
>>> s1 = "Arandomword"
>>> s2 = "Arandomword"
>>> s1 is s2
True

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

Почему имеет значение обращение с изменяемыми и неизменяемыми объектами:

Я показал некоторые эффекты, которые можно увидеть при использовании неизменяемых и изменяемых типов. Но есть еще несколько тонких различий, возникающих при передаче объектов в функции. Поскольку все переменные в Python передаются по ссылке, функция, которой они передаются, может напрямую управлять данными своих аргументов. Это одна из причин того, почему tuples так полезны: они действуют как списки, но не позволяют вызываемому объекту случайно изменить данные в области действия вызывающего. Одним из следствий этого является то, что если вы передаете изменяемый тип данных в функцию, вы должны быть готовы к его изменению. Поскольку неизменяемые типы не могут быть изменены, вы также должны помнить о том, когда операции, выполняемые внутри функции, не будут распространяться на вызывающий объект, например операции ints. Более того, не все операции с изменяемыми типами приводят к их изменению на месте.

Для объектов list любая операция среза создает копию фрагмента списка. Добавление двух list объектов вместе также создает новый список. Однако встроенное добавление не создает новый list:

>>> a = [1, 2]
>>> id(a)
140441783766856
>>> a = a + a
>>> id(a)
140441780552200
>>> a
[1, 2, 1, 2]
>>> a *= 2
>>> a
[1, 2, 1, 2, 1, 2, 1, 2]
>>> id(a)
140441780552200

Таким образом, встроенные операции выполняются на месте, а более длинная версия - нет. Это особенно интересно, поскольку встроенная версия обычно рассматривается как сокращение для более длинной, но эффект другой. Таким образом, в функции у вас будет два совершенно разных результата в зависимости от того, какой метод вы выбрали. В общем, обычное присваивание (не встроенные операции, такие как *=) создает новый объект, если только он не имеет чисто форму a = b.

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

И последнюю странность можно увидеть в обращении с tuples. Как описано выше, tuples неизменяемы. Они не поддерживают назначение элементов. Однако элемент tuple может быть изменяемым типом, например, list или dict. Поскольку tuple является просто упорядоченным набором ссылок на другие объекты, до тех пор, пока адрес объекта, который он содержит, не изменяется, tuple по-прежнему будет неизменным. Одним из следствий этого является то, что для использования tuple в качестве ключа в словаре вы не должны использовать изменяемые объекты в качестве элементов. Если вы попытаетесь, интерпретатор выдаст вам ошибку. И наоборот, даже если словари являются изменяемыми, ключи должны быть неизменными, чтобы хеширование работало правильно. Итак, эти типы так пересекаются.

Заключение:

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