В Python при вызове функции используется передача параметров по ссылке или по значению? Сегодня мы попытаемся разобраться. В этой статье вы узнаете: Python не использует модели передачи по ссылке и по значению, а применяет передачу параметров через присваивание; о характеристиках объектов; разницу между изменяемыми(mutable) и неизменяемыми (immutable) объектами.
Передача параметров в других языках
Многие языки программирования предлагают две модели передачи параметров в функции/процедуры:
- передача по значению (pass-by-value),
- передача по ссылке (pass-by-reference).
И то, какую модель использует язык программирования, важно знать, поскольку за эффективность кода отвечает программист лично. А теперь попробуем разобраться, какую модель использует язык Python.
Использует ли Python модель передачи по значению?
При передачи по значению в функцию данные копируются в момент её вызова. Здесь важно понять, что данные (значения) именно копируются, это не те же самые значения. Поэтому в теле функции с ними можно делать всё что угодно, в т.ч. изменять, и это не отразится на исходных данных (тех, которые передали), так как это всего лишь их копии.
В Python не применяется модель передачи по значению, но в некоторых случаях можно видеть нечто похожее. Например, рассмотрим следующий пример:
def foo(x): x = 4 a = 3 foo(a) print(a) # 3
Функция вызывается, но изменения оказанные на переменную a
больше не действую после возврата из функции, ведь a
всё ещё равна 3. Поэтому может показаться, что используется передача по значению, но это не так. Python не копирует значения параметров при вызове функции. Если мы рассмотрим другую функцию:
def clearly_not_pass_by_value(my_list): my_list[0] = 42 l = [1, 2, 3] clearly_not_pass_by_value(l) print(l) # [42, 2, 3]
— то мы четко видим, что элемент исходного списка l
был изменен после вызова функции. Значит ли это, что объект списка использовался один и тот же?
Использует ли Python передачу по ссылке?
При передачи по ссылке в момент вызова функции передаются адреса переменных, причем с адресами работают так, как если бы это была обычная переменная (поэтому не нужно дополнительно проводить разыменование, как это делается в Си). Такая модель подразумевает, что исходные переменные и параметры функции — это одни и те же объекты. Изменяя параметры в теле функции, вы изменяете их и в вызывающем контексте.
Так вот Python не использует и эту модель передачи параметров. Взглянем на следующий код:
def not_pass_by_reference(my_list): my_list = [42, 73, 0] l = [1, 2, 3] not_pass_by_reference(l) print(l) # [1, 2, 3]
Мы предполагаем, что функция должна полностью изменить список, но этого не происходит, он остался прежним. Так какая же модель передачи параметров в функции в используется в Python?
Объекты в Python
В Python всё является объектом. Каждый объект характеризуется следующими характеристиками:
- идентифицирумостью (каждый объект обладает уникальными номером),
- типом (каждый тип обладает своими операциями, которые можно применять),
- содержимое самого объекта.
Эти характеристики можно узнать следующим образом:
>>> id(obj) 2698212637504 >>> type(obj) <class 'list'> >>> obj [1, 2, 3]
Эти знания нам пригодятся в дальнейшем.
Изменяемые и неизменяемые объекты
Объекты в Python могут быть изменяемыми (mutable) или неизменяемыми (immutable). Это свойство полностью зависит от типа объекта. Иными словами, не/изменяемость является характеристикой типа, а не конкретных объектов.
Тип является изменяемым, если содержимое объекта может быть изменено без изменений его идентификатора и типа.
Список (list) является изменяемым типом данных. Почему? Потому что списки являются контейнерами: в них можно добавлять данные и из можно удалять данные. Иными словами, их можно спокойно изменять.
Ниже приведен пример того, как содержимое списка меняется, но идентификатор остается тем же самым, что бы мы ни делали.
>>> obj = [] >>> id(obj) 2287844221184 >>> obj.append(0); obj.extend([1, 2, 3]); obj [42, 0, 1, 2, 3] >>> id(obj) 2287844221184 >>> obj.pop(0); obj.pop(0); obj.pop(); obj 42 0 3 [1, 2] >>> id(obj) 2287844221184
С другой стороны, содержимое неизменяемых объектов нельзя изменить. Они существует так, как их инициализировали в первый раз. Строка str
является хорошим примером неизменяемого типа.
Вызывая различные методы, призванные как-то модифицировать состояние, объекты с неизменяемым типом что-то возвращают, в отличие от изменяемых. Например, метод списка append
ничего не возвращает (он изменяет существующий), а методы строки возвращают новую строку (новый объект):
>>> [].append(0) # Ничего не возвращается >>> "Hello, world!".upper() # Возвращается новая строка 'HELLO, WORLD!"
Другим сигналом неизменяемости объекта является невозможность изменить отдельные его элементы, например, при присваивании через индексы:
>>> obj[0] 'H' >>> obj[0] = "h" Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment
Для справки, в Python встроенные типы:
- неизменяемые —
int
,float
,bool
,str
,tuple
,complex
; - изменяемые —
list
,set
,dict
.
Переменные и их имя
Ещё необходимо понять, что название переменной — это не то же самое, что и сам объект. Например, имя obj
был просто некой меткой, которая была связана с объектом с идентификатором 2287844221184
, типом list
и содержимым 1, 2, 3
. Это называется механизмом связывания, о котором мы говорил тут.
Мы вольны связывать и другие метки к этому же самому объекту:
>>> foo = bar = baz = obj >>> id(foo) 2698212637504 >>> id(bar) 2698212637504 >>> id(baz) 2698212637504 >>> id(obj) 2698212637504 # При этом, если изменить объект: >>> obj = 5 >>> id(obj) 94830320197056
Как видим все метки обращаются к одному объекту. В Python даже есть специальный оператор is
, который проверяет являются ли два объекты одной сущностью (читай: равны ли их id
).
>>> foo is obj True >>> bar is foo True >>> obj is foo True
Изменяя содержимое через одну переменную, эти изменения можно пронаблюдать через другие, поскольку это один и тот же объект. При этом нужно понимать, объекты, которые имеют тот же тип и содержимое, но не id
, не являются одним объектом:
>>> obj is [1, 2, 3] False
В Python используется передача параметров через присваивание
Теперь мы готовы понять, что за модель передачи параметров используется в Python. При вызове функции каждый параметр связывается с соответствующим объектом, указанным в сигнатуре функции.
Так, если мы передаем неизменяемые параметр (например, int
), то у нас нет возможности хоть как-то его изменить. Каждый раз, когда используется присваивание, то создается новый объект, хоть и имеющий то же самое имя. Взгляните на данный пример:
def foo(bar): bar = 3 return bar a = 5 foo(a)
Вызов функции для неизменяемого типа подразумевает, что используется связывание bar = 5
. Сразу после этого в теле функции осуществляется второе связывание bar = 3
. Но все эти объекты разные. Поэтому при использовании неизменяемых объектов в качестве параметров, их передача осуществляется как будто по схеме передаче значений.
Разработка и внедрение ML-решений
Код курса
MLOPS
Ближайшая дата курса
Продолжительность
24 ак.часов
Стоимость обучения
54 000 руб.
С другой стороны, изменяемые объекты, которые передаются в качестве параметров, могут быть изменены, точнее их содержимое. Собственно поэтому функции, которые удаляли или добавляли элементы в переданный в качестве параметра список, изменяли и исходный. А вот когда мы присвоили параметр новому списку (my_list = [42, 73, 0]
), то мы просто создали новый объект и связали его с именем my_list
. Поэтому стоит говорить, что в Python используется модель передачи через присваивание (pass-by-assignment).
Будьте бдительны, что вы передаете в функции
Вышесказанное дает понять, что с параметрами нужно быть осторожными. Если ваша функция ожидает изменяемый объект, нужно предпринять один из следующих шагов:
- не изменять его в теле функции
- задокументировать, что объект будет изменен.
И ещё один момент. Не используйте значения по умолчанию для списков или других подобных структур. Не делайте так:
def foo(a, l=[]): l.append(a) return l
На первый взгляд кажется, что при вызове функции будет создаваться новый объект, но это не так. Объект списка будет один и тот же, если не передавать второй параметр:
>>> foo(1) [1] >>> foo(2) [1, 2] >>> foo(3) [1, 2, 3] >>> foo(3, [4, 5]) [4, 5, 3]
Правильней было бы написать следующим образом:
def foo(a, l=None): if l is None: l = [] l.append(a) return l
Если вы хотите передать изменяемый объект в функцию, при этом для надежности требуется передать его копию, то вам может потребоваться глубокое копирование, о котором мы поговорим в следующей статье.
Ещё больше подробностей об особенностях Python вы узнаете на наших образовательных курсах в лицензированном учебном центре обучения и повышения квалификации руководителей и ИТ-специалистов (менеджеров, архитекторов, инженеров, администраторов, Data Scientist’ов и аналитиков Big Data) в Москве:
- DPREP: Подготовка данных для Data Mining на Python
- Разработка и внедрение ML-решений
- Графовые алгоритмы. Бизнес-приложения
1. https://mathspp.com/blog/pydonts/pass-by-value-reference-and-assignment