Python — это объектно-ориентированный язык программирования, но он может вести себя странно. Если вы пришли из других языков ООП, этот пост может быть вам полезен
В главе 8 книги Fluent Python Лучано Рамальо обсуждает, как объекты python находятся под капотом. Здесь будет определена фундаментальная концепция хранения переменных в python и рассмотрены некоторые соответствующие примечания.
Без дальнейших церемоний, давайте погрузимся.
1 — Переменные Python — это не блоки
Совет. Переменные Python — это метки для значений.
В программировании 101 нас часто учат, что переменные — это ящики, в которых хранится значение. Например, в ящике a
хранится список [1,2,3]
. Однако в питоне это не так.
На рисунке 1 имена переменных (синий цвет) являются «метками» для расположения в памяти. Обратите внимание, что у одного объекта может быть несколько имен переменных, как показано слева. Ячейки в памяти (черные) — это «ящик», а не само название. В этих полях хранятся значения (красные). Давайте рассмотрим пример.
var_name = 1 var_name # 1 id(var_name) # 4443642160
В приведенном выше фрагменте переменной var_name
присваивается значение 1
. «Ящик», в котором хранится значение, — это идентификатор памяти 4443642160
.
Теперь давайте добавим еще одну переменную в то же самое место в памяти…
another_var_name = var_name id(var_name) # 4443642160 var_name # 1 id(var_name) == id(another_var_name) # True
Как мы видим, у нас есть две переменные Python, которые ссылаются на один и тот же объект. Однако, как только мы изменим нашу вторую переменную, например, запустив y += 1
, наша ячейка памяти изменится на 4443642192
, что сделает объекты другими.
В python концептуально более интуитивно кажется, что имена переменных назначаются местам в памяти. И эти места в памяти хранят значения.
2 — Объектное равенство
Совет. Используйте ==
, чтобы определить, имеют ли две переменные одинаковые значения, и is
, чтобы определить, имеют ли две переменные общие местоположения в памяти.
Учитывая настройку в разделе 1, этот раздел будет кратким.
==
определяет, имеют ли переменные одинаковое значение — это похоже на функцию .equals()
в java. Однако, если вы действительно хотите определить, являются ли переменные одним и тем же объектом, вам следует использовать is
для проверки идентификаторов памяти.
a = 1 b = 1 c = a a == b == c # True a is c # True
А теперь интересное примечание. Мы ожидаем, что a is b
будет оцениваться как False
. Однако иногда это True
.
Удивительно, но для экономии места Python иногда присваивает переменные одному и тому же объекту, если значения объектов совпадают. Это экономит память, потому что в нашем случае вам не нужно создавать отдельные экземпляры целого числа 1
— вы можете просто ссылаться на один и тот же объект.
Теперь, как только мы изменим любую из этих переменных, она получит собственное место в памяти.
3 — Копирование против глубокого копирования
Совет: если вы не знаете, что делаете, используйте deepcopy()
.
Если вы работали с пандами, вы, вероятно, видели и/или использовали df.copy()
. Использование copy
вместо deepcopy
влияет только на работу с составными объектами (объектами, которые содержат другие объекты, такие как списки или экземпляры классов). Но для некоторых парадигм программирования составные объекты являются обычным явлением.
Вот разница между глубоким копированием и поверхностным копированием.
- Глубокая копия создает новый составной объект, а затем рекурсивно вставляет в него копии объектов, найденных в оригинале.
- Вповерхностной копии создается новый составной объект, а затем (насколько это возможно) вставляется в него ссылки на объекты, найденные в оригинале.
Пример должен помочь прояснить это…
Класс Bus
создает список пассажиров, тем самым делая каждый экземпляр класса составным объектом.
Как мы видим, bus2
— это поверхностная копия bus1
, а bus3
— глубокая копия. Когда объекты в bus1
будут изменены, это повлияет на bus2
. Однако bus3
совершенно не пострадал.
Вот почему безопаснее использовать deepcopy
— вы получите совершенно новую копию значений объекта.
4 — Функциональные параметры
Совет: будьте осторожны, если вы установили пустую изменяемую переменную в качестве функции Python по умолчанию.
Параметры Python передаются с помощью системы, называемой вызов путем совместного использования, что означает, что передаваемые значения являются поверхностными копиями.
Если вы передадите пустой список по умолчанию, например def my_func(x=[]):
, а затем измените x
, вы будете редактировать встроенную функциональность list()
, а не переменную x
.
Рекомендуемый метод в книге — указать значения по умолчанию в начале функции через логику случая…
def my_func(x): if x is None: x = []
Лично я считаю, что описанный выше метод излишен и не подходит для стиля, однако это ваш выбор.
5 — Сбор мусора
Совет: переменные python «собираются мусором», когда на них заканчиваются ссылки.
В качестве последнего совета давайте поговорим о том, как работает сборка мусора в Python.
Каждый раз, когда мы присваиваем объекту имя переменной, мы даем ему ссылку. Таким образом, a=1; b=a
означает, что ячейка памяти, в которой хранится 1
, имеет счетчик ссылок, равный 2.
Теперь предположим, что мы присваиваем a
и b
None
, например. a=None; b=None
. Теперь, поскольку нет переменных, ссылающихся на объект со значением 1
, эта область памяти может быть удалена сборщиком мусора.
Если счетчик ссылок равен 0, ячейка памяти стирается для повторного использования.
Довольно просто, правда?
Теперь, чтобы завершить это, давайте также поговорим о ключевом слове del
. del
используется для удаления имен переменных, а не самого объекта. Удаляя имя нашей переменной, мы освобождаем это имя для последующего использования и тем самым избегаем возможных конфликтов имен. Однако на бэкенде, если del
приводит к тому, что счетчик ссылок становится равным 0, объект также будет удален.
Спасибо за чтение! Я напишу еще 16 постов, посвященных академическим исследованиям в индустрии DS. В моем комментарии есть ссылки на основной источник этого поста и некоторые полезные ресурсы.