Что вы не знали и передаче параметров в функции

автор рубрика
Что вы не знали и передаче параметров в функции

В 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. Но все эти объекты разные. Поэтому при использовании неизменяемых объектов в качестве параметров, их передача осуществляется как будто по схеме передаче значений.


С другой стороны, изменяемые объекты, которые передаются в качестве параметров, могут быть изменены, точнее их содержимое. Собственно поэтому функции, которые удаляли или добавляли элементы в переданный в качестве параметра список, изменяли и исходный. А вот когда мы присвоили параметр новому списку (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) в Москве:

Источники

1. https://mathspp.com/blog/pydonts/pass-by-value-reference-and-assignment

Комментировать