Изменяемые и неизменяемый типы объектов#

Рассмотрим ещё один пример.

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
../../_images/ba0.png

Note

Имена всегда указывают на объекты. Имена не могут указывать на другие имена, но объекты могут содержать в себе ссылки на другие объекты.

Увеличим значение a на 42.

a += 42
print(f"{a is b = }")   
print(f"{a=}, {b=}")  
a is b = False
a=42, b=0
../../_images/ba0a42.png

Создаётся объект 42 и имя a связывается с ним. Объект 0 не претерпел изменений (и не мог бы, int неизменяемый), имя b по-прежнему связанно с ним. Таким образом, если есть несколько ссылок на один и тот же неизменяемый объект, можно не переживать, что изменяя объект по одной из этих ссылок, вы измените значение по остальным ссылкам.

Разделяемые ссылки на изменяемый объект#

Предыдущее утверждение не распространяется на изменяемые объекты. Создадим список и две ссылки на него.

L1 = ["a", "b", "c"]
L2 = L1
print(L1 is L2)
True
../../_images/l1l2abc.png

Изменим список 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, так как они ссылаются на один и тот изменяемый объект.

../../_images/l1l2abc123.png

Оба имени 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;
}
  1. x — параметр функции f, передаваемый по значению. При вызове функции, создаётся локальная переменная x, её значение инициализируется значением переменной a. Изменение x никак не повлияет на значение переменной a.

  2. 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