4. Больше инструментов управления потоком выполнения программы

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

4.1. Инструкция if

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

>>> x = int(input("Пожалуйста, введите целое число: "))
Пожалуйста, введите целое число: 42
>>> if x < 0:
...     x = 0
...     print('Отрицательное число заменено нулём')
... elif x == 0:
...     print('Ноль')
... elif x == 1:
...     print('Единица')
... else:
...     print('Больше единицы')
...
Больше единицы

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

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

4.2. Инструкция for

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

>>> # Измеряем несколько строк:
>>> words = ['кот', 'окно', 'выбросить']
>>> for w in words:
...     print(w, len(w))
...
кот 3
окно 4
выбросить 9

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

# Создать пример коллекции
users = {'Hans': 'активен', 'Éléonore': 'неактивна', '景太郎': 'активен'}

# Стратегия: итерироваться по копии
for user, status in users.copy().items():
    if status == 'неактивна':
        del users[user]

# Стратегия: создать новую коллекцию
active_users = {}
for user, status in users.items():
    if status == 'активен':
        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 = ['У', 'Мэри', 'была', 'маленькая', 'овечка']
>>> for i in range(len(a)):
...     print(i, a[i])
...
0 У
1 Мэри
2 была
3 маленькая
4 овечка

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

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

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

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

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

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

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

4.4. Инструкции break и continue

Инструкция break прерывает выполнение самого внутреннего цикла for или while:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(f"{n} equals {x} * {n//x}")
...             break
...
4 equals 2 * 2
6 equals 2 * 3
8 equals 2 * 4
9 equals 3 * 3

Инструкция continue переходит к следующей итерации цикла:

>>> for num in range(2, 10):
...     if num % 2 == 0:
...         print(f"Найдено чётное число {num}")
...         continue
...     print(f"Найдено нечётное число {num}")
...
Найдено чётное число 2
Найдено нечётное число 3
Найдено чётное число 4
Найдено нечётное число 5
Найдено чётное число 6
Найдено нечётное число 7
Найдено чётное число 8
Найдено нечётное число 9

4.5. else ветви в циклах

В цикле for или while инструкция break может сочетаться с ветвью else. Если цикл завершается без выполнения break, выполняется ветвь else.

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

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

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

Это показано в следующем циклом for, который ищет простые числа:

