Изменяемые и неизменяемый типы объектов
Contents
Изменяемые и неизменяемый типы объектов¶
Рассмотрим ещё один пример.
a = 0 # 1
a += 1 # 2
Программисту C/C++ может показаться, что во второй строке значение, хранящееся в целочисленном объекте созданном в первой строке, увеличивается на 1. На самом деле это не так: создаётся новый объект, хранящий результат вычисления выражения a + 1, а затем a связывается с этим новым объектом. Это проще увидеть, переписав выражение a += 1 в виде a = a + 1.
a = 0
print(id(a))
a += 1
print(id(a))
140706216089328
140706216089360
На самом деле, в python все типы объектов можно разделить на два вида: изменяемые (mutable) и неизменяемый (immutable).
Значение, хранящееся в объекте неизменяемого типа, невозможно изменить никакими способами. Единственный способ получить объект такого типа с другим значением — создать новый объект.
Значение, хранящееся в объекте изменяемого типа, можно изменить не создавая новый объект. Изменения объекта возможно за счет вызова его методов или применения к нему операторов.
Все встроенные числовые типы (bool, int, float, complex, (decimal, fractions, которые также присутствуют в стандартной библиотеке, но обсуждаться не будут)) являются неизменяемыми. Единственный изменяемый тип объектов, с которым мы уже сталкивались — list.
Разделяемый ссылки¶
Изменяемость и неизменяемость играет наибольшую роль в случае, когда несколько имен ссылаются на один и тот же объект.
Разделяемые ссылки на неизменяемый объект¶
Создадим две ссылки a и b на один и тот же объект целочисленного типа.
a = 0
b = a
Во второй строке создаётся имя b и оно связывается с объектом, на которое указывает имя a. Убедиться, что имена указывают на один и тот же объект, можно сравнив их адреса (id(a) == id(b)). В python помимо оператора сравнения ==, ещё есть оператор сравнения на идентичность is, который по сути дела сравнивает ссылки на объекты.
print(a is b)
True
Note
Имена всегда указывают на объекты. Имена не могут указывать на другие имена, но объекты могут содержать в себе ссылки на другие объекты.
Расширим предыдущий пример ещё двумя строками.
a += 42
Создаётся объект 42 и имя a связывается с ним. Объект 0 не претерпел изменений (и не мог бы, int неизменяемый), имя b по прежнему связанно с ним.
print(a is b)
print(a, b)
False
42 0
Таким образом, если есть несколько ссылок на один и тот неизменяемый объект, можно не переживать, что изменяя объект по одной из них вы измените значения по остальным ссылкам.
Разделяемые ссылки на изменяемый объект¶
Рассмотрим пример с изменяемым типом данных list. Если слева и справа от оператора += стоит список, то в конец списка слева добавляются все элементы списка справа от оператора. Т.е. если список l — список, то выражение
l += v
эквивалентно выражению
l.extend(v)
l = ["a", "b", "c"]
l += [1, 2, 3]
print(l)
['a', 'b', 'c', 1, 2, 3]
Создадим список и две ссылки на него.
l1 = ["a", "b", "c"]
l2 = l1
print(l1 is l2)
True
Изменим список l1, добавив в него какой-нибудь элемент.
l1 += [1, 2, 3]
print(l1)
['a', 'b', 'c', 1, 2, 3]
Что произошло с списком l2?
print(l2)
print(l1 is l2)
['a', 'b', 'c', 1, 2, 3]
True
Изменение объекта по имени l1 отражается и на имени l2, так как они ссылаются на один и тот изменяемый объект.
Оба имени l1 и l2 ссылаются на один и тот же изменяемый объект. Изменяемые объекты могут менять своё содержимое на месте. Тем не менее оператор присваивания = сохраняет смысл связывания имени и объекта.
l1 = ["a", "b", "c"]
l2 = l1
print(l1 is l2)
l1 = [1, 2, 3]
print(l1 is l2)
True
False
В качестве аргументов функций¶
Note
Строго говоря, понятия аргументы и параметры функции отличаются. Параметры — переменные, которые указываются при объявлении функции, а аргументы — то, что передаётся функции вместо параметров при её вызове.
Изменяемость/неизменяемость типа объекта играет большую при написании функций в python.
Передача аргументов в функцию в C/C++¶
В C/C++ функции могут принимать параметры двумя способами.
#include <iostream>
void f(int x, int &y){ // объявление функции, x и y --- её параметры
x++;
y++;
return;
}
int main(){
int a = 0, b=0;
f(a, b); // вызов функции, a и b --- её аргументы
std::cout << a << " " << b;
return 0;
}
x— параметр функцииf, передаваемый по значению. При вызове функции, создаётся локальная переменнаяx, её значение инициализируется значением переменнойa. Изменениеxникак не повлияет на значение переменнойa.y— параметр функцииf, передаваемый по ссылке. При вызове функции, создаётся ссылкаyна переменнуюbи изменение значенияyзатронет и значение переменнойb.
Кроме того, можно объявлять константные параметры функции с помощью ключевого слова const. Компилятор выдаст ошибку, если функция пытается изменить значение такого аргумента.
Передача аргументов в функцию в python¶
В python отсутствуют концепции как передачи аргументов по ссылке или по значению, так и константности переменной/параметра в смысле C/C++. В качестве аргумента всегда передаётся ссылка на аргумент (объект), а возможность изменить исходный объект внутри функции зависит от того, какого он типа.
def f(a, b):
a += b
return a
x = 0
y = f(x, 42)
print(f"{x=}, {y=}")
print(x is y)
x=0, y=42
False
l1 = ["a", "b", "c"]
l2 = f(l1, [1, 2, 3])
print(f"{l1=}, {l2=}")
print(l1 is l2)
l1=['a', 'b', 'c', 1, 2, 3], l2=['a', 'b', 'c', 1, 2, 3]
True
В примере выше одна и та же функция изменяет или не изменяет значение своего аргумента в зависимости от того, какого типа этот аргумент. Программист должен сам следить за такими деталями и при необходимости передавать в функцию копию объектов.
Note
Выражение s + t совершает конкатенацию списков s и t (создаёт новый объект). Выражение s += t добавляет элементы из списка t в список s (изменяет s).
Создание копий объектов¶
Чтобы гарантировать, что значения аргументов функции не изменятся, можно создать копию этих аргументов и далее пользоваться ими. За копирование объектов отвечает модуль copy. В нем есть две функции копирования copy.copy и copy.deepcopy. Разница между этими функциями заметна только для объектов, которые содержат в себе другие объекты (list, например), но и то не всегда. Подробнее это отличие будет обсуждаться позже.
Предположим, что нас не устраивает, что функция изменила список l1 в предыдущем примере.
from copy import copy # <-----
def f(a, b):
a = copy(a) # <-----
a += b
return a
l1 = ["a", "b", "c"]
l2 = f(l1, [1, 2, 3])
print(f"{l1=}, {l2=}")
print(l1 is l2)
l1=['a', 'b', 'c'], l2=['a', 'b', 'c', 1, 2, 3]
False