Изменяемые и неизменяемый типы объектов
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