>>> for n in range(2, 10):
...     for x in range(2, n):
...         if n % x == 0:
...             print(n, 'равно', x, '*', n//x)
...             break
...     else:
...         #  цикл завершился, не найдя делителя
...         print(n, ' — простое число')
...
2 — простое число
3 — простое число
4 равно 2 * 2
5 — простое число
6 равно 2 * 3
7 — простое число
8 равно 2 * 4
9 равно 3 * 3

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

Один из способов понять ветвь else в цикле — представить её в паре с инструкцией if внутри цикла. По мере выполнения цикла будет выполняться последовательность действий типа if/if/if/else. if находится внутри цикла и встречается несколько раз. Если условие когда-либо будет истинным, произойдет прерывание цикла с помощью break. Если условие никогда не будет истинным, то будет выполнена ветвь else вне цикла.

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

4.6. Инструкция pass

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

>>> while True:
...     pass  #  Ожидание прерывания с клавиатуры (Ctrl+C)
...

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

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

Другое применение инструкции pass — заглушка для тела функции или ветки условной инструкции, когда вы работаете над новым кодом. Это позволяет думаю о коде на более высоком, абстрактном уровне. Инструкция pass просто игнорируется:

>>> def initlog(*args):
...     pass   # Не забыть реализовать это!
...

В таких случаях многие используют литерал многоточия ... вместо pass. Это использование не имеет особого значения для Python, и не является частью определения языка (здесь можно использовать любое константное выражение), но ... принято использовать как заглушку вместо реального блока кода. См. The Ellipsis Object.

4.7. Инструкция match

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

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

def http_error(status):
    match status:
        case 400:
            return "Некорректный запрос"
        case 404:
            return "Не найдено"
        case 418:
            return "Я — чайник"
        case _:
            return "Что-то не так с интернетом"

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

Можно объединять несколько литералов в одном шаблоне, используя | («или»):

case 401 | 403 | 404:
    return "Доступ запрещён"

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

# point — это кортеж (x, y)
match point:
    case (0, 0):
        print("Начало координат")
    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("Это не точка")

Изучите это внимательно! Первый шаблон имеет два литерала, и его можно рассматривать как расширение шаблона литералов, показанного выше. Но следующие два шаблона объединяют литерал и переменную, а переменная привязывается к значению из сопоставляемого объекта (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("Начало координат")
        case Point(x=0, y=y):
            print(f"Y={y}")
        case Point(x=x, y=0):
            print(f"X={x}")
        case Point():
            print("Где-то в другом месте")
        case _:
            print("Это не точка")

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

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

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

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

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

match points:
    case []:
        print("Нет точек")
    case [Point(0, 0)]:
        print("Начало координат")
    case [Point(x, y)]:
        print(f"Одна точка {x}, {y}")
    case [Point(0, y1), Point(0, y2)]:
        print(f"Две точки на оси Y: {y1}, {y2}")
    case _:
        print("Что-то другое")

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

match point:
    case Point(x, y) if x == y:
        print(f"Точка на диагонали: Y = X = {x}")
    case Point(x, y):
        print(f"Не на диагонали")

Несколько других ключевых особенностей этой инструкции:

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

  • Шаблоны последовательностей поддерживают расширенную распаковку: [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("Введите ваш выбор of 'red', 'blue' или 'green': "))
    
    match color:
        case Color.RED:
            print("Я вижу красный!")
        case Color.GREEN:
            print("Трава зелёная")
        case Color.BLUE:
            print("Похоже, накрыла голубая хандра :(")
    

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

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

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

>>> def fib(n):    # вывести ряд Фибоначчи меньше n
...     """Вывести ряд Фибоначчи для чисел, меньших n."""
...     a, b = 0, 1
...     while a < n:
...         print(a, end=' ')
...         a, b = b, a+b
...     print()
...
>>> # Теперь вызовем только что определённую функцию:
>>> fib(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597

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

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

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

Фактические параметры (аргументы) вызова функции помещаются в локальную таблицу символов вызываемой функции при её вызове; таким образом, аргументы передаются по значению (где значением всегда является ссылка на объект, а не сам объект). [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):  # вернуть ряд Фибоначчи до n
...     """Вернуть список, содержащий ряд Фибоначчи до n."""
...     result = []
...     a, b = 0, 1
...     while a < n:
...         result.append(a)    # см. ниже
...         a, b = b, a+b
...     return result
...
>>> f100 = fib2(100)    # вызвать
>>> f100                # вывести результат
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

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

  • Инструкция return возвращает значение из функции. return без выражения возвращает None. Достижение конца функции также возвращает None.

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

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

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

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

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

def ask_ok(prompt, retries=4, reminder='Пожалуйста, попробуйте ещё раз!'):
    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('некорректный ответ пользователя')
        print(reminder)

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

  • давая только обязательный аргумент: ask_ok('Вы действительно хотите выйти?')

  • передавая один из необязательных аргументов: ask_ok('ОК, чтобы перезаписать файл?', 2)

  • или даже приведя все аргументы: ask_ok('ОК перезаписать файл?', 2, 'Давай, только да или нет!')

В этом примере также представлено ключевое слово 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.9.2. Именованные аргументы

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

def parrot(voltage, state='окоченевший', action='вжух', type='норвежский голубой'):
    print("-- Этот попугай бы не", action, end=' ')
    print("если пропустить через него", voltage, "вольт.")
    print("-- Прекрасное оперение у", type)
    print("-- Он", state, "!")

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

parrot(1000)                                          # 1 позиционный аргумент
parrot(voltage=1000)                                  # 1 именованный аргумент
parrot(voltage=1000000, action='ВЖУУУУХ')             # 2 именованных аргумента
parrot(action='ВЖУУУУХ', voltage=1000000)             # 2 именованных аргумента
parrot('миллион', 'лишён жизни', 'прыжок')         # 3 позиционных аргумента
parrot('тысяча', state='протянул ноги')  # 1 позиционный, 1 именованный

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

parrot()                     # отсутствует обязательный аргумент
parrot(voltage=5.0, 'мёртв')  # позиционный аргумент после именованного
parrot(110, voltage=220)     # повторяющееся значение для одного аргумента
parrot(actor='John Cleese')  # неизвестный именованный аргумент

При вызове функции именованные аргументы должны следовать за позиционными аргументами. Все передаваемые именованные аргументы должны соответствовать одному из аргументов, принимаемых функцией (например, actor не является допустимым аргументом для функции parrot), и их порядок не важен. Это относится и к обязательным параметрам (например, parrot(напряжение=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, он получает словарь (см. Mapping Types — dict), содержащий все именованные аргументы, кроме тех, что соответствуют формальным параметрам. Это можно комбинировать с формальным параметром вида *name (описан в следующем подразделе), который получает кортеж позиционных аргументов кроме явно перечисленных среди формальных параметров. (*name должен стоять перед **name). Например, если определить функцию так:

def cheeseshop(kind, *arguments, **keywords):
    print("-- У вас есть", kind, "?")
    print("-- Извините, у нас закончился весь", kind)
    for arg in arguments:
        print(arg)
    print("-" * 40)
    for kw in keywords:
        print(kw, ":", keywords[kw])

Это можно было бы вызвать так:

cheeseshop("Лимбургер", "Он очень жидкий, сэр.",
           "Он действительно ОЧЕНЬ, ОЧЕНЬ жидкий, сэр.",
           shopkeeper="Майкл Пэйлин",
           client="Джон Клиз",
           sketch="Скетч «Сырная лавка»")

и, конечно же, он напечатает:

-- У вас есть Лимбургер ?
-- Извините, у нас закончился весь Лимбургер
Он очень жидкий, сэр.
Он действительно ОЧЕНЬ, ОЧЕНЬ жидкий, сэр.
----------------------------------------
shopkeeper : Майкл Пэйлин
client : Джон Клиз
sketch : Скетч «Сырная лавка»

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

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

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

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

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Позиционные или именованные   |
        |                                - Только именованные
         -- Только позиционные

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

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

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

4.9.3.2. Только позиционные параметры

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

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

4.9.3.3. Только именованные аргументы

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

4.9.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_arg допускает только именованные аргументы, на что указывает символ * в её определении:

>>> 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.9.3.5. Краткое повторение

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

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

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

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

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

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

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

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

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

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

>>> def concat(*args, sep="/"):
...     return sep.join(args)
...
>>> concat("земля", "марс", "венера")
'земля/марс/венера'
>>> concat("земля", "марс", "венера", sep=".")
'земля.марс.венера'

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

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

>>> list(range(3, 6))            # обычный вызов с отдельными аргументами
[3, 4, 5]
>>> args = [3, 6]
>>> list(range(*args))            # вызов с распаковкой аргументов из списка
[3, 4, 5]

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

>>> def parrot(voltage, state='жёсткий', action='вжух'):
...     print("-- Этот попугай бы не", action, end=' ')
...     print("если бы вы пропустили", voltage, "вольт через него.", end=' ')
...     print("Он", state, "!")
...
>>> d = {"voltage": "четыре миллиона", "state": "совсем окочурился", "action": "ВЖУХ"}
>>> parrot(**d)
-- Этот попугай бы не ВЖУХ если бы вы пропустили четыре миллиона вольт через него. Он совсем окочурился !

4.9.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

В приведенном выше примере лямбда-выражение используется для возврата функции. Другое использование — передать небольшую функцию в качестве аргумента. Например, метод list.sort() принимает функцию сортировки в параметре key, которая может быть лямбда-функцией:

>>> 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.9.7. Строки документации

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

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

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

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

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

>>> def my_function():
...     """Ничего не делает, но документирует это.
...
...     Правда, она действительно ничего не делает.
...
...         >>> my_function()
...         >>>
...     """
...     pass
...
>>> print(my_function.__doc__)
Ничего не делает, но документирует это.

Правда, она действительно ничего не делает.

    >>> my_function()
    >>>

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

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

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

>>> def f(ham: str, eggs: str = 'яйца') -> str:
...     print("Аннотации:", f.__annotations__)
...     print("Аргументы:", ham, eggs)
...     return ham + ' и ' + eggs
...
>>> f('спам')
Аннотации: {'ham': <class 'str'>, 'return': <class 'str'>, 'eggs': <class 'str'>}
Аргументы: спам яйца
'спам и яйца'

4.10. Отступление: стиль написания программ

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

Примечания