ПрограммированиеPython разработчик / Data Engineer

Что происходит при передаче изменяемого объекта (например, списка или словаря) в функцию Python? Как избежать неожиданных изменений внутри и вне функции?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

Передача параметров в 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]

Ключевые особенности:

  • Передача изменяемого объекта позволяет изменить его состояние как внутри, так и вне функции.
  • Для избежания этого используют копирование данных (shallow/deep copy).
  • Неизменяемые объекты защищены от таких изменений.

Вопросы с подвохом.

Можно ли быть уверенным, что копия списка через .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 как значения по умолчанию и явное создание нового объекта при каждом вызове.

Плюсы:

Предсказуемость, отсутствие неожиданных побочных эффектов, надёжность.

Минусы:

Требует осознанности и чуть больше кода.