Изменяемые и неизменяемый типы объектов
Contents
Изменяемые и неизменяемый типы объектов#
Рассмотрим ещё один пример.
a = 0 # 1
a += 1 # 2
Программисту C/C++
может показаться, что во второй строке увеличивается на 1
значение, хранящееся в созданном на первой строке целочисленном объекте. На самом деле это не так: создаётся новый объект, хранящий результат вычисления выражения a + 1
, а затем имя a
связывается с этим новым объектом. Чтобы продемонстрировать это, напечатаем идентификатор объекта до и после увеличения на 1.
a = 0
print(a, id(a))
a += 1
print(a, id(a))
0 2835673868560
1 2835673868592
Видим, что идентификатор меняет свое значение, а значит имя a
ссылается на разные объекты до и после инструкции a += 1
.
В python
все типы объектов можно разделить на два вида: изменяемые (mutable
) и неизменяемые (immutable
).
Значение, хранящееся в объекте неизменяемого типа, невозможно изменить никакими способами. Единственный способ получить объект такого типа с другим значением — создать новый объект с нужным значением.
Значение, хранящееся в объекте изменяемого типа, можно изменить не создавая новый объект. Изменения объекта возможно за счет вызова его методов или применения к нему операторов.
Все встроенные числовые типы (bool
, int
, float
, complex
, (decimal
, fractions
, которые также присутствуют в стандартной библиотеке, но обсуждаться здесь не будут)) являются неизменяемыми. Единственный изменяемый тип объектов, с которым мы уже сталкивались — list
. Продемонстрируем его изменяемость.
Оператор “+=
” определен и для списков, только с другим эффектом: он расширяет список слева от него, элементами итерируемого объекта справа.
l = [42]
print(l, id(l))
l += "abc"
print(l, id(l))
[42] 1384847112704
[42, 'a', 'b', 'c'] 1384847112704
Note
Для неизменяемого объекта a
выражение a += b
эквивалентно выражению a = a + b
.
Разделяемый ссылки#
Изменяемость и неизменяемость играет наибольшую роль в случае, когда несколько имен ссылаются на один и тот же объект.
Разделяемые ссылки на неизменяемый объект#
Создадим две ссылки a
и b
на один и тот же объект целочисленного типа.
a = 0
b = a
Во второй строке создаётся имя b
и оно связывается с объектом, на которое указывает имя a
. Убедиться, что имена указывают на один и тот же объект, можно сравнив их адреса (id(a) == id(b)
). В python
помимо оператора сравнения на равенство “==
”, ещё есть оператор сравнения на идентичность is
, который по сути дела сравнивает ссылки на объекты.
Note
Сравнение на идентичность — более сильная концепция, чем сравнение на равенство: если объекты по ссылками идентичны (один и тот же объект, никак иначе), то они гарантировано равны, но не наоборот, равные объекты могут являться разными сущностями.
print(a is b)
True
Note
Имена всегда указывают на объекты. Имена не могут указывать на другие имена, но объекты могут содержать в себе ссылки на другие объекты.
Увеличим значение a
на 42.
a += 42
print(f"{a is b = }")
print(f"{a=}, {b=}")
a is b = False
a=42, b=0
Создаётся объект 42
и имя a
связывается с ним. Объект 0
не претерпел изменений (и не мог бы, int
неизменяемый), имя b
по-прежнему связанно с ним.
Таким образом, если есть несколько ссылок на один и тот же неизменяемый объект, можно не переживать, что изменяя объект по одной из этих ссылок, вы измените значение по остальным ссылкам.
Разделяемые ссылки на изменяемый объект#
Предыдущее утверждение не распространяется на изменяемые объекты. Создадим список и две ссылки на него.
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
Видим, что значение по ссылки x
осталось неизменным, хотя оно передавалось первым аргументом в функцию f
.
Теперь повторим это со списками.
l1 = [42]
l2 = f(l1, "abc")
print(f"{l1=}, {l2=}")
print(l1 is l2)
l1=[42, 'a', 'b', 'c'], l2=[42, 'a', 'b', 'c']
True
Значение по ссылке l1
изменилось.
Итого, одна и та же функция изменяет или не изменяет значение своего аргумента в зависимости от того, какого типа этот аргумент. Программист должен сам следить за такими деталями и при необходимости передавать в функцию копию объектов.
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