Функции — это жизнь. Правда? Если вы только начали осваивать Python, неважно — первый ли это ваш язык программирования, или вы пришли в Python из другого языка, то вы уже знаете о том, что количество параметров в объявлении функции соответствует количеству аргументов, которые передают функции при вызове.



Это — основы. Это то, что помогает людям понимать окружающий мир. Но утверждение «количество параметров равно количеству аргументов» закладывает в голову новичка бомбу замедленного действия, которая срабатывает после того, как он увидит в объявлении функции таинственные конструкции *args или **kwargs.

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

Позиционные и именованные аргументы


Для того чтобы разобраться с *args и **kwargs, нам нужно освоить концепции позиционных (positional) и именованных (keyword) аргументов.

Сначала поговорим о том, чем они отличаются. В простейшей функции мы просто сопоставляем позиции аргументов и параметров. Аргумент №1 соответствует параметру №1, аргумент №2 — параметру №2 и так далее.

def printThese(a,b,c):
   print(a, "is stored in a")
   print(b, "is stored in b")
   print(c, "is stored in c")
printThese(1,2,3)
"""
1 is stored in a
2 is stored in b
3 is stored in c
"""

Для вызова функции необходимы все три аргумента. Если пропустить хотя бы один из них — будет выдано сообщение об ошибке.

def printThese(a,b,c):
   print(a, "is stored in a")
   print(b, "is stored in b")
   print(c, "is stored in c")
printThese(1,2)
"""
TypeError: printThese() missing 1 required positional argument: 'c'
"""

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

def printThese(a,b,c=None):
   print(a, "is stored in a")
   print(b, "is stored in b")
   print(c, "is stored in c")
printThese(1,2)
"""
1 is stored in a
2 is stored in b
None is stored in c
"""

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

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

def printThese(a=None,b=None,c=None):
   print(a, "is stored in a")
   print(b, "is stored in b")
   print(c, "is stored in c")
printThese(c=3, a=1)
"""
1 is stored in a
None is stored in b
3 is stored in c
"""

Оператор «звёздочка»


Оператор * чаще всего ассоциируется у людей с операцией умножения, но в Python он имеет и другой смысл.

Этот оператор позволяет «распаковывать» объекты, внутри которых хранятся некие элементы. Вот пример:

a = [1,2,3]
b = [*a,4,5,6]
print(b) # [1,2,3,4,5,6]

Тут берётся содержимое списка a, распаковывается, и помещается в список b.

Как пользоваться *args и **kwargs


Итак, мы знаем о том, что оператор «звёздочка» в Python способен «вытаскивать» из объектов составляющие их элементы. Знаемы мы и о том, что существует два вида параметров функций. Вполне возможно, что вы уже додумались до этого сами, но я, на всякий случай, скажу об этом. А именно, *args — это сокращение от «arguments» (аргументы), а **kwargs — сокращение от «keyword arguments» (именованные аргументы).

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

def printScores(student, *scores):
   print(f"Student Name: {student}")
   for score in scores:
      print(score)
printScores("Jonathan",100, 95, 88, 92, 99)
"""
Student Name: Jonathan
100
95
88
92
99
"""

Я не использовал при объявлении функции конструкцию *args. Вместо неё у меня — *scores. Нет ли тут ошибки? Ошибки здесь нет. Дело в том, что «args» — это всего лишь набор символов, которым принято обозначать аргументы. Самое главное тут — это оператор *. А то, что именно идёт после него, особой роли не играет. Благодаря использованию * мы создали список позиционных аргументов на основе того, что было передано функции при вызове.

После того, как мы разобрались с *args, с пониманием **kwargs проблем быть уже не должно. Имя, опять же, значения не имеет. Главное — это два символа **. Благодаря им создаётся словарь, в котором содержатся именованные аргументы, переданные функции при её вызове.

def printPetNames(owner, **pets):
   print(f"Owner Name: {owner}")
   for pet,name in pets.items():
      print(f"{pet}: {name}")
printPetNames("Jonathan", dog="Brock", fish=["Larry", "Curly", "Moe"], turtle="Shelldon")
"""
Owner Name: Jonathan
dog: Brock
fish: ['Larry', 'Curly', 'Moe']
turtle: Shelldon
"""

Итоги


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

  • Используйте общепринятые конструкции *args и **kwargs для захвата позиционных и именованных аргументов.
  • Конструкцию **kwargs нельзя располагать до *args. Если это сделать — будет выдано сообщение об ошибке.
  • Остерегайтесь конфликтов между именованными параметрами и **kwargs, в случаях, когда значение планируется передать как **kwarg-аргумент, но имя ключа этого значения совпадает с именем именованного параметра.
  • Оператор *можно использовать не только в объявлениях функций, но и при их вызове.

Уважаемые читатели! Какими параметрами вы чаще всего пользуетесь при объявлении Python-функций?

