Программирование для журналистики данных

Магистерская программа «Журналистика данных», факультет коммуникаций, медиа и дизайна, НИУ ВШЭ, 2016-17 учебный год.

  • Автор курса: Илья Щуров.
  • Страница курса
  • Этот ноутбук доступен по лицензии CC BY SA 4.0 Unported. Вы можете использовать его в любых целях при условии указания имени автора, аффилиации и ссылки на страницу курса. Все фрагменты кода также распространяются по лицензии MIT.

Занятие 3

Исключения

Рассмотрим простую задачу: мы хотим ввести с клавиатуры вещественное число, прибавить к нему 1 и вывести результат.

In [1]:
x = float(input("Enter some number: "))
print(x + 1)
12.34
13.34

Если пользователь действительно введёт число, то получит правильный ответ. А если введёт не число, а что-то другое?

In [2]:
x = float(input())
print(x + 1)
asdf
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-2-fc09e59e6e6f> in <module>()
----> 1 x = float(input())
      2 print(x + 1)

ValueError: could not convert string to float: 'asdf'

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

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

s = float(input())
if not is_float_number(s):
    print("Sorry, this is not a number")
else:
    x = float(s)
    print(x + 1)

Здесь функция is_float_number(s) должна проверять, записано ли в строку s вещественное число: если да, то она возвращает True, а если нет, то `False.

Проблема состоит в том, что реализовать функцию is_float_number() не так-то легко. Существует очень много способов представлять число с плавающей точкой в виде строки. Например, 12, 12.34, +12.34, 1e2, +1e-2 — это всё корректные способы записать число с плавающей точкой. И мы, возможно, хотим дать пользователю возможность использовать их все.

Вместо того, чтобы реализовывать функцию is_float_number(), можно действовать по-другому. Можно попробовать преобразовать строку в число, а если не получится, то «поймать» ошибку и корректно её обработать. Для этого нужно использовать конструкцию try-except:

In [8]:
try:
    x = float(input("Enter some number: "))
    print("The following number is", x + 1)
except ValueError:
    print("Sorry, this is not a number")
print("Bye")
Enter some number: Hello
Sorry, this is not a number
Bye

Блок строчек после ключевого слова try выполняется в специальном режиме: все ошибки, которые возникнут в ходе выполнения этих команд, могут быть перехвачены и обработаны. Это делается в блоке except. Здесь указано, какого типа ошибки (они назыаются исключениями) мы перехватываем: в данном случае это ValueError — именно такого типа ошибку мы получаем, когда пытаемся сконвертировать строку, не похожую на вещественное число, в вещественное число. Это название указывается в тексте ошибки, когда она выдаётся пользователю:

ValueError: could not convert string to float: 'asdf'

Итак, когда в блоке try на строчке x = float(input("Enter some number: ")) возникает ошибка ValueError, выполнение кода в блоке try прекращается (до второй строки этого блока мы не дойдём) и мы оказываемся на строке except. Дальше мы проверяем, что возникла именно та ошибка, которую мы умеем обрабатывать, и выполняем строчку в блоке except — в данном случае выдаём сообщение об ошибке. Дальше блок except заканчивается и мы переходим к следующей строке — print("Bye") — и выполняем её как ни в чём не бывало. Если бы мы не перехватили исключение, то выполнение программы прекратилось бы сразу и до этой строчки мы не дошли бы.

Задача 1

С помощью исключений написать такой код. Пользователь вводит вещественное число x и в ответ получает обратное число 1/x. Если введен ноль, пользователь получает сообщение об ошибке I can't divide by zero!. Если введена строка, пользователь получает сообщение об ошибке This is not a number.

Обработку случая с делением на ноль можно было бы сделать с помощью if, но мы хотим также использовать исключения. Для начала выясним, как называется соответствующая ошибка.

In [10]:
1/0
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-10-05c9758a9c21> in <module>()
----> 1 1/0

ZeroDivisionError: division by zero

Видно, что она называется ZeroDivisionError. Теперь можно написать код.

In [11]:
try:
    x = float(input("Enter some number: "))
    print("The reciprocal is:", 1/x)
except ValueError:
    print("This is not a number.")
except ZeroDivisionError:
    print("I can't divide by zero!")
print("That's all")
Enter some number: 12
The reciprocal is: 0.08333333333333333
That's all

У одного и того же блока try может быть несколько блоков except. Они проверяются по порядку и выполняется тот, который соответствует реально случившейся ошибке. Выполняется ровно один из блоков except.

Задача 2

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

Для начала создадим какой-нибудь файл.

In [13]:
content = """12.2
lalala
3
5.6

haha 23.3
8

"""
f = open("testfile.txt", "w")
print(content, file=f)
f.close()

Проверим, что записалось.

In [15]:
f = open("testfile.txt")
for s in f:
    print(s.rstrip())
f.close()
12.2
lalala
3
5.6

haha 23.3
8


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

In [17]:
while True:
    filename = input("Enter file name: ")
    try:
        f = open(filename)
        break
    except FileNotFoundError:
        print("No such file. Try again.")
Enter file name: asd
No such file. Try again.
Enter file name: testfile.txt

Ошибка, которая возникает, когда мы пытаемся открыть файл, называется FileNotFoundError. (Чтобы узнать это, можно было попытаться открыть какой-нибудь несуществующий файл и посмотреть, какими словами на нас будет ругаться Python.) Если строчка f = open(filename) сработает, то мы попадём на следующую строчку break и выйдем из цикла. Если не сработает, то уйдём в except и потом вернёмся в начало цикла.

Теперь, когда файл открыт, можно посчитать сумму числе в нём.

In [18]:
summa = 0
for line in f:
    try:
        number = float(line)
        summa += number
    except ValueError:
        print("String without number skipped: " + line.rstrip())
f.close()
print(summa)
String without number skipped: lalala
String without number skipped: 
String without number skipped: haha 23.3
String without number skipped: 
String without number skipped: 
28.799999999999997

finally

Есть ещё одно применение обработки исключений. Иногда бывает, что нам нужно совершить какое-то действие «во что бы то ни стало». Например, если файл был открыт с помощью функции open(), то его хорошо бы закрыть. Если в процессе обработки файла произошла какая-нибудь ошибка, мы можем не добраться до строчки, в которой файл закрывается. Рассмотрим, например, такой код. Он должен открыть файл, запросить у ползователя число, и записать в файл число на 1 больше введенного.

f = open("filename.txt", "w")
    print("Let us begin!", file=f)
    number = int(input("Enter some number: "))
    print(number + 1, file=f)
    f.close()

Если пользователь ввёл строку, не являющуюся числом, то программа не дойдёт до строчки f.close() и файл останется не закрытым. Это можно привести к проблемам: например, строка Les ut begin! скорее всего даже не запишется в файл, несмотря на то, что первый print будет выполнен. (Это связано с некоторыми особенностями работы операционных систем, которых мы сейчас не будем касаться.) Конечно, мы могли бы обработать исключение, которое может возникнуть в фрагменте кода между открытием и закрытием файла, но зачастую мы не можем обработать все возможные исключения: обязательно произойдёт что-то непредвиденное.

Чтобы решить эту проблему, придумали инструкцию final.

In [20]:
f = open("filename.txt", "w")
try:
    print("Let us begin!", file=f)
    number = int(input("Enter some number: "))
    print(number + 1, file=f)
    print("Okay, I did it!")
finally:
    print("Something went wrong. Let us clean up.")
    f.close()
    print("File is closed.")
Enter some number: asdfasf
Something went wrong. Let us clean up.
File is closed.
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-20-b00cd9d8d7a4> in <module>()
      2 try:
      3     print("Let us begin!", file=f)
----> 4     number = int(input("Enter some number: "))
      5     print(number + 1, file=f)
      6     print("Okay, I did it!")

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

Как видим, в этом случае ошибка обработки пользовательского ввода, хоть и не была «поймана» с помощью except, не помешала нам закрыть файл.

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

In [23]:
# f = open("filename.txt)
with open("filename.txt") as f:
    for line in f:
        print(line.rstrip())
    print("Is file closed?", f.closed)
print("Out of with")
print("Is file closed?", f.closed)
Let us begin!
Is file closed? False
Out of with
Is file closed? True

Объектно-ориентированное программирование

Простейший пример класса.

In [11]:
class Student(object):
    def __init__(self, name):
        self.name = name
        self.grades = []
    def get_average_grade(self):
        return sum(self.grades)/len(self.grades)
    def add_grade(self, grade):
        self.grades.append(grade)
In [23]:
Ivan = Student("Ivan")
Ivan.name
Out[23]:
'Ivan'
In [24]:
Ivan.add_grade(3)
In [25]:
Ivan.grades
Out[25]:
[3]
In [26]:
Ivan.add_grade(10)
In [27]:
Ivan.get_average_grade()
Out[27]:
6.5

Задача

  1. Модифицировать класс Student, сделав так, чтобы у студента были отдельные имя и фамилия.
  2. Добавить метод has_tails(), проверяющий, есть ли у студента академические задолженности (то есть оценки ниже 3).
  3. (*) Добавить атрибут partner, указывающий на супруга студента. Если у студента нет супруга, инициализировать в None. Добавить метод set_partner, принимающий на вход другой объект класса Student и устанавшивающий своё свойство partner в этот объект и одновременно свойство partner этого объекта в себя.
In [2]:
class Student(object):
    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname
        self.grades = []
        self.partner = None
    def get_average_grade(self):
        return sum(self.grades)/len(self.grades)
    def add_grade(self, grade):
        self.grades.append(grade)
    def has_tails(self):
        return min(self.grades) < 3
    def set_partner(self, partner):
        if self.partner is not None:
            self.remove_partner()
        if partner.partner is not None:
            partner.remove_partner()
        self.partner = partner
        partner.partner = self
    def remove_partner(self):
        my_ex = self.partner
        self.partner = None
        my_ex.partner = None
In [3]:
Mary = Student("Mary", "Smith")
Dan = Student("Dan", "McClaud")
In [4]:
print(Mary.partner)
None
In [5]:
Mary.set_partner(Dan)
In [6]:
Mary.partner.firstname
Out[6]:
'Dan'
In [7]:
Dan.partner
Out[7]:
<__main__.Student at 0x10d1bf1d0>
In [8]:
Mary.partner.partner.partner.partner.firstname
Out[8]:
'Mary'
In [9]:
Ivan = Student("Ivan", "Ivanov")
In [12]:
Ivan.add_grade(2)
In [13]:
if Ivan.has_tails():
    print("Let us call Ivan's parents")
Let us call Ivan's parents
In [14]:
Mary = Student("Mary Ann")
Christian = Student("Christian")
yyy = Student("Somebody")
In [17]:
Mary.add_grade(10)
In [18]:
Mary.get_average_grade()
Out[18]:
6.0
In [20]:
Mary.add_grade(5)
Mary.add_grade(3)
print(Mary.get_average_grade())
4.857142857142857
In [21]:
yyy.name
Out[21]:
'Somebody'