Именованные аргументы. Фактические и формальные параметры

Курс по Python: https://stepik.org/course/100707

Сейчас мы с вами будем говорить о способах определения параметров в функциях и передачи им аргументов. Казалось бы, мы подробно разобрали эту тему и каждый из вас уже знает, что при объявлении функции в круглых скобках через запятую можно указать сколько угодно параметров. Например, простая функция вычисления объема прямоугольного параллелепипеда, очевидно должна принимать, как минимум, три параметра (ширину, высоту и глубину):

def get_V(a, b, c):
    print(f"a = {a}, b = {b}, c = {c}")
    return a * b * c

А, затем, может быть вызвана с конкретными числовыми значениями:

v = get_V(1, 2, 3)
print(v)

Причем, параметру a будет соответствовать число 1, параметру b – число 2, а c – число 3. Это так, потому что здесь используется позиционная запись аргументов при вызове функции, то есть, значения параметров a, b, c определяются порядком записи аргументов. А можно ли, не меняя порядка, параметру b присвоить 1, параметру c – 2, а a – 3? Оказывается да, в языке Python такое возможно, если явно указывать имена параметров при вызове функции:

v = get_V(b=1, a=2, c=3)

Такие аргументы называются именованными. Теперь, при запуске программы, мы видим, указанные значения у параметров a, b и c.

А можем ли мы комбинировать позиционные и именованные аргументы? Да и такое тоже возможно. Только вначале следует указывать позиционные, а в конце – именованные, например, так:

v = get_V(1, c=2, b=3)

Если же, мы не будем следовать этому правилу и позиционные аргументы запишем после именованного:

v = get_V(a=1, 2, 3)

то возникнет синтаксическая ошибка – так делать нельзя. Сначала всегда позиционные и только потом – именованные:

v = get_V(1, 2, c=3)

Причем, обратите внимание, последним именованным аргументом здесь может быть только имя параметра c. Если записать любой другой из трех, например b:

v = get_V(1, 2, b=3)

то у нас получится дублирование передаваемых данных. Второй позиционный аргумент уже присваивается параметру b, а далее, мы снова этому же параметру присваиваем значение 3. Так делать нельзя.

Вернемся теперь к параметрам самой функции. Мы их объявили просто через запятую с именами a, b, c. Однако, можно задавать параметры со значениями по умолчанию, например, так:

def get_V(a, b, c, verbose=True):
    if verbose:
        print(f"a = {a}, b = {b}, c = {c}")
 
    return a * b * c

Такие параметры называются формальными, а обычные – фактическими. В чем отличие формальных параметров от фактических, помимо значений по умолчанию? Их не обязательно прописывать при вызове функции. Например, наш прежний вызов:

v = get_V(1, 2, 3)

сработает без каких-либо проблем. Мы не указали аргумент для последнего формального параметра verbose. В этом случае он принимает значение по умолчанию True. Если же указать его:

v = get_V(1, 2, 3, False)

то функция print() внутри функции вызвана уже не будет. Разумеется, можно использовать и соответствующий именованный аргумент:

v = get_V(1, 2, 3, verbose=False)

Все будет работать также.

Зачем вообще нужны формальные параметры и когда их следует использовать? Я, думаю, ответ здесь очевиден – для удобства использования функций. Как мы только что видели, аргументы формальным параметрам можно не передавать, если нас устраивает поведение функции по умолчанию. В других, как полагается, редких ситуациях, всегда можно поменять значение такого параметра на другое и скорректировать работу функции.

Чтобы это было понятнее, приведу такой простой пример. Допустим, мы собираемся сравнивать строки в разных режимах: с учетом и без учета регистра, а также учитывать или не учитывать пробелы вначале и в конце. Для этого объявим следующую функцию:

def compare_str(s1, s2, reg=False, trim=True):
    if reg:
        s1 = s1.lower()
        s2 = s2.lower()
    if trim:
        s1 = s1.strip()
        s2 = s2.strip()
 
    return s1 == s2

Формальные параметры reg и trim определяют наиболее частый вариант использования операции сравнения строк: с учетом регистра и с удалением пробелов.

Теперь мы можем взывать эту функцию, просто с двумя аргументами:

print(compare_str("Python ", "  Python"))

или менять ее поведение, указывая другие значения формальных параметров:

print(compare_str("Python ", "  Python", trim=False))
print(compare_str("Python", "PYTHON", True, False))

В последнем варианте записаны обычные позиционные аргументы. В этом случае параметр reg примет значение True, а параметр trim – False. Конечно, всегда можно воспользоваться и именованными аргументами, здесь полная свобода выбора:

print(compare_str("Python", "PYTHON", reg=True, trim=False))

В заключение этого занятия хочу показать вам один нюанс при объявлении функции с формальными параметрами. Предположим, мы определяем вот такую очень простую функцию:

def add_value(value, lst=[]):
    lst.append(value)
    return lst

У нее один фактический и один формальный параметр, причем, второй параметр lst по умолчанию ссылается на изменяемый тип данных – список. Конечно, нам никто не запрещает этого делать, но давайте посмотрим, как она будет работать. Вызовем ее два раза:

l = add_value(1)
l = add_value(2)
print(l)

В консоли видим два значения в списке 1 и 2. Возможно, объявляя таким образом функцию, мы ожидали, что  при каждом вызове параметр lst будет ссылаться на пустой список и функция будет каждый раз возвращать один элемент в этом списке. Но, получилось, что она сохраняет прежнее состояние списка при повторном вызове. Почему так произошло?

На самом деле, все просто. Когда мы объявляем функцию, то создается объект-функция и объекты для формальных параметров, в данном случае пустой список. Когда, затем, мы вызываем функцию, то (опуская некоторые детали) формальный параметр lst ссылается на этот список и добавление элемента происходит в него. При повторном вызове функции lst продолжает ссылаться на этот же список и в него добавляется следующее значение. Это все показывает, что при вызовах функции список не инициализируется повторно, а используется один и тот же объект.

Конечно, мы можем при вызовах функции каждый раз передавать пустой список:

l = add_value(1, [])
l = add_value(2, [])

И тогда формальный параметр lst будет при каждом вызове ссылаться уже на новый пустой список. А как сделать так, чтобы такое поведение было по умолчанию, чтобы нам явно не приходилось передавать пустой список? Исправить ситуацию можно, например, так:

def add_value(value, lst=None):
    if lst is None:
        lst = []
 
    lst.append(value)
    return lst

Значение формального параметра определим неизменяемым значением None, а в самой функции сделаем проверку, если этот параметр равен None, то есть, функция вызвана с одним первым аргументом, то создаем новый пустой список и в него помещаем значение. В этом случае, при каждом вызове функции:

l = add_value(1)
l = add_value(2)

будет автоматически создаваться новый список. То, что мы и хотели. А если хотим продолжить предыдущий, то достаточно его указать вторым аргументом:

l = add_value(2, l)

Вот такой нюанс существует при определении формальных параметров функции на изменяемые объекты, такие как списки, словари и множества.

Итак, из этого занятия вам нужно запомнить, что такое позиционные и именованные аргументы и как их можно записывать и комбинировать. Что такое фактические и формальные параметры, зачем нужны и как используются на практике, ну и, конечно же, как работают формальные параметры с изменяемыми типами данных. Закрепляйте материал практическими заданиями и переходите к следующему уроку.

Курс по Python: https://stepik.org/course/100707

Видео по теме