[Перевод] Большая история аргументов функций в Python

Ну, на самом деле, история аргументов в Python не такая уж и большая.

Я всегда удивлялся тому, что для работы с аргументами Python-функций достаточно лишь разобраться с *args и **kwargs. И удивлялся я не зря. Как оказалось, аргументы — это далеко не так просто. В этом материале я хочу дать общий обзор всего того, что связано с аргументами функций в Python. Надеюсь, что в итоге у меня, и правда, получится показать общую картину работы с аргументами, и что эта статья не станет очередной публикацией, в которой читателю не удастся найти ничего нового. А теперь — к делу.

6nee3b16_u-trfzwnynnh6fu3te.png

Большинству читателей этой статьи, полагаю, понятна сущность аргументов функций. Для начинающих поясню, что это — объекты, отправляемые функции инициатором её вызова. При передаче аргументов функции выполняется множество действий, зависящих от того, объекты какого типа отправляют функции (изменяемые или неизменяемые объекты). Инициатор вызова функции — это сущность, которая вызывает функцию и передаёт ей аргументы. Говоря о вызове функций, стоит поразмыслить над некоторыми вещами, которые мы сейчас обсудим.
В аргументы, имена которых заданы при объявлении функции, записываются объекты, передаваемые функциям при вызове. При этом, если соответствующим локальным переменным функций, их параметрам, что-то присваивают, эта операция не влияет на передаваемые функциям неизменяемые объекты. Например:

def foo(a):
    a = a+5
    print(a)             # Выводит 15

a = 10
foo(a)
print(a)                 # Выводит 10


Как видно, вызов функции никак не повлиял на переменную a. Именно это происходит в том случае, если функции передаётся неизменяемый объект.

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

def foo(lst):
    lst = lst + ['new entry']
    print(lst)                # Выводит ['Book', 'Pen', 'new entry']

lst = ['Book', 'Pen']
print(lst)                    # Выводит ['Book', 'Pen']
foo(lst)
print(lst)                    # Выводит ['Book', 'Pen']


Заметили ли вы тут что-то новое? Если вы ответите «Нет», то будете правы. Но если как-то повлиять на элементы изменяемого объекта, переданного функции, мы станем свидетелями кое-чего другого.

def foo(lst):
    lst[1] = 'new entry'
    print(lst)                # Выводит ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                     # Выводит ['Book', 'Pen']
foo(lst)
print(lst)                     # Выводит ['Book', 'new entry']


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

def foo(lst):
    lst = lst[:]
    lst[1] = 'new entry'
    print(lst)                   # Выводит ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                       # Выводит ['Book', 'Pen']
foo(lst)
print(lst)                       # Выводит ['Book', 'Pen']


Это вас ещё не удивило? Если нет — тогда хотелось бы мне сделать так, чтобы вы, пропустив то, что вам известно, сразу же перешли к новому для вас материалу. А если да — то, помяните мои слова, вы, ближе знакомясь с аргументами, узнаете ещё много интересного.

Итак, вот что следует знать об аргументах функций:

  1. Порядок передачи функциям позиционных аргументов.
  2. Порядок передачи функциям именованных аргументов.
  3. Назначение значений аргументов, применяемых по умолчанию.
  4. Организация обработки наборов аргументов переменной длины.
  5. Распаковка аргументов.
  6. Использование аргументов, которые можно передавать только по имени (keyword-only).


Разберём каждый из этих пунктов.

1. Порядок передачи функциям позиционных аргументов


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

def foo(d, e, f):
    print(d, e, f)

a, b, c = 1, 2, 3
foo(a, b, c)                  # Выводит 1, 2, 3
foo(b, a, c)                  # Выводит 2, 1, 3
foo(c, b, a)                  # Выводит 3, 2, 1


Переменные a, b и c имеют, соответственно, значения 1, 2 и 3. Эти переменные играют роль аргументов, с которыми вызывается функция foo. Они, при первом вызове функции, соответствуют параметрам d, e и f. Этот механизм применим практически во всех из вышеперечисленных выше 6 пунктов, касающихся того, что нужно знать об аргументах функций в Python. Место размещения позиционного аргумента, передаваемого функции при вызове, играет главную роль при назначении значений параметрам функции.

2. Порядок передачи функциям именованных аргументов


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

def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(a,b,c)                          # Выводит 1 2 3
foo(arg1=a, arg2=b, arg3=c)         # Выводит 1 2 3
foo(arg3=c, arg2=b, arg1=a)         # Выводит 1 2 3
foo(arg2=b, arg1=a, arg3=c)         # Выводит 1 2 3