Комментарии (7)


  1. dark_gf
    10.01.2020 14:34

    Я не понимаю поведения в питоне

    1)Если по определению a = [1,2,3] и *a = 1,2,3 (пишу образно для понимания моего не понимания :) ), то в функции printScores(student, *scores) когда вызываем printScores(«Jonathan», 100, 95, 88, 92, 99) получается мы делам обратную операцию *, от всех «следующих аргументов» и тут возникает вопрос, почему не от всех аргументов, а только от «последних», а что если я хочу такую функцию printScores(student, *scores, *otherData)?
    И вообще почему нельзя сделать так *a = [1,2,3], что бы получить a = [[1,2,3]]?

    2)def printPetNames(owner, **pets) — тут двойная звездочка, в моём понимании по определению это двойная операция *, т.е. если **pets = dog=«Brock», fish=[«Larry», «Curly», «Moe»], turtle=«Shelldon», то *pets = {dog=«Brock», fish=[«Larry», «Curly», «Moe»], turtle=«Shelldon»]} а pets = [{dog=«Brock», fish=[«Larry», «Curly», «Moe»], turtle=«Shelldon»]}]

    имхо оператор * имеет только похожее поведение на то что используется в функциях (*args и **kwargs)


    1. tolord
      10.01.2020 15:55
      +1

      1) Если ты напишешь print_scores(arg1, arg2, ..., argN, *scores), предполагается, что у тебя N аргументов обязательные, а остальные опциональные, и эти остальные соберутся в отдельный кортеж. То есть конструкция func(arg, *args1, *args2) невозможна, потому что непонятно, как поделить остальные переменные между args1 и args2. Если тебе хочется получить два списка аргументов, лучше явно запихни их в списки (fun(arg1=(1, 2, 3), arg2=(4, 5, 6)))


      Писать *a = [1, 2, 3] нельзя, потому что * это операция распаковки, а не обращение к указателю как в C++. Даже если предположить, что такая конструкция имела бы место, это было бы сильно не pythonic way, потому что гораздо проще и понятнее то, что ты написал: a = [[1, 2, 3]].


      2) Двойная звёздочка это не двойная распаковка. Это сборка именованных параметров в словарь и наоборот. Одинарная звёздочка на словарь вернёт тебе список ключей. Например,


      def fun(*args):
          print(', '.join(args))
      
      fun(*{'kek': 4, 'lol': 8, 'cheburek': 15})

      напечатает на экране: kek, lol, cheburek. Логика «двойной оператор это то же самое, что два одинарных» не работает, потому что есть же разница между = и ==.


      UPD: Коллега ниже отметил, что звезда собирает аргументы в кортеж, исправил оговорку.


    1. MonstarNN
      10.01.2020 15:55
      +1

      В описании функции не допускается многократное использование позиционных и именованных аргументов, то есть такое описание функции, как printScores(student, *scores, *otherData), или же printScores(student, **kwargs, **other_kwargs) недопустимо — интерпретатор упадёт с ошибкой «invalid syntax». Оно и понятно — если более одного набора позиционных или именованных аргументов — нет возможности однозначно поделить аргументы между ними.

      Позиционные аргументы приходят в виде кортежа (tuple), который как раз определяется звёздочкой, именованные аргументы приходят в виде словаря dict, и двойная звёздочка тут есть именно задание словаря. То есть при описании функции звёздочки это не операторы, выполняющие какое-либо действие, а именно описание параметров функции, так для printScores(student, *args, **kwargs) мы указываем, что внутри функции мы будем работать с кортежем args и словарём kwargs, по которым будут «разбросаны» все позиционные и именованные аргументы вызова функции. И чтобы не было путаницы, в вызове сначала указываются позиционные параметры (попадают в кортеж args с сохранением последовательности), а потом именованные, то есть вызов printScores('name', param='value', 42) недопустим.

      При вызове printScores('name', [1,2,3]) в scores будет кортеж ([1,2,3]), ну или, если кортеж преобразовать в список, то будет список: list(scores) = [[1,2,3]]


    1. Pand5461
      10.01.2020 16:10
      +1

      Дело в том, что звёздочки — это не процедуры, а часть синтаксиса. Смысл звёздочек в определении процедуры и в выражении, вызывающем процедуру — разный.


      *x в списке формальных аргументов — обозначает, что принимается произвольное число аргументов, которые в теле процедуры будут доступны в кортеже под именем x. (slurp)
      *x в вызове процедуры — обозначает, что коллекция x распаковывается подставляется как отдельные аргументы. (splat)


      Соответственно, если a = [1, 2, 3], то *a вне контекста вызова процедуры — это синтаксическая ошибка.


  1. Pavel1114
    11.01.2020 05:34

    Отрывок очень не точной и поверхностной перепечатки документации — второе место в топе Хабра. Согласен что для совсем новичков в программировании статья может показаться полезной. Но блин на хабре я ожидаю более глубокие статьи. Может я не прав, может хабр для всех…


    1. SantaCluster
      12.01.2020 12:57

      лично я ожидаю, что если статья написана для новичков (и даже для новичков-новичков), то профессионалы либо не станут читать или комментировать (особенно в духе выражения своего "Фи!") либо прокомментируют по существу — укажут на ошибку или добавят интересный случай/пример из практики


  1. prostomarkeloff
    11.01.2020 16:19
    -1

    Стоило бы хоть PEP8 соблюдать.