Изменяемые и неизменяемые типы, передача аргументов
Две группы типов
| Неизменяемые (immutable) | Изменяемые (mutable) |
|---|---|
int, float, complex, bool |
list |
str, bytes |
dict |
tuple, frozenset |
set |
None |
bytearray |
| экземпляры обычных классов |
У неизменяемого объекта значение нельзя поменять после создания. Любая «модификация» создаёт новый объект.
s = "кот"
print(id(s))
s = s + "ик" # новый объект; имя s перепривязано
print(id(s), s)
nums = [1, 2]
print(id(nums))
nums.append(3) # тот же объект изменился
print(id(nums), nums)
+=: новый объект или тот же?
x += y вызывает x.__iadd__(y), если он есть (изменяет на месте), иначе x = x + y (новый объект):
a = [1, 2]
b = a
a += [3] # list.__iadd__ — на месте
print(a, b, a is b)
x = (1, 2)
y = x
x += (3,) # у tuple нет __iadd__ → x = x + (3,) — новый объект
print(x, y, x is y)
a = [1, 2]
b = a
a = a + [3] # + всегда создаёт новый список
print(a, b, a is b)
Пройдите 👣 Пошагово — номера объектов показывают, где объект новый, а где тот же.
Передача аргументов: call by sharing
При вызове f(obj) параметр функции становится ещё одним именем того же объекта.
def modify(lst, num, text):
lst.append(99) # изменение объекта — видно снаружи
num += 1 # перепривязка локального имени — снаружи не видно
text = text.upper() # то же самое
lst = ["новый"] # перепривязка — снаружи не видно
my_list, my_num, my_text = [1], 10, "abc"
modify(my_list, my_num, my_text)
print(my_list, my_num, my_text)
Правило:
- изменить объект (append, update, присвоить атрибут, элемент) — изменение видно вызывающему;
- присвоить параметру новое значение — меняется только локальное имя.
На собеседовании
«Python передаёт по значению или по ссылке?» Правильный ответ: ни так, ни так — по ссылке на объект (call by object reference / call by sharing). Значение — это ссылка; функция получает копию ссылки. Поэтому изменяемые объекты можно мутировать, но перепривязать переменную вызывающего нельзя.
Как не испортить аргумент
Функция, которая молча меняет переданный список, — источник трудноуловимых ошибок. Варианты:
def normalized_bad(scores):
scores.sort() # побочный эффект!
return scores
def normalized_good(scores):
return sorted(scores) # новый список
data = [3, 1, 2]
print(normalized_good(data), data)
normalized_bad(data)
print(data) # исходные данные изменены
- Возвращайте новый объект (
sorted, включения,{**d, ...}). - Если нужна правка — сделайте копию в начале:
items = list(items). - Если функция специально меняет аргумент — пусть возвращает
None(какlist.sort) и это будет ясно из имени. - Принимайте неизменяемые типы (
tuple,frozenset) там, где менять не нужно.
Неизменяемость «вглубь» не гарантирована
t = (1, 2, [3])
try:
t[2] += [4]
except TypeError as e:
print("TypeError:", e)
print(t)
Кортеж неизменяем, но список внутри — нет. Эта головоломка показывает, что += выполняется в два шага: изменить объект, потом присвоить результат.
Почему неизменяемость полезна
- Хешируемость: неизменяемые объекты можно использовать как ключи словаря и элементы множеств.
- Безопасность: можно передать объект куда угодно — его не испортят.
- Потокобезопасность: неизменяемые данные не нужно защищать блокировками.
- Оптимизации: интернирование строк, кеш малых чисел, константы в байткоде.
Итог
- Неизменяемые: числа,
str,bytes,tuple,frozenset,None. Изменяемые:list,dict,set, обычные объекты. +=меняет на месте, если есть__iadd__(list), иначе создаёт новый объект (int, str, tuple).- Аргументы передаются по ссылке на объект: мутации видны снаружи, перепривязка — нет.
- Не мутируйте аргументы без явной необходимости — возвращайте новые объекты.
Практика решено 0 из 5
Изменяемый тип или нет?
| Изменяемый | Неизменяемый | |
|---|---|---|
list |
||
str |
||
tuple |
||
dict |
||
int |
||
set |
||
frozenset |
||
bytearray |
Что выведет код?
def f(x, items):
x += 1
items += [1]
a, b = 0, []
f(a, b)
print(a, b)
Функция add_tax сейчас портит переданный список цен. Перепишите её так, чтобы она
возвращала новый список цен с налогом 20% (округление до 2 знаков), а исходный не менялся.
Какие тесты проверяют код (3)
assert add_tax([100, 50]) == [120.0, 60.0] p = [10, 20]; r = add_tax(p); assert p == [10, 20], "Исходный список не должен меняться" p = [10]; assert add_tax(p) is not p, "Нужно вернуть новый список"
def add_tax(prices):
return [round(p * 1.2, 2) for p in prices]
Как точнее всего назвать способ передачи аргументов в Python?
Что выведет код?
t = (1, 2, [3])
try:
t[2] += [4]
except TypeError:
pass
print(t)
Войдите, чтобы отмечать пройденные уроки.