4. Другие инструменты для работы с потоком управления

Помимо только что представленного оператора while, в Python используется еще несколько, с которыми мы познакомимся в этой главе.

4.1. if Заявления

Пожалуй, самым известным типом оператора является оператор if. Например:

>>> x = int(input("Please enter an integer: "))
Please enter an integer: 42
>>> if x < 0:
...     x = 0
...     print('Negative changed to zero')
... elif x == 0:
...     print('Zero')
... elif x == 1:
...     print('Single')
... else:
...     print('More')
...
More

Частей elif может быть ноль или больше, а часть else необязательна. Ключевое слово „elif сокращенно означает «else if» и полезно для того, чтобы избежать чрезмерных отступов. В качестве ключевого слова ifelifelif … является заменой операторов switch или case, встречающихся в других языках.

Если вы сравниваете одно и то же значение с несколькими константами или проверяете наличие определенных типов или атрибутов, вам также может пригодиться оператор match. Более подробная информация приведена в разделе match Заявления.

4.2. for Заявления

Оператор for в Python немного отличается от того, к чему вы привыкли в C или Pascal. Вместо того чтобы всегда выполнять итерацию по арифметической прогрессии чисел (как в Pascal) или давать пользователю возможность определить шаг итерации и условие остановки (как в C), оператор for в Python выполняет итерацию по элементам любой последовательности (списка или строки) в том порядке, в котором они появляются в этой последовательности. Например (без каламбура):

>>> # Measure some strings:
... words = ['cat', 'window', 'defenestrate']
>>> for w in words:
...     print(w, len(w))
...
cat 3
window 6
defenestrate 12

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

# Create a sample collection
users = {'Hans': 'active', 'Éléonore': 'inactive', '景太郎': 'active'}

# Strategy:  Iterate over a copy
for user, status in users.copy().items():
    if status == 'inactive':
        del users[user]

# Strategy:  Create a new collection
active_users = {}
for user, status in users.items():
    if status == 'active':
        active_users[user] = status

4.3. Функция range() Функция

Если вам необходимо выполнить итерацию по последовательности чисел, вам пригодится встроенная функция range(). Она генерирует арифметические прогрессии:

>>> for i in range(5):
...     print(i)
...
0
1
2
3
4

Заданная конечная точка никогда не является частью сгенерированной последовательности; range(10) генерирует 10 значений - законные индексы для элементов последовательности длины 10. Можно позволить диапазону начинаться с другого числа или указать другое приращение (даже отрицательное; иногда это называется «шагом»):

>>> list(range(5, 10))
[5, 6, 7, 8, 9]

>>> list(range(0, 10, 3))
[0, 3, 6, 9]

>>> list(range(-10, -100, -30))
[-10, -40, -70]

Для итерации по индексам последовательности можно комбинировать range() и len() следующим образом:

>>> a = ['Mary', 'had', 'a', 'little', 'lamb']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 Mary
1 had
2 a
3 little
4 lamb

Однако в большинстве таких случаев удобно использовать функцию enumerate(), см. Техника зацикливания.

Странная вещь происходит, если вы просто печатаете диапазон:

>>> range(10)
range(0, 10)

Во многих отношениях объект, возвращаемый range(), ведет себя так, как будто это список, но на самом деле это не так. Это объект, который возвращает последовательные элементы нужной последовательности при итерации по нему, но на самом деле он не формирует список, экономя место.

Мы говорим, что такой объект является iterable, то есть подходит в качестве цели для функций и конструкций, которые ожидают чего-то, из чего они могут получать последовательные элементы, пока запас не будет исчерпан. Мы видели, что оператор for является такой конструкцией, а примером функции, принимающей итерируемый объект, является sum():

>>> sum(range(4))  # 0 + 1 + 2 + 3
6

Позже мы увидим больше функций, которые возвращают итерации и принимают итерации в качестве аргументов. В главе Структуры данных мы более подробно рассмотрим list().

4.4. break и continue Высказывания, а также else Клаузулы в циклах

Оператор break выходит из внутреннего цикла for или while.

Цикл for или while может содержать предложение else.

В цикле for условие else выполняется после того, как цикл достигнет своей последней итерации.

В цикле while он выполняется после того, как условие цикла становится ложным.

В любом типе цикла предложение else не выполняется, если цикл был завершен break.

