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

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

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

Note

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

Расширим предыдущий пример ещё двумя строками.

a += 42
../_images/ba0a42.svg

Создаётся объект 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;
}
  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
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