Большая история. Эээ, не такая уж большая история.

Мне всегда было интересно, что в аргументах функции python нет ничего, кроме понимания *args и **kwargs, но, очевидно, это гораздо больше, чем просто это. В этой статье я пытаюсь дать общую картину аргументов функций Python. Я, однако, хотел бы знать, действительно ли это была действительно большая картина в конечном итоге или просто еще одна статья, в которой нечего изучать. Что ж, хватило разговоров. Давайте узнаем что-нибудь новое.

У большинства читателей было бы очевидное понимание того, что такое аргументы, но для начала это объекты, отправленные в функцию в качестве входных данных вызывающей стороной. При передаче аргументов функциям происходит несколько вещей в зависимости от типа передаваемых нами объектов (изменяемые / неизменяемые объекты). Вызывающий - это тот, кто вызывает функцию, передавая аргументы. Вот некоторые моменты, над которыми стоит задуматься:

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

def foo(a):
    a = a+5
    print(a)             # prints 15

a = 10
foo(a)
print(a)                 # prints 10

Как мы видим, переменная a не оказывает никакого влияния из-за вызова функции. Это то, что мы видим, когда передаем неизменяемые объекты в аргумент функции.

При передаче изменяемых объектов мы можем увидеть поведение, отличное от того, что мы видели ранее.

def foo(lst):
    lst = lst + ['new entry']
    print(lst)                # Prints ['Book', 'Pen', 'new entry']

lst = ['Book', 'Pen']
print(lst)                    # Prints ['Book', 'Pen']
foo(lst)
print(lst)                    # Prints ['Book', 'Pen']

Мы видели что-нибудь другое? Если ваш ответ был «Нет», значит, вы правы. Однако, когда мы изменяем элемент изменяемого объекта, который находится на месте, мы не увидим того же, что и выше.

def foo(lst):
    lst[1] = 'new entry'
    print(lst)                # Prints ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                     # Prints ['Book', 'Pen']
foo(lst)
print(lst)                     # Prints ['Book', 'new entry']

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

def foo(lst):
    lst = lst[:]
    lst[1] = 'new entry'
    print(lst)                   # Prints ['Book', 'new entry']

lst = ['Book', 'Pen']
print(lst)                       # Prints ['Book', 'Pen']
foo(lst)
print(lst)                       # Prints ['Book', 'Pen']

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

Когда мы передаем аргументы функции, следует помнить следующее: 1) Они читаются слева направо 2) Их можно сопоставить по имени аргумента ключевого слова 3) Предоставление значений по умолчанию для аргументов 4) Сбор нескольких позиционных или ключевых аргументов 5) Отправка нескольких аргументы вызывающей стороны 6) Использование аргументов, содержащих только ключевые слова. Вам должно быть интересно, почему я написал их все в абзаце, а не в качестве указателей. Очень часто, когда мы понимаем контекст, мы склонны пропускать строки при чтении больших абзацев. И, следовательно, выделение каждой точки еще раз обязательно привлечет ваше внимание.

  1. Аргументы читаются слева направо, то есть положение переданных аргументов напрямую сопоставляется с положением переменных в заголовке функции.
def foo(d, e, f):
    print(d, e, f)            

a, b, c = 1, 2, 3
foo(a, b, c)                  # Prints 1, 2, 3
foo(b, a, c)                  # Prints 2, 1, 3
foo(c, b, a)                  # prints 3, 2, 1

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

2. Сопоставление имени аргумента ключевого слова: это напрямую сопоставляет ключевое слово с его соответствующим именем в заголовке функции.

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

a, b, c = 1, 2, 3
foo(a,b,c)                          # Prints 1 2 3
foo(arg1=a, arg2=b, arg3=c)         # Prints 1 2 3
foo(arg3=c, arg2=b, arg1=a)         # Prints 1 2 3
foo(arg2=b, arg1=a, arg3=c)         # Prints 1 2 3

Функция foo принимает 3 аргумента, а именно arg1, arg2 и arg3. Обратите внимание, как вызывающий изменил положение аргументов. Хотя он читается слева направо, python сопоставляет каждый аргумент, переданный вызывающей стороной, с соответствующим именем аргумента в заголовке функции foo. Python сопоставляет аргументы здесь по имени, а не по позиции. Таким образом, независимо от позиции аргумента ключевого слова в вызывающей функции мы видим тот же результат print, который равен 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)                         # Prints 1 0 0
foo(arg1=a, arg2=b )                # Prints 1 2 0
foo(arg1=a, arg2=b, arg3=c)         # Prints 1 2 3

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