Как видите, функция foo принимает 3 аргумента. Эти аргументы имеют имена arg1, arg2 и arg3. Обратите внимание на то, как мы, при вызове функции, меняем позиции аргументов. Именованные аргументы обрабатываются не так, как позиционные, хотя система продолжает читать их слева направо. Python, при назначении соответствующих значений параметрам функций, учитывает имена аргументов, а не их позиции. В результате оказывается, что функция выводит одно и то же независимо от позиций переданных ей аргументов. Это — всегда 1 2 3.

Обратите внимание на то, что здесь продолжают действовать механизмы, описанные в пункте №1.

3. Назначение значений аргументов, применяемых по умолчанию


Именованным аргументам можно назначать значения, применяемые по умолчанию. При использовании этого механизма в функции определённые аргументы становятся необязательными. Объявление подобных функций выглядит как то, что мы рассматривали в пункте №2. Единственное различие заключается в том, как именно вызываются эти функции.

def foo(arg1=0, arg2=0, arg3=0):
    print(arg1, arg2, arg3)

a, b, c = 1, 2, 3
foo(arg1=a)                         # Выводит 1 0 0
foo(arg1=a, arg2=b )                # Выводит 1 2 0
foo(arg1=a, arg2=b, arg3=c)         # Выводит 1 2 3


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

foo(arg2=b)                         # Выводит 0 2 0
foo(arg2=b, arg3=c )                # Выводит 0 2 3

foo(arg3=c)                         # Выводит 0 0 3
foo(arg3=c, arg1=a )                # Выводит 1 0 3


Это — простые и понятные примеры использования вышеописанных механизмов вызова функций с передачей ей именованных аргументов. А теперь давайте усложним наши эксперименты, объединив то, о чём мы до сих пор говорили в пунктах №1, №2 и №3:

foo(a, arg2=b)                      # Выводит 1 2 0
foo(a, arg2=b, arg3=c)              # Выводит 1 2 3
foo(a, b, arg3=c)                   # Выводит 1 2 3

foo(a)                              # Выводит 1 0 0
foo(a,b)                            # Выводит 1 2 0


Тут при вызове функции использованы и позиционные и именованные аргументы. При использовании позиционных аргументов порядок их указания играет, как и прежде, важнейшую роль в правильной передаче функции входных данных.

Здесь мне хотелось бы обратить ваше внимание на одну примечательную деталь. Она заключается в том, что позиционные аргументы нельзя указывать после именованных аргументов. Вот пример, который позволит вам лучше понять эту идею:

foo(arg1=a, b)
>>>
foo(arg1=a, b)
           ^
SyntaxError: positional argument follows keyword argument
foo(a, arg2=b, c)
>>>
foo(a, arg2=b, c)
              ^
SyntaxError: positional argument follows keyword argument


Вы можете воспринимать это как правило. Позиционные аргументы не должны следовать за именованными аргументами при вызове функции.

4. Организация обработки наборов аргументов переменной длины


Здесь речь пойдёт о конструкциях *args и **kwargs. Когда эти конструкции используются при объявлении функции, мы ожидаем, что при вызове функции наборы аргументов произвольной длины будут представлены в виде параметров args и kwargs. При применении конструкции *args в параметр args попадают позиционные аргументы, представляемые в виде кортежа. При применении **kwargs в kwargs попадают именованные аргументы, представленные в виде словаря.

def foo(*args):
    print(args)

a, b, c = 1, 2, 3

foo(a, b, c)                # Выводит (1, 2, 3)
foo(a, b)                   # Выводит (1, 2)
foo(a)                      # Выводит (1)
foo(b, c)                   # Выводит (2, 3)


Этот код доказывает то, что в параметре args хранится кортеж, содержащий то, что передано функции при её вызове.

def foo(**kwargs):
    print(kwargs)

foo(a=1, b=2, c=3)        # Выводит {'a': 1, 'b': 2, 'c': 3}
foo(a=1, b=2)             # Выводит {'a': 1, 'b': 2}
foo(a=1)                  # Выводит {'a': 1}
foo(b=2, c=3)             # Выводит {'b': 2, 'c': 3}


В вышеприведённом коде показано то, что в параметре kwargs хранится словарь, состоящий из пар ключ-значение и представляющий именованные аргументы, переданные функции при вызове.

Но надо отметить, что функции, рассчитанной на приём позиционных аргументов, нельзя передавать именованные аргументы (и наоборот).

def foo(*args):
    print(args)

foo(a=1, b=2, c=3)
>>>
foo(a=1, b=2, c=3)
TypeError: foo() got an unexpected keyword argument 'a'
#########################################################
def foo(**kwargs):
    print(kwargs)

