Что происходит при передаче изменяемого объекта в функцию в Python

Ответ

В Python аргументы передаются "по присваиванию" (pass-by-assignment), что для изменяемых типов данных (таких как list, dict, set) означает передачу ссылки на сам объект.

Это значит, что любые изменения, произведенные с объектом внутри функции, будут видны и за её пределами, поскольку и внешняя, и внутренняя переменные указывают на одну и ту же область памяти.

Пример: изменение исходного объекта

# Функция добавляет элемент в переданный список
def add_to_list(items: list):
    print(f"  ID объекта внутри функции: {id(items)}")
    items.append(4)

my_list = [1, 2, 3]
print(f"ID объекта до вызова: {id(my_list)}")

add_to_list(my_list)

print(f"Список после вызова функции: {my_list}") # Вывод: [1, 2, 3, 4]

Как избежать нежелательных изменений

Чтобы избежать изменения (мутации) оригинального объекта, необходимо работать с его копией. Для этого можно использовать метод .copy() или срезы [:].

# Функция работает с копией списка
def safe_add_to_list(items: list) -> list:
    local_list = items.copy() # Создаем поверхностную копию
    local_list.append(4)
    return local_list

my_list = [1, 2, 3]
new_list = safe_add_to_list(my_list)

print(f"Оригинальный список: {my_list}") # Вывод: [1, 2, 3]
print(f"Новый список: {new_list}")       # Вывод: [1, 2, 3, 4]

Ответ 18+ 🔞

А, ну вот, опять про эту питонячью магию с передачей аргументов. Слушай, тут народ постоянно обжигается, как дурак на сковородке, потому что не понимает, как это работает. А работает-то всё просто, как три копейки, но с подвохом, блядь.

Вот смотри, в Python аргументы передаются по так называемому "присваиванию". Это такая хитрая жопа, которая для изменяемых типов — списков, словарей, сетов — означает, что ты передаёшь не копию, а ссылку на сам объект в памяти. Прямо как передать другу ключи от своей квартиры и сказать: "Посиди тут". А он возьмёт и обосрёт тебе весь унитаз, и ты потом будешь охуевать.

Поэтому любые изменения, которые ты сделаешь с объектом внутри функции, будут видны и снаружи. Потому что и твоя внешняя переменная, и внутренний аргумент — это два ярлыка, которые смотрят на одну и ту же квартиру. Один крикнул "Муму!" — другой уже плывёт с мешком.

Пример: как всё пиздануться может

# Функция добавляет элемент в переданный список
def add_to_list(items: list):
    print(f"  ID объекта внутри функции: {id(items)}")
    items.append(4)

my_list = [1, 2, 3]
print(f"ID объекта до вызова: {id(my_list)}")

add_to_list(my_list)

print(f"Список после вызова функции: {my_list}") # Вывод: [1, 2, 3, 4]

Видишь? ID один и тот же, квартира одна. Ты внутри функции добавил четвёрку, а она, сука, материализовалась и в твоём исходном списке. Вот тебе и "Муму" на дне. Терпения ноль ебать, когда такое в большом проекте вылезет.

Как не обосраться с этим

Чтобы не менять оригинальный объект, надо работать не с ним, а с его копией. Не давать ключи от квартиры, а сделать дубликат. Для списков есть метод .copy() или старый добрый срез [:].

# Функция работает с копией списка
def safe_add_to_list(items: list) -> list:
    local_list = items.copy() # Создаем поверхностную копию, новый мешок, блядь
    local_list.append(4)
    return local_list

my_list = [1, 2, 3]
new_list = safe_add_to_list(my_list)

print(f"Оригинальный список: {my_list}") # Вывод: [1, 2, 3]
print(f"Новый список: {new_list}")       # Вывод: [1, 2, 3, 4]

Вот теперь красота. Оригинал цел, новая версия с дополнениями лежит отдельно. Главное — помнить, что .copy() делает поверхностную копию. Если внутри списка лежат другие списки или словари, то копируются опять же ссылки на них. Это уже история про глубокое копирование, но это, как говорится, совсем другая история, и там овердохуища нюансов.