Мутабельность и переработка объектов в Python

RedDeveloper
30.01.2023 12:31
Мутабельность и переработка объектов в Python

Краткое описание того, как объекты ссылаются, изменяются и перерабатываются в Python

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

В этой статье мы рассмотрим эти темы:

  • Что такое переменные в Python?
  • Мелкие и глубокие копии объектов
  • Сборка мусора в Python

Переменные - это не коробки

Обычно переменные рассматриваются как коробки или контейнеры, что мешает пониманию ссылочных переменных в объектно-ориентированных языках.

Переменные Python похожи на ссылочные переменные в Java, вы можете думать о них как о метках с именами, прикрепленными к объектам.

var_one = [1, 2, 3]
var_two = var_one
var_one.append(4)
print(var_two)
# [1, 2, 3, 4]

В этом примере мы изменяем список, на который ссылается "var_one", добавляя еще один элемент. Когда мы печатаем "var_two", мы получаем тот же список.

Это означает, что "var_two" ссылается на тот же список, на который ссылается "var_one".

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

Это приводит нас к другим вопросам! Как проверить, равны ли два объекта?

В Python каждый объект имеет свой id, и его можно получить с помощью функции id(obj). Теперь две переменные, ссылающиеся на один и тот же объект, будут иметь одинаковый id, то есть id - это адрес памяти объекта, и он уникален в течение жизненного цикла объекта.

charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
print(lewis is charles)
# True

print(id(charles), id(lewis))
# (4300473992, 4300473992)

lewis['balance'] = 950
print(charles)
# {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}

alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
print(alex == charles)
# True

print(alex is not charles)
# False
Чарльз и льюис привязаны к одному объекту, а алекс - к отдельному объекту равной ценности
Чарльз и льюис привязаны к одному объекту, а алекс - к отдельному объекту равной ценности

Операторы is и not сравнивают идентичность двух объектов, а функция

Id() возвращает целое число, представляющее идентичность.

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

Оператор is не может быть перегружен и, как правило, работает быстрее, чем оператор ==, поскольку его можно перегрузить.

Большинство встроенных типов и объектов Python переопределяют специальный метод __eq__ для поддержки оператора ==.

Мелкое копирование VS глубокое копирование

Мелкие копии делать проще всего, но это может быть не то, что вам нужно.

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

Это экономит память и не вызывает проблем, если все объекты неизменяемы, но если есть изменяемые объекты, это может привести к неприятным сюрпризам.

list_1 = [3, [66, 55, 44], (7, 8, 9)]
list_2 = list(list_1)
Визуализация в Python Tutor
Визуализация в Python Tutor

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

list_1.append(100)
list_1[1].remove(55)
print('list_1:', list_1)
# list_1: [3, [66, 44], (7, 8, 9), 100]
print('list_2:', list_2)
# list_2: [3, [66, 44], (7, 8, 9)]
list_2[1] += [33, 22]
list_2[2] += (10, 11)
print('list_1:', list_1)
# list_1: [3, [66, 44, 33, 22], (7, 8, 9), 100]
print('list_2:', list_2)
# list_2: [3, [66, 44, 33, 22], (7, 8, 9, 10, 11)]
Последнее состояние выполнения кода примера
Последнее состояние выполнения кода примера

Несколько моментов, которые следует иметь в виду:

  • Любые операции над встроенными объектами будут видны другим переменным, ссылающимся на эти объекты, если только этот объект не является Immutable.
  • являются неизменяемыми, поэтому любая операция над ними создает новый кортеж и перепривязывает его к переменной, как в примере.

Глубокое копирование на помощь

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

В стандартной библиотеке есть модуль, который реализует это, он предлагает две функции copy() и deepcopy(), первая - для поверхностного копирования, а вторая - для глубокого.

import copy
 
class Bus:
    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)
   
    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)

print(id(bus1), id(bus2), id(bus3))
# (4301498296, 4301499416, 4301499752)

bus1.drop('Bill')

print(bus2.passengers)
# ['Alice', 'Claire', 'David']

print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers))
# (4302658568, 4302658568, 4302657800)

print(bus3.passengers)
# ['Alice', 'Bill', 'Claire', 'David']

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

Решением является реализация собственных специальных методов __copy__ и __deepcopy__.

Сборка мусора

Объекты в Python никогда не уничтожаются явно, как, например, в C#, они собираются в мусор, когда становятся недоступными.

У нас есть оператор, который удаляет ссылки, del, но никогда - объекты.

a = [1, 2]
b = a
del a
print(b)
# [1, 2]
 b = [3]
# The original object is now ready to be garbage collected

'сборщик мусора выбросит объект из памяти, если на него нет ссылок.

Дальнейшее чтение

  • 6-я глава книги Fluent Python
  • Официальная документация Python по модулю copy
  • Глава "Модель данных" в книге The Python Language Reference

Предыдущая статья в серии: Классы данных и как их создавать

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?

20.08.2023 18:21

Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в 2023-2024 годах? Или это полная лажа?".

Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией

20.08.2023 17:46

В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.

Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox

19.08.2023 18:39

Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в частности, магию поплавков и гибкость flexbox.

Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest

19.08.2023 17:22

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

Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️

18.08.2023 20:33

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

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL

14.08.2023 14:49

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