foo(arg2=b)                         # Prints 0 2 0
foo(arg2=b, arg3=c )                # Prints 0 2 3

foo(arg3=c)                         # Prints 0 0 3
foo(arg3=c, arg1=a )                # Prints 1 0 3

Это были простые подходы к отправке аргументов функции foo. Давайте поиграемся еще немного, имея в виду #1, #2 и #3.

foo(a, arg2=b)                      # Prints 1 2 0
foo(a, arg2=b, arg3=c)              # Prints 1 2 3
foo(a, b, arg3=c)                   # Prints 1 2 3

foo(a)                              # Prints 1 0 0
foo(a,b)                            # Prints 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 и kwargs в зависимости от типа вызова. *args принимает несколько аргументов, собирает их, формирует кортеж значений и назначает этот кортеж переменной args. Точно так же **kwargs принимает несколько аргументов ключевого слова, собирает их в словарь пар ключ-значение и назначает словарь переменной kwargs.

def foo(*args):
    print(args)

a, b, c = 1, 2, 3

foo(a, b, c)                # Prints (1, 2, 3)
foo(a, b)                   # Prints (1, 2)
foo(a)                      # Prints (1)
foo(b, c)                   # Prints (2, 3)

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

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


foo(a=1, b=2, c=3)        # Prints {'a': 1, 'b': 2, 'c': 3}
foo(a=1, b=2)             # Prints {'a': 1, 'b': 2}
foo(a=1)                  # Prints {'a': 1}
foo(b=2, c=3)             # Prints {'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.

Здесь мы сталкиваемся с еще одним правилом: «Мы не должны определять * после **».

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

Мы можем включать позиционные аргументы и аргументы ключевого слова с * и ** следующим образом:

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

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

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

foo(1, 2, a=1, b=2)                     # Call3
# 1 (2,) {'a': 1, 'b': 2}
foo(1, 2, 3, a=1, b=2)                  # call4
# 1 (2, 3) {'a': 1, 'b': 2}
foo(1, 2)                               # Call5
# 1 (2,) {}

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

В call1 переданы аргументы 1 и a=1, которые являются позиционным аргументом и аргументом ключевого слова соответственно. call2 - это еще один вариант call1. Здесь аргументы переменной длины равны нулю.

В call3 мы передаем 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,)                               # Call1
# 1 0 () {'a': 1}

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

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

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

foo(1, kvar=2)                             # Call5
# 1 2 () {}

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

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

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

Обратите внимание, как работает foo(1). Это связано с тем, что переменная kvar принимает значение по умолчанию, когда вызывающий объект не передает аргумент.

Еще несколько ошибок, которые мы можем увидеть:

foo(kvar=1)                             #call2
>>>
TypeError: foo() missing 1 required positional argument: 'var'
foo(kvar=1, 1, a=1)                      #call2
>>>
SyntaxError: positional argument follows keyword argument
foo(1, kvar=2, 3, a=2)                   #call3
>>>
SyntaxError: positional argument follows keyword argument

Обратите внимание, как call3 вызвал ошибку.

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

args = (1, 2, 3, 4)
print(*args)                  # Prints 1 2 3 4
print(args)                   # Prints (1, 2, 3, 4)

kwargs = { 'a':1, 'b':2}
print(kwargs)                 # Prints {'a': 1, 'b': 2}
print(*kwargs)                # Prints 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 пытается переопределить позиционный аргумент. Как мы видели во втором разделе при передаче аргументов ключевого слова, порядок не имеет значения. Мы видим аналогичную ошибку в любом случае.

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

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'

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

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, потому что они использовали бы значения по умолчанию из определения функции.

Это была действительно долгая история. Фуу. Интересно, что чувствовали читатели этой статьи? Вы узнали что-то новое или это просто освежило ваш разум? Есть небольшая часть (правила), о которой я не писал, о которой я хотел бы написать в другой статье, чтобы не утомлять моих читателей большим объемом контента, который нужно переварить.