Примером может служить следующий цикл for, который ищет простые числа:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'equals', x, '*', n//x)
...             break
...     else:
...         # loop fell through without finding a factor
...         print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3

(Да, это правильный код. Посмотрите внимательно: предложение else относится к циклу for, не к оператору if).

При использовании в цикле предложение else имеет больше общего с предложением else оператора try, чем с предложением if: предложение else оператора try выполняется, когда не происходит исключения, а предложение else цикла выполняется, когда не происходит break. Подробнее об операторе try и исключениях см. в разделе Обработка исключений.

Оператор continue, также заимствованный из языка C, продолжает следующую итерацию цикла:

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print("Found an even number", num)
...         continue
...     print("Found an odd number", num)
...
Found an even number 2
Found an odd number 3
Found an even number 4
Found an odd number 5
Found an even number 6
Found an odd number 7
Found an even number 8
Found an odd number 9

4.5. pass Заявления

Оператор pass ничего не делает. Его можно использовать, когда утверждение требуется синтаксически, но программа не требует никаких действий. Например:

>>> while True:
...     pass  # Busy-wait for keyboard interrupt (Ctrl+C)
...

Это обычно используется для создания минимальных классов:

>>> class MyEmptyClass:
...     pass
...

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

>>> def initlog(*args):
...     pass   # Remember to implement this!
...

4.6. match Заявления

Оператор match берет выражение и сравнивает его значение с последовательными шаблонами, заданными в виде одного или нескольких блоков case. Внешне это похоже на оператор switch в C, Java или JavaScript (и многих других языках), но в большей степени это похоже на сопоставление шаблонов в таких языках, как Rust или Haskell. Выполняется только первый совпавший шаблон, и он также может извлекать компоненты (элементы последовательности или атрибуты объекта) из значения в переменные.

В простейшей форме значение предмета сравнивается с одним или несколькими литералами:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

Обратите внимание на последний блок: «имя переменной» _ действует как волшебный знак и никогда не перестает совпадать. Если ни один случай не совпадает, ни одна из ветвей не выполняется.

Вы можете объединить несколько литералов в одном шаблоне с помощью | («или»):

case 401 | 403 | 404:
    return "Not allowed"

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

# point is an (x, y) tuple
match point:
    case (0, 0):
        print("Origin")
    case (0, y):
        print(f"Y={y}")
    case (x, 0):
        print(f"X={x}")
    case (x, y):
        print(f"X={x}, Y={y}")
    case _:
        raise ValueError("Not a point")

Изучите его внимательно! В первом шаблоне два литерала, и его можно рассматривать как расширение шаблона литералов, показанного выше. Но следующие два шаблона объединяют литерал и переменную, причем переменная связывает значение из субъекта (point). Четвертый паттерн фиксирует два значения, что делает его концептуально похожим на задание распаковки (x, y) = point.

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def where_is(point):
    match point:
        case Point(x=0, y=0):
            print("Origin")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Somewhere else")
        case _:
            print("Not a point")

Вы можете использовать позиционные параметры с некоторыми встроенными классами, которые обеспечивают упорядочивание своих атрибутов (например, классы данных). Вы также можете задать определенную позицию для атрибутов в шаблонах, установив специальный атрибут __match_args__ в ваших классах. Если он установлен в («x», «y»), то все следующие шаблоны эквивалентны (и все связывают атрибут y с переменной var):

Point(1, var)
Point(1, y=var)
Point(x=1, y=var)
Point(y=var, x=1)

Рекомендуемый способ чтения шаблонов - рассматривать их как расширенную форму того, что вы поместите слева от присваивания, чтобы понять, какие переменные будут установлены в какое значение. Оператор соответствия присваивает только отдельные имена (например, var выше). Имена с точками (например, foo.bar), имена атрибутов (например, x= и y=) или имена классов (распознаются по «(…)» рядом с ними, как Point выше) никогда не присваиваются.

Шаблоны могут быть произвольно вложенными. Например, если у нас есть короткий список Points с добавлением __match_args__, мы можем сопоставить его следующим образом:

