Python для сбора данных

Алла Тамбовцева, НИУ ВШЭ

Lambda-функции, исключения, assert.

Lambda-функции в Python

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

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

In [2]:
sq = lambda x: x ** 2 # готово

Использовать эту функцию можно как функции, заданные через def:

In [3]:
sq(10)
Out[3]:
100
In [4]:
sq(-7)
Out[4]:
49

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

In [5]:
my_sum = lambda x, y: x + y
In [6]:
my_sum(0, 7)
Out[6]:
7
In [7]:
my_sum(6, 7)
Out[7]:
13

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

In [8]:
L = [0, 2, 7, 5, 4, 3, 2]
L.index(2) # только первая 2
Out[8]:
1

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

In [47]:
list(filter(lambda x: x > 3, L))  # элементы списка L больше 3
Out[47]:
[7, 5, 4]
In [48]:
list(filter(lambda x: x % 2 == 0, L))  # четные элементы списка L
Out[48]:
[0, 2, 4, 2]

Условия можно совмещать:

In [49]:
list(filter(lambda x: (x > 3) & (x < 7), L))   # 3 < x < 7
Out[49]:
[5, 4]

Обратите внимание: перед filter() всегда добавляется list(). Это нужно для того, чтобы увидеть результаты явно и получить их в виде списка. Иначе мы просто получим «закрытый» объект типа filter() ( вспомните историю про zip()).

In [9]:
filter(lambda x: x > 3, L)
Out[9]:
<filter at 0x10dde1eb8>

Теперь попробуем совместить lambda-функцию и функцию map(), которая позволяет получать новый список на основе старого, преобразовывая его элементы:

In [50]:
list(map(lambda x: x ** 2, L))  # квадраты элементов списка L
Out[50]:
[0, 4, 49, 25, 16, 9, 4]

Исключения

Что происходит, когда мы просим Python выполнить недопустимую операцию? Например, возвести строку в квадрат, найти натуральный логарифм от отрицательного числа, вывести на экран элемент списка с несуществующим индексом... Мы получаем сообщение об ошибке (на самом деле, исключение):

In [12]:
x = input("Enter a number: ")  # ввели не число
n = int(x)  # превратить tuy в целое число невозможно
Enter a number: tuy
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-12-131599087ac0> in <module>()
      1 x = input("Enter a number: ")
----> 2 n = int(x)

ValueError: invalid literal for int() with base 10: 'tuy'

В примере выше мы получили исключение типа ValueError, которое свидетельствует о том, что Python не может выполнить запрашиваемое действие с указанным значением. Несмотря на то, что на экран выводится краткое пояснение ошибки, полученное сообщение очень специфическое и вряд ли поможет пользователю. Как быть? Написать программу, которая будет «ловить» ошибки (исключения) и в случае, если ошибка допущена, выводить пользователю более доступное сообщение о том, что именно пошло не так.

Структура конструкций с исключениями напоминает устройство конструкций if-else: есть «развилка», если действие допустимо, оно выполняется (движемся по первой ветке «развилки»), если нет – не выполняется, но ошибки при этом не высвечивается. Конструкция для «ловли» исключений – конструкция try-except. В ветке с try указывается набор действий, которые Python в любом случае пытается выполнить, в except – набор действий, которые должны быть исполнены, если реализовать операции в try не получилось. В отличие от конструкции if-else, где у else нет никакого условия, except не может существовать сам по себе. При нём всегда есть тип исключения, на который программа должна реагировать. В нашем случае это ValueError:

In [13]:
x = input("Enter a number: ")
try:
    n = int(x)
    print("Ok")  # если все хорошо
except ValueError:
    print("Incorrect input. Not a number.")  # если пользователь ввел нечто, что привело к ValueError
Enter a number: tyu
Incorrect input. Not a number.

Иногда помимо основных действий добавляют оператор pass, который ничего не делает, а просто означает отсутсиве действия. Если в код выше мы допишем pass, ничего не изменится:

In [ ]:
x = input("Enter a number: ")
try:
    n = int(x)
    print("Ok")
except ValueError:
    print("Incorrect input. Not a number.")
    pass

Может возникнуть вопрос, а зачем тогда он вообще нужен? Например, затем, чтобы обозначить отсутствие действия и избежать добавления ненужных строк кода. Если мы хотим, чтобы в случае столкновения с ValueError Python НЕ ДЕЛАЛ НИЧЕГО (и не выводил никаких предупреждений на экран), оставив после выражения с except пустоту, мы получим ошибку синтаксиса. А если просто допишем pass, то всё будет, как надо:

In [14]:
x = input("Enter a number: ")
Enter a number: егн
In [15]:
try:
    n = int(x)
    print("Ok")
except ValueError:
    pass # молча

В конструкцию try-except можно включать более одного except, и эти исключения могут быть разными. Рассмотрим пример: пользователь вводит число x, а ему возвращается число 1/x (обратное). Какие проблемы могут возникнуть? Во-первых, пользователь может ввести не число (как в примере выше). Во-вторых, пользователь может ввести 0, а делить на 0, как известно, нельзя.

In [16]:
1/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-16-9e1622b385b6> in <module>()
----> 1 1/0

ZeroDivisionError: division by zero

Получили исключение типа ZeroDivisionError. Теперь мы можем расширить нашу конструкцию и по-разному реагировать на разные типы ошибок:

In [70]:
x = input("Enter a number: ")
try:
    n = int(x)
    res = 1/n
    print("Ok")
    print(res)
except ValueError:
    print("Not a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")
Enter a number: uio
Not a valid number.

Конструкции с assert для отладки кода

Допустим, нам нужно написать функцию, которая склеивает список строк в одну строку с помощью дефиса. Мы знаем, что, например, результат применения функции к списку ['a', 'b'] должен быть 'a-b'. Напишем функцию my_merge():

In [1]:
def my_merge(L):
    return "-".join(L)

Теперь проверим, что она работает так, как нужно: из списка ['a', 'b'] делает строку 'a-b'. Сравним возращаемое функцией значение и строку 'a-b', и если результат не совпадает с этой строкой, будем выводить сообщение "Something went wrong".

In [2]:
assert my_merge(["a", "b"]) == "a-b", "Something went wrong"

В нашем случае все работает корректно, и сообщения об ошибке мы не увидели. Что было бы, если бы мы забыли нашу задачу и написали бы функцию неправильно, указав в .join() символ '+' в качестве разделителя?

In [3]:
def my_merge(L):
    return "+".join(L)
In [4]:
assert my_merge(["a", "b"]) == "a-b", "Something went wrong"
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-4-1228fe994359> in <module>()
----> 1 assert my_merge(["a", "b"]) == "a-b", "Something went wrong"

AssertionError: Something went wrong

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