История вопроса:
Передача параметров в Python реализует принцип "call by object reference" (иногда его называют call by sharing). Это значит, что переменная внутри функции начинает указывать на тот же самый объект в памяти, что и аргумент, переданный снаружи.
Проблема:
Если функция изменяет переданный изменяемый объект (например, список или словарь), изменения видны и снаружи функции. Это может привести к трудноуловимым багам, особенно если ожидается, что функция не будет менять входные данные.
Решение:
Чтобы избежать побочных эффектов, следует делать копию объекта внутри функции либо использовать неизменяемые структуры данных. Для копирования применяют стандартные методы (например, list.copy() для списков, dict.copy() для словарей или copy.deepcopy()).
Пример кода:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # Как избежать изменений? Сделать копию: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Ключевые особенности:
Можно ли быть уверенным, что копия списка через .copy() полностью независима от исходного списка?
Нет — .copy() создаёт поверхностную копию. Если внутри есть вложенные изменяемые объекты, изменения в них будут видны и в оригинале.
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
Является ли возвращение нового объекта на основе входного гарантией отсутствия изменений в оригинале?
Не всегда. Если внутри нового объекта используются части исходного (например, ссылка на внутренний вложенный список), исходный объект может быть изменен.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
Могут ли аргументы-значения по умолчанию для изменяемых объектов привести к проблемам при многократном вызове функции?
Да — значение по умолчанию вычисляется только один раз при определении функции.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Негативный кейс
В библиотеке обработки конфигураций использовали список как значение по умолчанию, что привело к накапливанию элементов между разными вызовами функции. Поведение было непредсказуемым и очень долго выявлялось.
Плюсы:
Меньше кода для повторных вызовов, видимая экономия памяти.
Минусы:
Неявное поведение, трудности при отладке, ошибки вдолгую.
Позитивный кейс
Использование None как значения по умолчанию и явное создание нового объекта при каждом вызове.
Плюсы:
Предсказуемость, отсутствие неожиданных побочных эффектов, надёжность.
Минусы:
Требует осознанности и чуть больше кода.