class Point:
    __match_args__ = ('x', 'y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

match points:
    case []:
        print("No points")
    case [Point(0, 0)]:
        print("The origin")
    case [Point(x, y)]:
        print(f"Single point {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Two on the Y axis at {y1}, {y2}")
    case _:
        print("Something else")

Мы можем добавить к шаблону предложение if, известное как «охрана». Если охранник ложен, match переходит к попытке выполнения следующего блока case. Обратите внимание, что перехват значения происходит до того, как будет оценена защита:

match point:
    case Point(x, y) if x == y:
        print(f"Y=X at {x}")
    case Point(x, y):
        print(f"Not on the diagonal")

Еще несколько ключевых особенностей этого заявления:

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

  • Шаблоны последовательностей поддерживают расширенную распаковку: [x, y, *rest] и (x, y, *rest) работают аналогично назначениям распаковки. Имя после * также может быть _, поэтому (x, y, *_) соответствует последовательности, состоящей как минимум из двух элементов, не связывая остальные элементы.

  • Шаблоны отображения: {"bandwidth": b, "latency": l} захватывает значения "bandwidth" и "latency" из словаря. В отличие от шаблонов последовательности, лишние ключи игнорируются. Поддерживается также распаковка типа **rest. (Но **_ будет избыточным, поэтому он не разрешен).

  • Подшаблоны могут быть захвачены с помощью ключевого слова as:

    case (Point(x1, y1), Point(x2, y2) as p2): ...
    

    примет второй элемент входных данных за p2 (если входные данные представляют собой последовательность из двух точек)

  • Большинство литералов сравниваются по равенству, однако синглтоны True, False и None сравниваются по тождеству.

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

    from enum import Enum
    class Color(Enum):
        RED = 'red'
        GREEN = 'green'
        BLUE = 'blue'
    
    color = Color(input("Enter your choice of 'red', 'blue' or 'green': "))
    
    match color:
        case Color.RED:
            print("I see red!")
        case Color.GREEN:
            print("Grass is green")
        case Color.BLUE:
            print("I'm feeling the blues :(")
    

Для более подробного объяснения и дополнительных примеров вы можете заглянуть в PEP 636, который написан в формате учебника.

4.7. Определение функций

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

>>> def fib(n):    # write Fibonacci series up to n
...     """Print a Fibonacci series up to n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Now call the function we just defined:
... fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

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

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

При выполнении функции вводится новая таблица символов, используемая для локальных переменных функции. Точнее, все присваивания переменных в функции сохраняют значение в локальной таблице символов; в то время как ссылки на переменные сначала ищутся в локальной таблице символов, затем в локальных таблицах символов вложенных функций, затем в глобальной таблице символов и, наконец, в таблице встроенных имен. Таким образом, глобальным переменным и переменным вложенных функций нельзя напрямую присвоить значение внутри функции (если только глобальные переменные не названы в операторе global или переменные вложенных функций не названы в операторе nonlocal), хотя на них можно ссылаться.

Фактические параметры (аргументы) вызова функции вносятся в локальную таблицу символов вызываемой функции при ее вызове; таким образом, аргументы передаются с помощью call by value (где value - это всегда объектная ссылка, а не значение объекта). [1] Когда функция вызывает другую функцию или рекурсивно вызывает саму себя, для этого вызова создается новая локальная таблица символов.

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

>>> fib
<function fib at 10042ed0>
>>> f = fib
>>> f(100)
0 1 1 2 3 5 8 13 21 34 55 89

Приверженцы других языков могут возразить, что fib - это не функция, а процедура, поскольку она не возвращает значения. На самом деле даже функции без оператора return возвращают значение, хотя и довольно скучное. Это значение называется None (это встроенное имя). Запись значения None обычно подавляется интерпретатором, если это будет единственное записанное значение. Если очень хочется, можно увидеть его, используя print():

>>> fib(0)
>>> print(fib(0))
None

Очень просто написать функцию, которая возвращает список чисел ряда Фибоначчи, вместо того чтобы выводить его на печать:

>>> def fib2(n):  # return Fibonacci series up to n
...     """Return a list containing the Fibonacci series up to n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # see below
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # call it
>>> f100                # write the result
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Этот пример, как обычно, демонстрирует некоторые новые возможности Python:

  • Оператор return возвращает значение из функции. Оператор return без аргумента выражения возвращает None. Отпадение от конца функции также возвращает None.

  • Оператор result.append(a) вызывает метод объекта-списка result. Метод - это функция, которая «принадлежит» объекту и имеет имя obj.methodname, где obj - некоторый объект (это может быть выражение), а methodname - имя метода, определяемого типом объекта. Разные типы определяют разные методы. Методы разных типов могут иметь одинаковые имена, не вызывая двусмысленности. (Можно определить свои собственные типы объектов и методы, используя классы, см. Занятия). Метод append(), показанный в примере, определен для объектов-списков; он добавляет новый элемент в конец списка. В данном примере он эквивалентен result = result + [a], но более эффективен.

4.8. Подробнее об определении функций

Также можно определять функции с переменным числом аргументов. Существует три формы, которые можно комбинировать.

4.8.1. Значения аргументов по умолчанию

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

def ask_ok(prompt, retries=4, reminder='Please try again!'):
    while True:
        reply = input(prompt)
        if reply in {'y', 'ye', 'yes'}:
            return True
        if reply in {'n', 'no', 'nop', 'nope'}:
            return False
        retries = retries - 1
        if retries < 0:
            raise ValueError('invalid user response')
        print(reminder)

Эта функция может быть вызвана несколькими способами:

  • указывая только обязательный аргумент: ask_ok('Do you really want to quit?')

  • задавая один из необязательных аргументов: ask_ok('OK to overwrite the file?', 2)

  • или даже привести все аргументы: ask_ok('OK to overwrite the file?', 2, 'Come on, only yes or no!')

В этом примере также введено ключевое слово in. Оно проверяет, содержит ли последовательность определенное значение или нет.

Значения по умолчанию оцениваются в точке определения функции в определяющей области видимости, так что

i = 5

def f(arg=i):
    print(arg)

i = 6
f()

выведет 5.

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

def f(a, L=[]):
    L.append(a)
    return L

print(f(1))
print(f(2))
print(f(3))

В результате будет напечатано

[1]
[1, 2]
[1, 2, 3]

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

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

4.8.2. Аргументы к ключевым словам

Функции также можно вызывать с помощью keyword arguments в форме kwarg=value. Например, следующая функция:

def parrot(voltage, state='a stiff', action='voom', type='Norwegian Blue'):
    print("-- This parrot wouldn't", action, end=' ')
    print("if you put", voltage, "volts through it.")
    print("-- Lovely plumage, the", type)
    print("-- It's", state, "!")

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

parrot(1000)                                          # 1 positional argument
parrot(voltage=1000)                                  # 1 keyword argument
parrot(voltage=1000000, action='VOOOOOM')             # 2 keyword arguments
parrot(action='VOOOOOM', voltage=1000000)             # 2 keyword arguments
parrot('a million', 'bereft of life', 'jump')         # 3 positional arguments
parrot('a thousand', state='pushing up the daisies')  # 1 positional, 1 keyword

но все следующие вызовы будут недействительны:

parrot()                     # required argument missing
parrot(voltage=5.0, 'dead')  # non-keyword argument after a keyword argument
parrot(110, voltage=220)     # duplicate value for the same argument
parrot(actor='John Cleese')  # unknown keyword argument

В вызове функции аргументы в виде ключевых слов должны следовать за позиционными аргументами. Все передаваемые ключевые аргументы должны совпадать с одним из аргументов, принимаемых функцией (например, actor не является допустимым аргументом для функции parrot), и их порядок не имеет значения. Это касается и неопциональных аргументов (например, parrot(voltage=1000) тоже допустим). Ни один аргумент не может получить значение более одного раза. Вот пример, который не работает из-за этого ограничения:

>>> def function(a):
...     pass
...
>>> function(0, a=0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function() got multiple values for argument 'a'

Когда присутствует конечный формальный параметр вида **name, он получает словарь (см. Типы отображения — dict), содержащий все аргументы ключевых слов, кроме тех, которые соответствуют формальному параметру. Он может быть объединен с формальным параметром вида *name (описан в следующем подразделе), который получает tuple, содержащий позиционные аргументы, выходящие за пределы списка формальных параметров. (*name должен встречаться перед **name.) Например, если мы определим функцию следующим образом:

def cheeseshop(kind, *arguments, **keywords):
    print("-- Do you have any", kind, "?")
    print("-- I'm sorry, we're all out of", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

Его можно назвать так:

cheeseshop("Limburger", "It's very runny, sir.",
           "It's really very, VERY runny, sir.",
           shopkeeper="Michael Palin",
           client="John Cleese",
           sketch="Cheese Shop Sketch")

и, конечно, она будет напечатана:

-- Do you have any Limburger ?
-- I'm sorry, we're all out of Limburger
It's very runny, sir.
It's really very, VERY runny, sir.
----------------------------------------
shopkeeper : Michael Palin
client : John Cleese
sketch : Cheese Shop Sketch

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

4.8.3. Специальные параметры

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

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

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

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

4.8.3.1. Аргументы с позицией или ключевым словом

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

4.8.3.2. Параметры, зависящие только от положения

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

Параметры, следующие за /, могут быть позиционными или ключевыми словами или только ключевыми словами.

4.8.3.3. Аргументы, содержащие только ключевые слова

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

4.8.3.4. Примеры функций

Рассмотрим следующий пример определения функции, обращая пристальное внимание на маркеры / и *:

>>> def standard_arg(arg):
...     print(arg)
...
>>> def pos_only_arg(arg, /):
...     print(arg)
...
>>> def kwd_only_arg(*, arg):
...     print(arg)
...
>>> def combined_example(pos_only, /, standard, *, kwd_only):
...     print(pos_only, standard, kwd_only)

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

>>> standard_arg(2)
2

>>> standard_arg(arg=2)
2

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

>>> pos_only_arg(1)
1

>>> pos_only_arg(arg=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: pos_only_arg() got some positional-only arguments passed as keyword arguments: 'arg'

Третья функция kwd_only_args допускает только аргументы в виде ключевых слов, о чем свидетельствует символ * в определении функции:

>>> kwd_only_arg(3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: kwd_only_arg() takes 0 positional arguments but 1 was given

>>> kwd_only_arg(arg=3)
3

И последняя использует все три соглашения о вызове в одном определении функции:

>>> combined_example(1, 2, 3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() takes 2 positional arguments but 3 were given

>>> combined_example(1, 2, kwd_only=3)
1 2 3

>>> combined_example(1, standard=2, kwd_only=3)
1 2 3

>>> combined_example(pos_only=1, standard=2, kwd_only=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: combined_example() got some positional-only arguments passed as keyword arguments: 'pos_only'

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

def foo(name, **kwds):
    return 'name' in kwds

Не существует возможного вызова, который заставит его вернуть True, поскольку ключевое слово 'name' всегда будет связываться с первым параметром. Например:

>>> foo(1, **{'name': 2})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() got multiple values for argument 'name'
>>>

Но использование / (только позиционные аргументы) возможно, поскольку позволяет использовать name в качестве позиционного аргумента и 'name' в качестве ключа в аргументах ключевого слова:

>>> def foo(name, /, **kwds):
...     return 'name' in kwds
...
>>> foo(1, **{'name': 2})
True

Другими словами, имена только позиционных параметров могут быть использованы в **kwds без двусмысленности.

4.8.3.5. Отзыв

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

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):

В качестве руководства:

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

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

  • Для API используйте только позиционное значение, чтобы не нарушить изменения API, если имя параметра будет изменено в будущем.

4.8.4. Произвольные списки аргументов

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

def write_multiple_items(file, separator, *args):
    file.write(separator.join(args))

Как правило, эти вариативные аргументы будут последними в списке формальных параметров, поскольку на них приходятся все оставшиеся входные аргументы, передаваемые в функцию. Все формальные параметры, которые встречаются после параметра *args, являются аргументами «только для ключевых слов», то есть могут использоваться только как ключевые слова, а не как позиционные аргументы.

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("earth", "mars", "venus")
'earth/mars/venus'
>>> concat("earth", "mars", "venus", sep=".")
'earth.mars.venus'

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

Обратная ситуация возникает, когда аргументы уже находятся в списке или кортеже, но должны быть распакованы для вызова функции, требующей отдельных позиционных аргументов. Например, встроенная функция range() ожидает отдельных аргументов start и stop. Если они недоступны отдельно, напишите вызов функции с помощью *-оператора, чтобы распаковать аргументы из списка или кортежа:

>>> list(range(3, 6))            # normal call with separate arguments
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # call with arguments unpacked from a list
[3, 4, 5]

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

>>> def parrot(voltage, state='a stiff', action='voom'):
...     print("-- This parrot wouldn't", action, end=' ')
...     print("if you put", voltage, "volts through it.", end=' ')
...     print("E's", state, "!")
...
>>> d = {"voltage": "four million", "state": "bleedin' demised", "action": "VOOM"}
>>> parrot(**d)
-- This parrot wouldn't VOOM if you put four million volts through it. E's bleedin' demised !

4.8.6. Лямбда-выражения

Небольшие анонимные функции можно создавать с помощью ключевого слова lambda. Эта функция возвращает сумму двух своих аргументов: lambda a, b: a+b. Лямбда-функции можно использовать везде, где требуются объекты функций. Синтаксически они ограничены одним выражением. Семантически они являются синтаксическим сахаром для обычного определения функции. Как и определения вложенных функций, лямбда-функции могут ссылаться на переменные из содержащей области видимости:

>>> def make_incrementor(n):
...     return lambda x: x + n
...
>>> f = make_incrementor(42)
>>> f(0)
42
>>> f(1)
43

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

>>> pairs = [(1, 'one'), (2, 'two'), (3, 'three'), (4, 'four')]
>>> pairs.sort(key=lambda pair: pair[1])
>>> pairs
[(4, 'four'), (1, 'one'), (3, 'three'), (2, 'two')]

4.8.7. Строки документации

Вот некоторые соглашения о содержании и форматировании строк документации.

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

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

Парсер Python не снимает отступы с многострочных строковых литералов в Python, поэтому инструменты, обрабатывающие документацию, должны снимать отступы по желанию. Для этого используется следующее соглашение. Первая непустая строка после первой строки строки определяет величину отступа для всей строки документации. (Мы не можем использовать первую строку, так как она обычно находится рядом с открывающими кавычками строки, поэтому ее отступ не виден в строковом литерале). Затем пробелы, «эквивалентные» этому отступу, удаляются из начала всех строк строки. Строки с меньшим отступом не должны встречаться, но если они встречаются, то все их ведущие пробельные символы должны быть удалены. Эквивалентность пробельных символов должна проверяться после расширения табуляции (обычно до 8 пробелов).

Вот пример многострочной строки документа:

>>> def my_function():
...     """Do nothing, but document it.
...
...     No, really, it doesn't do anything.
...     """
...     pass
...
>>> print(my_function.__doc__)
Do nothing, but document it.

    No, really, it doesn't do anything.

4.8.8. Аннотации функций

Function annotations - это совершенно необязательные метаданные о типах, используемых пользовательскими функциями (см. PEP 3107 и PEP 484 для получения дополнительной информации).

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

>>> def f(ham: str, eggs: str = 'eggs') -> str:
...     print("Annotations:", f.__annotations__)
...     print("Arguments:", ham, eggs)
...     return ham + ' and ' + eggs
...
>>> f('spam')
Annotations: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Arguments: spam eggs
'spam and eggs'

4.9. Интермеццо: стиль кодирования

Теперь, когда вы собираетесь писать более длинные и сложные фрагменты на Python, самое время поговорить о стиле кодирования. Большинство языков могут быть написаны (или, более кратко, форматированы) в разных стилях; некоторые из них более удобочитаемы, чем другие. Сделать так, чтобы другим было легко читать ваш код, - всегда хорошая идея, и принятие хорошего стиля кодирования очень помогает в этом.

Для Python PEP 8 стал руководством по стилю, которого придерживается большинство проектов; он пропагандирует очень читабельный и приятный для глаз стиль кодирования. Каждый разработчик Python должен прочитать его в какой-то момент; здесь приведены наиболее важные моменты, извлеченные для вас:

  • Используйте отступ в 4 интервала и не делайте табуляции.

    4 пробела - это хороший компромисс между маленьким отступом (позволяет увеличить глубину вложенности) и большим отступом (легче читать). Табуляции вносят путаницу, поэтому их лучше не использовать.

  • Оберните строки так, чтобы они не превышали 79 символов.

    Это помогает пользователям с маленькими дисплеями и позволяет разместить несколько файлов кода рядом на больших дисплеях.

  • Используйте пустые строки для разделения функций и классов, а также больших блоков кода внутри функций.

  • По возможности помещайте комментарии в отдельную строку.

  • Используйте документальные строки.

  • Используйте пробелы вокруг операторов и после запятых, но не непосредственно внутри скобочных конструкций: a = f(1, 2) + g(3, 4).

  • Называйте свои классы и функции одинаково; принято использовать UpperCamelCase для классов и lowercase_with_underscores для функций и методов. Всегда используйте self в качестве имени первого аргумента метода (подробнее о классах и методах см. в разделе Первый взгляд на классы).

  • Не используйте причудливые кодировки, если ваш код предназначен для использования в международном окружении. В любом случае лучше использовать кодировку Python по умолчанию, UTF-8 или даже обычный ASCII.

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

Сноски