a, b, c = 1, 2, 3
foo(a, b, c)
>>>
TypeError: foo() takes 0 positional arguments but 3 were given


А теперь давайте соберём вместе всё то, что мы разобрали в пунктах №1, №2, №3 и №4, и со всем этим поэкспериментируем, исследовав разные комбинации аргументов, которые можно передавать функциям при их вызове.

def foo(*args,**kwargs):
    print(args, kwargs)

foo(a=1,)
# () {'a': 1}

foo(a=1, b=2, c=3)
# () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)
# (1, 2) {'a': 1, 'b': 2}

foo(1, 2)
# (1, 2) {}


Как видите, в нашем распоряжении оказывается кортеж args и словарь kwargs.

А вот — ещё одно правило. Оно заключается в том, что конструкцию *args нельзя использовать после конструкции **kwargs.

def foo(**kwargs, *args):
    print(kwargs, args)
>>>
    def foo(**kwargs, *args):
                      ^
SyntaxError: invalid syntax


То же самое правило распространяется и на порядок указания аргументов при вызове функций. Позиционные аргументы не должны следовать за именованными.

foo(a=1, 1)
>>>
    foo(a=1, 1)
            ^
SyntaxError: positional argument follows keyword argument
foo(1, a=1, 2)
>>>
    foo(1, a=1, 2)
               ^
SyntaxError: positional argument follows keyword argument


При объявлении функций можно комбинировать позиционные аргументы, *args и *kwagrs следующим образом:

def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(1, a=1,)                            # Вызов 1
# 1 () {'a': 1}

foo(1, a=1, b=2, c=3)                   # Вызов 2
# 1 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, a=1, b=2)                     # Вызов 3
# 1 (2,) {'a': 1, 'b': 2}
foo(1, 2, 3, a=1, b=2)                  # Вызов 4
# 1 (2, 3) {'a': 1, 'b': 2}
foo(1, 2)                               # Вызов 5
# 1 (2,) {}


При объявлении функции foo мы исходили из того, что у неё должен быть один обязательный позиционный аргумент. За ним следует набор позиционных аргументов переменной длины, а за этим набором идёт набор именованных аргументов переменной длины. Зная это, мы легко сможем «расшифровать» каждый из вышеприведённых вызовов функции.

В Вызове 1 функции переданы аргументы 1 и a=1. Это, соответственно, позиционный и именованный аргументы. Вызов 2 — это разновидность Вызова 1. Здесь длина набора позиционных аргументов равна нулю.

В Вызове 3 мы передаём функции 1, 2 и a=1,b=2. Это значит, что она теперь принимает два позиционных аргумента и два именованных аргумента. В соответствии с объявлением функции оказывается, что 1 воспринимается как обязательный позиционный аргумент, 2 идёт в набор позиционных аргументов переменной длины, а a=1 и b=2 попадают в набор именованных аргументов переменной длины.

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

def foo(var, *args,**kwargs):
    print(var, args, kwargs)

foo(a=1)
>>>
foo(a=1)
TypeError: foo() missing 1 required positional argument: 'var'


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

def foo(var, kvar=0, *args,**kwargs):
    print(var, kvar, args, kwargs)

foo(1, a=1,)                               # Вызов 1
# 1 0 () {'a': 1}

foo(1, 2, a=1, b=2, c=3)                   # Вызов 2
# 1 0 () {'a': 1, 'b': 2, 'c': 3}

foo(1, 2, 3, a=1, b=2)                     # Вызов 3
# 1 2 () {'a': 1, 'b': 2}

foo(1, 2, 3, 4, a=1, b=2)                  # Вызов 4
# 1 2 (3,) {'a': 1, 'b': 2}

foo(1, kvar=2)                             # Вызов 5
# 1 2 () {}


Вызовы этой функции можно «расшифровать» так же, как это делалось при анализе предыдущей функции.

При вызове этой функции ей надо передавать, как минимум, один позиционный аргумент. Иначе мы столкнёмся с ошибкой:

foo()
>>>
foo()
TypeError: foo() missing 1 required positional argument: 'var'
foo(1)
# 1 0 () {}


Обратите внимание на то, что вызов foo(1) работает нормально. Дело тут в том, что в том случае, если функцию вызывают, не указывая значение для именованного аргумента, значение ему назначается автоматически.

А вот ещё некоторые ошибки, с которыми можно столкнуться при неправильном вызове этой функции:

foo(kvar=1)                             # Вызов 1
>>>
TypeError: foo() missing 1 required positional argument: 'var'
foo(kvar=1, 1, a=1)                      # Вызов 2
>>>
SyntaxError: positional argument follows keyword argument
foo(1, kvar=2, 3, a=2)                   # Вызов 3
>>>
SyntaxError: positional argument follows keyword argument


Обратите особое внимание на ошибку, возникающую при выполнении Вызова 3.

5. Распаковка аргументов


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

args = (1, 2, 3, 4)
print(*args)                  # Выводит 1 2 3 4
print(args)                   # Выводит (1, 2, 3, 4)

kwargs = { 'a':1, 'b':2}
print(kwargs)                 # Выводит {'a': 1, 'b': 2}
print(*kwargs)                # Выводит a b


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

def foo(a, b=0, *args, **kwargs):
    print(a, b, args, kwargs)

tup = (1, 2, 3, 4)
lst = [1, 2, 3, 4]
d = {'e':1, 'f':2, 'g':'3'}

foo(*tup)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(*lst)             # foo(1, 2, 3, 4)
# 1 2 (3, 4) {}

foo(1, *tup)          # foo(1, 1, 2, 3, 4)
# 1 1 (2, 3, 4) {}

foo(1, 5, *tup)       # foo(1, 5, 1, 2, 3, 4)
# 1 5 (1, 2, 3, 4) {}

foo(1, *tup, **d)     # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 1 (2, 3, 4) {'e': 1, 'f': 2, 'g': '3'}

foo(*tup, **d)         # foo(1, 1, 2, 3, 4 ,e=1 ,f=2, g=3)
# 1 2 (3, 4) {'e': 1, 'f': 2, 'g': '3'}
d['b'] = 45
foo(2, **d)             # foo(1, e=1 ,f=2, g=3, b=45)
# 2 45 () {'e': 1, 'f': 2, 'g': '3'}


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

Экспериментируя с распаковкой аргументов, можно столкнуться с новой ошибкой:

foo(1, *tup, b=5)
>>>
TypeError: foo() got multiple values for argument 'b'
foo(1, b=5, *tup)
>>>
TypeError: foo() got multiple values for argument 'b'


Эта ошибка возникает из-за конфликта именованного аргумента, b=5, и позиционного аргумента. Как мы выяснили в разделе №2, при передаче именованных аргументов их порядок значения не имеет. В результате в обоих случаях возникает одна и та же ошибка.

6. Использование аргументов, которые можно передавать только по имени (keyword-only)


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

def foo(a, *args, b):
    print(a, args, b)

tup = (1, 2, 3, 4)

foo(*tup, b=35)
# 1 (2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, 5, *tup, b=35)
# 1 (5, 1, 2, 3, 4) 35

foo(1, *tup, b=35)
# 1 (1, 2, 3, 4) 35

foo(1, b=35)
# 1 () 35

foo(1, 2, b=35)
# 1 (2,) 35

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'b'

foo(1, 2, 3)
# TypeError: foo() missing 1 required keyword-only argument: 'b'


Как видите, ожидается, что функции обязательно будет передан именованный аргумент b, который, в объявлении функции, указан после *args. При этом в объявлении функции можно использовать просто символ *, после которого, через запятую, идут идентификаторы именованных аргументов, которые можно передавать функции только по имени. Такая функция не будет рассчитана на приём набора позиционных аргументов переменной длины.

def foo(a, *, b, c):
    print(a, b, c)

tup = (1, 2, 3, 4)

foo(1, b=35, c=55)
# 1 35 55

foo(c= 55, b=35, a=1)
# 1 35 55

foo(1, 2, 3)
# TypeError: foo() takes 1 positional argument but 3 were given

foo(*tup, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given

foo(1, b=35)
# TypeError: foo() takes 1 positional argument but 4 positional arguments (and 1 keyword-only argument) were given


Функция, объявленная в предыдущем примере, принимает один позиционный аргумент и два именованных аргумента, которые можно передавать только по имени. Это приводит к тому, что для правильного вызова функции ей необходимо передавать оба именованных аргумента. После * можно описывать и именованные аргументы, которым заданы значения, применяемые по умолчанию. Это даёт нам определённую свободу при вызове подобных функций.

def foo(a, *, b=0, c, d=0):
    print(a, b, c, d)

foo(1, c=55)
# 1 0 55 0

foo(1, c=55, b=35)
# 1 35 55 0

foo(1)
# TypeError: foo() missing 1 required keyword-only argument: 'c'


Обратите внимание на то, что функцию можно нормально вызывать, не передавая ей аргументы b и d, так как для них заданы значения, применяемые по умолчанию.

Итоги


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

Узнали ли вы из этого материала что-то новое об аргументах функций в Python?

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru