Kodomo

Пользователь

7

Об ошибках

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

   1 >>> file = open("Молоко — Википедия.html")
   2 >>> text = file.read()
   3 Traceback (most recent call last):
   4   File "<pyshell#1>", line 1, in <module>
   5     text = file.read()
   6   File "C:\Python34\lib\encodings\cp1251.py", line 23, in decode
   7     return codecs.charmap_decode(input,self.errors,decoding_table)[0]
   8 UnicodeDecodeError: 'charmap' codec can't decode byte 0x98 in position 34254: character maps to <undefined>

Здесь главное посмотреть на слово UnicodeDecodeError. Два полезных кусочка знаний. Во-первых, у питона в памяти любые тексты хранятся в формате, который питон называет словом unicode (спорный вопрос, насколько это корректное название, нам главное понимать, что так это представление текстов называет питон). Во-вторых, питон называет переведение текста из любой кодировки во внутренний формат словом decode, а из внутреннего представление в любую кодировку словом encode. Итого, UnicodeDecodeError обозначает ошибку перевода текста из какой-то кодировки во внутреннее представление. А дальше питон вдаётся в дебри того, какой это был байт по номеру, и какой у него был код, и почему он не смог его расшифровать – пожалуй, такая подробность может быть полезна только людям, которые разрабатывают очередной перекодировщик.

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

   1 >>> print text[:100]
   2 SyntaxError: Missing parentheses in call to 'print'

Надеюсь, такая ошибка только у меня возникает. Разумеется, если мы хотим вызвать функцию, то после имени функции нужно ставить скобки. parentheses, собственно, и переводится как скобки. (Притом только круглые. Фигурные скобки называются braces, а квадратные brackets).

   1 >>> open = ("hello")
   2 >>> open()
   3 Traceback (most recent call last):
   4   File "<pyshell#9>", line 1, in <module>
   5     open()
   6 TypeError: 'str' object is not callable

Сначала читаем сообщение об ошибке: TypeError не очень информативно – очень много чего в питоне называется TypeError. Читаем дальше: строковый объект не вызываемый. Вообще, слово callable в питоне вообще используется почти как существительное, и значит он такую штуку, после которой можно поставить скобочки, чтобы её вызвать. То есть попросту он говорит, что строка – не функция! (А мужики-то не знали)

Из этого мы делаем вывод, что он пытался строку вызвать как функцию. А смотрим мы на строку open(). Значит нужно пристальнее посмотреть, что у нас известно под именем open, и мы обнаруживаем, что вместо того, чтобы вызвать open мы присвоили в переменную open строку.

Перезапустим шелл, чтобы вернуть себе родной open.

   1 >>> open['Молоко — Википедия.html']
   2 Traceback (most recent call last):
   3   File "<pyshell#10>", line 1, in <module>
   4     open['Молоко — Википедия.html']
   5 TypeError: 'builtin_function_or_method' object is not subscriptable

TypeError неинтересно, встроенная_функция_или_метод не subscriptable. Слово subscript обозначает нижний индекс, как в a1, a2, ... an. В питоне предполагается, что запись a[n] является аллегорией к такой математической записи. (Или наоборот. В любом случае слово "аллегория" – клёвое, особенно в контексте языков программирования).

То есть мы попытались взять элемент по индексу или по ключу из чего-то, что не является ни списком, ни словарём.

   1 >>> file = open['Молоко — Википедия.html']
   2 Traceback (most recent call last):
   3   File "<pyshell#11>", line 1, in <module>
   4     file = open['Молоко — Википедия.html']
   5 TypeError: 'builtin_function_or_method' object is not subscriptable

И даже если результат класть в переменную, мы всё равно делаем ту же ошибку.

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

   1 >>> file = open('Молоко — '"Википедия.html')
   2             
   3 SyntaxError: EOL while scanning string literal

SyntaxError – синтаксическая ошибка. То есть питон ещё даже исполнять ничего не успел начать, он пока что вообще не может разобрать, что здесь написано. Ошибка в закорючках.

EOL расшифровывается как end of line. (А EOF – как end of file).

string literal обозначает задание строки в кавычках. Я на русский несколько некорректно перевожу это словосочетание как строковая константа.

Итого имеем телеграмму от питона: "ошибка в закорючках: наткнулся на конец строки, пока разбирал строку в кавычках" ТЧК2

Смотрим внимательно. Первая одинарная кавычка в строке питоном воспринимается не как апостроф, а как знак, что строка заканчивается. Следом за ней есть двойная кавычка – питон считает, что оттуда начинается следующая строка, и вот она-то и не заканчивается.

Здесь нам помогает странноватое свойство питона:

   1 >>> file = open('Молоко — ''Википедия.html')

На удивление, такая команда работает и открывает файл "Молоко — Википедия.html". См. ниже про implicit string literal concatenation.

   1 >>> file = open('Молоко — xВикипедия.html')
   2 Traceback (most recent call last):
   3   File "<pyshell#16>", line 1, in <module>
   4     file = open('Молоко — xВикипедия.html')
   5 FileNotFoundError: [Errno 2] No such file or directory: 'Молоко — xВикипедия.html'

Здесь всё просто и в типе ошикби "файл не найден", и в комментариях "нет такого файла или папки", да питона нам ещё и имя говорит.

   1 >>> file = open('Молоко — "Википедия.html')
   2 Traceback (most recent call last):
   3   File "<pyshell#15>", line 1, in <module>
   4     file = open('Молоко — "Википедия.html')
   5 OSError: [Errno 22] Invalid argument: 'Молоко — "Википедия.html'

Суть ошибки та же, но название совсем другое: OSError, то есть ошибка операционной системы (OS). Да ещё и некорректный аргумент.

Операционная система – в нашем случае Windows – решила, что файлов с кавычкой в имене не может быть, и по той простой причине, что таких файлов быть не может никогда, поэтому она даже не проверяла, есть ли такой файл, а сразу выругалась благим матом.

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

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

   1 >>> file.open('Молоко — Википедия.html')
   2 Traceback (most recent call last):
   3   File "<pyshell#17>", line 1, in <module>
   4     file.open('Молоко — Википедия.html')
   5 AttributeError: '_io.TextIOWrapper' object has no attribute 'open'

Читаем внимательно. AttributeError: ошибка атрибута? Вспоминаем: атрибут – это переменная внутри объекта. Если у нас есть объект y, то y.x – это атрибут x у объекта y.

Ошибка атрибута почти всегда говорит о том, что мы хотим взять из объекта что-то, чего в нём нет. (Изредка она говорит, что мы в него хотим положить то, что в него класть нельзя).

Ок, читаем дальше. _io.TextIOWrapper – приходится выучить, что таким ужасным образом питон называет файлы. (А во втором питоне он ещё называл файлы словом file, было такое время, и фонтаны были голубые).

Итак, у объекта файла нет атрибута open.

Так и не должно быть! Файлы же открывают так: file = open(...).

   1 >>> ','.join('hello')
   2 'h,e,l,l,o'

Питон не выругался, но сделал нечто, что может нас удивить. Мы бы ожидали, что раз мы join дали один аргумент, то он его же нам и вернёт?

Ан-нет, мы ему дали не список из одного элемента, а одну строку. А join только и надо, чтобы по тому, что мы ему дали, можно было пройти циклом. Если идти циклом по строке, то на каждом шагу мы будем брать очередной элемент. Вот join и воспринял нашу строку как список её букв, и склеил их через запятую.

   1 >>> ','.join([1,2,3])
   2 Traceback (most recent call last):
   3   File "<pyshell#19>", line 1, in <module>
   4     ','.join([1,2,3])
   5 TypeError: sequence item 0: expected str instance, int found
   6 >>> ','.join(['1','2',3])
   7 Traceback (most recent call last):
   8   File "<pyshell#20>", line 1, in <module>
   9     ','.join(['1','2',3])
  10 TypeError: sequence item 2: expected str instance, int found

Снова TypeError.

sequence item 0 – то есть речь идёт про элемент номер 0 из последовательности (которую мы дали на вход join).

str instance обозначает любую строку. Слово instance обозначает представителя класса или типа (в третьем питоне это почти синонимы). Например, 'hello' – представитель типа str, а 42 – представитель типа int. В питоне есть даже такая проверка: isinstance(42, int) – правда ли, что 42 – представитель типа int?

Итого питон говорит нам: в элементе последовательности с номером 0 мы ожидали бы увидеть строку, а нашли число.

Действительно, у join есть такое неприятное свойство, что ему подавай на вход обязательно список только строк. Сам преобразовать что угодно другое в строки он не умеет. (Не понимаю, почему. В большинстве случаев я в этом случае добавляю к вызову необходимый минимум кода, чтобы применить к каждому элементу списка функцию str).

На всякий случай вспомним, что является прямым назначением функции join:

   1 >>> ','.join(['1','2','3'])
   2 '1,2,3'
   3 >>> '+'.join(['1','2','3'])
   4 '1+2+3'

Дальше.

   1 >>> '+'.join(['1','2','3'], 2)
   2 Traceback (most recent call last):
   3   File "<pyshell#23>", line 1, in <module>
   4     '+'.join(['1','2','3'], 2)
   5 TypeError: join() takes exactly one argument (2 given)
   6 >>> '+'.join(['1','2','3'], ['3'])
   7 Traceback (most recent call last):
   8   File "<pyshell#24>", line 1, in <module>
   9     '+'.join(['1','2','3'], ['3'])
  10 TypeError: join() takes exactly one argument (2 given)
  11 >>> x = ['1','2']
  12 >>> y = ['3']
  13 >>> '+'.join(x, y)
  14 Traceback (most recent call last):
  15   File "<pyshell#29>", line 1, in <module>
  16     '+'.join(x, y)
  17 TypeError: join() takes exactly one argument (2 given)
  18 >>> '+'.join('1', '2')
  19 Traceback (most recent call last):
  20   File "<pyshell#30>", line 1, in <module>
  21     '+'.join('1', '2')
  22 TypeError: join() takes exactly one argument (2 given)

Снова TypeError. Дословно: функция join принимает РОВНО один аргумент, а мы ему даём два.

И упорно пытаемся давать, надо заметить. А он с не меньшим упорством не понимает, чего мы от него хотим. Зачем ему лишние аргументы-то?

   1 >>> x = ['1','2]
   2      
   3 SyntaxError: EOL while scanning string literal

Это мы уже видели: забыли кавычку закрыть.

   1 >>> '+'.join(['1','2','3'])
   2 '1+2+3'
   3 >>> sum([1,2,3])
   4 6

Есть в питоне встроенная функция sum, которая умеет складывать числа. (Вообще, в питоне немного встроенных функций. Чуть больше полусотни, а полезных всего штук 20).

   1 >>> sum(['1', '2', '3'])
   2 Traceback (most recent call last):
   3   File "<pyshell#33>", line 1, in <module>
   4     sum(['1', '2', '3'])
   5 TypeError: unsupported operand type(s) for +: 'int' and 'str'

Ситуация простая, а сообщение об ошибке неожиданное.

Снова TypeError. неподдерживаемые типы операндов для +: int и str.

Операнд – это аргумент операции.

То есть у нас почему-то полуается, что мы складываем число со строкой? Откуда число?

   1 >>> '1'+'2'
   2 '12'

Ведь здесь он не ругается!

   1 >>> help(sum)
   2 Help on built-in function sum in module builtins:
   3 
   4 sum(...)
   5     sum(iterable[, start]) -> value
   6     
   7     Return the sum of an iterable of numbers (NOT strings) plus the value
   8     of parameter 'start' (which defaults to 0).  When the iterable is
   9     empty, return start.

Оказывается, у функции sum есть необязательный второй аргумент: значение, которое нужно прибавить к сумме. И если его не указать, то оно по умолчанию (default) будет 0.

Вот и получается, что первое действие, которое пытался выполнить питон, было 0 + '1'. Отсюда и такое сообщение об ошибке.

Может, мы сможем это победить, если в start положим строку?

   1 >>> sum('123', '4')
   2 Traceback (most recent call last):
   3   File "<pyshell#35>", line 1, in <module>
   4     sum('123', '4')
   5 TypeError: sum() can't sum strings [use ''.join(seq) instead]
   6 >>> sum(['1','2','3'], '4')
   7 Traceback (most recent call last):
   8   File "<pyshell#36>", line 1, in <module>
   9     sum(['1','2','3'], '4')
  10 TypeError: sum() can't sum strings [use ''.join(seq) instead]

Снова TypeError. sum не может складывать строки, и вообще, шли бы вы все пользоваться joinом – как бы говорит нам питон.

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

   1 >>> ''.join(['1','2','3'])
   2 '123'

Дальше.

   1 >>> sum(int(''.join(['1','2','3'])))
   2 Traceback (most recent call last):
   3   File "<pyshell#38>", line 1, in <module>
   4     sum(int(''.join(['1','2','3'])))
   5 TypeError: 'int' object is not iterable

Снова TypeError. Число не iterable.

Словом iterable в питоне называется всё, по чему можно пройтись циклом: строки, списки, словари, итераторы3.

Число не список? Спасибо, кэп!

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

   1 >>> x = ['1','2','3']
   2 >>> y = ''.join(x)
   3 >>> z = int(y)
   4 >>> sum(z)
   5 Traceback (most recent call last):
   6   File "<ipython-input-13-448aa62d5214>", line 1, in <module>
   7     sum(z)
   8 TypeError: 'int' object is not iterable

Ага! Всё просто: в z у нас число, а sum хочет на вход список.

   1 >>> sum(4)
   2 Traceback (most recent call last):
   3   File "<pyshell#39>", line 1, in <module>
   4     sum(4)
   5 TypeError: 'int' object is not iterable
   6 >>> list(4)
   7 Traceback (most recent call last):
   8   File "<pyshell#40>", line 1, in <module>
   9     list(4)
  10 TypeError: 'int' object is not iterable

Та же история.

   1 >>> sum([int(''.join(['1','2','3']))])
   2 123

Мы хотели 6!

Но нет, join сделал строку '123', а int из неё сделал число 123. Мы из этого числа сделали список [123], а сумма элементов этого списка равна 123.

Снова помогло бы деление строки на несколько с помощью переменных.

   1 >>> sum([int(['1','2','3'])])
   2 Traceback (most recent call last):
   3   File "<pyshell#42>", line 1, in <module>
   4     sum([int(['1','2','3'])])
   5 TypeError: int() argument must be a string or a number, not 'list'

Снова TypeError (неужто их бесконечно много?). Аргумент int() должен быть строкой или числом, но не списком.

Вот тут нам просто самым прямым текстом объяснили, что мы делаем не так. (Правда, заодно тут как раз и мы сами не можем понять, чего же мы хотели).

Словари:

   1 >>> x = {1:2, 3:4}
   2 >>> x['1']
   3 Traceback (most recent call last):
   4   File "<pyshell#44>", line 1, in <module>
   5     x['1']
   6 KeyError: '1'

У словаря штука в квадратных скобках называется и не subscript, и не index, а key. (Вот ведь любители плодить термины!)

Лаконичненько питон сообщил нам, что ключа '1' нет в словаре.

Присматриваемся пристально. Ведь и вправду нет, есть ключи 1 и 3, но не '1'! Такую ошибку иногда можно очень долго и очень мучительно вылавливать, однако...

   1 >>> import Tkinter
   2 Traceback (most recent call last):
   3   File "<pyshell#45>", line 1, in <module>
   4     import Tkinter
   5 ImportError: No module named 'Tkinter'

Тут всё очевидно: нет такого модуля. Для питона регистр букв играет большую роль. Tkinter и tkinter – разные штуки!

   1 >>> import tkinter

А так всё работает.

   1 >>> x = 'hello'
   2 >>> x[2] = 'x'
   3 Traceback (most recent call last):
   4   File "<pyshell#48>", line 1, in <module>
   5     x[2] = 'x'
   6 TypeError: 'str' object does not support item assignment

Строка не поддерживает item assignment, то есть присвоения части по индексу.

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

   1 >>> y = x.replace('l', 'm')
   2 >>> x
   3 'hello'
   4 >>> y
   5 'hemmo'

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

   1 >>> x = x.replace('m', 'n')
   2 # эквивалент:
   3 >>> y = x.replace('l', 'm')
   4 >>> x = y
   5 >>> x
   6 'hemmo'

   1 >>> c
   2 Traceback (most recent call last):
   3   File "<pyshell#55>", line 1, in <module>
   4     c
   5 NameError: name 'c' is not defined

Тут думать не надо: нет такой переменной.

   1 >>> оpen('hello')
   2 Traceback (most recent call last):
   3   File "<pyshell#55>", line 1, in <module>
   4     оpen('hello')
   5 NameError: name 'оpen' is not defined
   6 >>> Open('hello')
   7 Traceback (most recent call last):
   8   File "<pyshell#55>", line 1, in <module>
   9     Open('hello')
  10 NameError: name 'Open' is not defined

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

   1 >>> l+2
   2 Traceback (most recent call last):
   3   File "<pyshell#56>", line 1, in <module>
   4     l+2
   5 NameError: name 'l' is not defined

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

Лично моё пожелание: чтобы имена переменных всегда были словами английского языка или словосочетаниями английского языка, записанными маленькими буквами через подчёркивание. ВСЕГДА. (Ну, кроме упражнений с питоном, когда мы пытаемся найти разыне эпичные способы питон сломать и прочих одноразовых экзерсисов, следов которых никто никогда не увидит.)

   1 >>> x.replace(1)
   2 Traceback (most recent call last):
   3   File "<pyshell#57>", line 1, in <module>
   4     x.replace(1)
   5 TypeError: replace() takes at least 2 arguments (1 given)

Тут всё понятно: мы дали функции недостаточно аргументы. replace нужно сказать, что на что мы меняем, а мы ей дали только какую-то непонятную единицу.

   1 >>> tkinter.Tk(1,2)
   2 Traceback (most recent call last):
   3   File "<pyshell#58>", line 1, in <module>
   4     tkinter.Tk(1,2)
   5   File "C:\Python34\lib\tkinter\__init__.py", line 1851, in __init__
   6     self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use)
   7 TypeError: must be str or None, not int

Тоже всё понятно: от нас хотят строку или число... Непонятно только, зачем функции Tk() аргументы – ну да это вопрос к документации...

   1 >>> re.sub(1)
   2 Traceback (most recent call last):
   3   File "<pyshell#60>", line 1, in <module>
   4     re.sub(1)
   5 TypeError: sub() missing 2 required positional arguments: 'repl' and 'string'

Не просто недостаточно аргументов, а недостаточно required positional arguments, да ещё питон и говорит нам, каких именно. Осталось только в хелпы подглядеть, что мы напутали.

positional argument – это аргументы, которые мы передаём просто через запятую. Есть ещё keyword argument – это аргументы, которые мы передаём по имени, как encoding в open('filename', encoding='utf-8').

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

   1 >>> 1,2+3
   2 (1, 5)
   3 >>> 1.2*2
   4 2.4

Должен вам доложить, что именно из-за ровно такой опечатки – замены точки на запятую – (только в совсем другом языке) был потерян один из спускаемых аппаратов на Марс (кажется, Фобос-1, если не путаю). Ошибочка ценой в какие-то безумные сотни миллионов долларов, однако.

Печально, что до сих пор почти ни в одном языке программирования не искорены случаи, где замена точки на запятую приводит к программе, которая продолжает работать, но делает это по-другому. Будьте бдительны!

   1 >>> for y in ['1', '2', '3'];
   2 SyntaxError: invalid syntax

Синтаксическая ошибка. Внимательно смотрим на все закорючки. Вот двоеточие на точку с запятой питон заменить не даст.

Если мы всё же хотим просуммировать числа, которые лежат в виде списка строк (по-русски это и не скажешь нормальн), то делаем мы это просто ручками:

   1 >>> x = 0
   2 >>> for y in ['1', '2', '3']:
   3         x += y # x = x + y
   4 
   5         
   6 Traceback (most recent call last):
   7   File "<pyshell#67>", line 2, in <module>
   8     x += y # x = x + y
   9 TypeError: unsupported operand type(s) for +=: 'int' and 'str'

Хм, где-то мы такую ошибку видели. 0 + '1'...

   1 >>> x = 0
   2 >>> for y in ['1', '2', '3']:
   3         x += int(y) # x = x + y
   4 >>> x
   5 6

Вот, теперь получилось.

   1 >>> file = open("Молоко — Википедия.html", encoding='utf-8')
   2 >>> file[:100]
   3 Traceback (most recent call last):
   4   File "<pyshell#74>", line 1, in <module>
   5     file[:100]
   6 TypeError: '_io.TextIOWrapper' object is not subscriptable

Все слова по отдельности уже знакомы, но что ж ему не нравится: файл нельзя использовать с квадратными собками? Но мы же его прочитали... Прочитали? Точно? Да нет же, мы его только открыли! Нужно добавить text = file.read(), и работать вообще с текстом.

   1 >>> file()
   2 Traceback (most recent call last):
   3   File "<pyshell#75>", line 1, in <module>
   4     file()
   5 TypeError: '_io.TextIOWrapper' object is not callable

Файл нельзя использовать в качестве функции. Ок, тут всё ясно.

   1 >>> re.findall(')', text)
   2 Traceback (most recent call last):
   3   File "<pyshell#78>", line 1, in <module>
   4     re.findall(')', text)
   5   File "C:\Python34\lib\re.py", line 206, in findall
   6     return _compile(pattern, flags).findall(string)
   7   File "C:\Python34\lib\re.py", line 288, in _compile
   8     p = sre_compile.compile(pattern, flags)
   9   File "C:\Python34\lib\sre_compile.py", line 465, in compile
  10     p = sre_parse.parse(p, flags)
  11   File "C:\Python34\lib\sre_parse.py", line 753, in parse
  12     raise error("unbalanced parenthesis")
  13 sre_constants.error: unbalanced parenthesis

sre_constants.error мало чего говорит. Если закопаться в детали реализации, то мы обнаружим, что модуль re использует для грязной работы модуль sre – собственно реализация регулярных выражений.

unbalanced = несбалансированная

parenthesis = скобка

Ага! В регулярных выражениях те скобки, которые для регулярных выражений имеют смысл, должны быть в балансе и гармонии открывающих и закрывающих скобок.

   1 >>>  1
   2  
   3 SyntaxError: unexpected indent

А вот это неожиданное.

indent = отступ, то есть число пробелов от начала строки.

unexpected = неожиданный.

Дело вот в чём. В питоне отступ играет существенную роль. Когда питон читает очередную команду, он должен понять, в какую составную команду она вложена – по отступу. И если у нас в теле if отступ был 4 пробела, снаружи от if отступ был 0 пробелов, а после тела if мы написали команду с отступом 2 пробела, то у питона большие проблемы, он не знает, что мы имели в виду, и, собственно, так нам об этом и скажет. А для пущего порядка и для защиты от ошибок он требует, чтобы и вообще всегда и везде строки на одном уровне вложенности имели одинаковый отступ.

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

Вот на это питон и ругается.

   1 >>> "" + file
   2 Traceback (most recent call last):
   3   File "<pyshell#85>", line 1, in <module>
   4     "" + file
   5 TypeError: Can't convert '_io.TextIOWrapper' object to str implicitly

Не можем сконвертировать файл в строку неявно (implicitly).

Действительно, всё та же ошибка, что в переменной file у нас не текст файла, а сам файл.

   1 >>> re.sub('a', 'b', file)
   2 Traceback (most recent call last):
   3   File "<pyshell#86>", line 1, in <module>
   4     re.sub('a', 'b', file)
   5   File "C:\Python34\lib\re.py", line 175, in sub
   6     return _compile(pattern, flags).sub(repl, string, count)
   7 TypeError: expected string or buffer
   8 >>> file
   9 <_io.TextIOWrapper name='Молоко — Википедия.html' mode='r' encoding='cp1251'>

Ошибка: ожидаю строку или буфер. (Буфером в питоне называется одна крайне экзотическая разновидность строк).

Всё то же: сам файл (а не его текст) мы не можем скормить в регулярные выражения.

>>> file.strip()
Traceback (most recent call last):
  File "<pyshell#88>", line 1, in <module>
    file.strip()
AttributeError: '_io.TextIOWrapper' object has no attribute 'strip'

У файлов нет атрибута strip. Потому, что strip есть только у строк. Чтобы из файла сделать строку, его нужно сначала прочитать.

   1 >>> outfile = open('smiles.txt', 'w', encoding='utf-8')
   2 >>> outfile.close()
   3 >>> re.sub('a', 'b', outfile.read())
   4 Traceback (most recent call last):
   5   File "<pyshell#89>", line 1, in <module>
   6     re.sub('a', 'b', outfile.read())
   7 ValueError: I/O operation on closed file.

ValueError – самое непонятное мне название типа ошибки: у вас ошибка с данными. (Хорошо хоть не GeneticCodeError)

I/O или IO очень много где обозначает ввод-вывод. Часто оно значит либо взаимодействие программы с окружающим миром (например, интерфейс), либо работу с файлом: чтение и/или записиь.

Итак: операция ввода-вывода на закрытом файле.

Действительно, после того, как мы закрыли файл, мы с ним ничего не можем делать. Разве что заново открыть.

   1 >>> x=[]
   2 >>> x[0]
   3 Traceback (most recent call last):
   4   File "<pyshell#91>", line 1, in <module>
   5     x[0]
   6 IndexError: list index out of range

IndexError. Ошибка... индексирования? Да, его. То есть взятия чего-то по индексу или записывания чего-то по индексу.

Индекс списка вне диапазона... Диапазаона чего? Допустимых значений.

В переводе на русский: мы задали номер элемента слишком большой или слишком маленький.

   1 >>> re.sub('a', 'b', file.write('ehllo'))
   2 Traceback (most recent call last):
   3   File "<pyshell#92>", line 1, in <module>
   4     re.sub('a', 'b', file.write('ehllo'))
   5 io.UnsupportedOperation: not writable
   6 >>> file
   7 <_io.TextIOWrapper name='Молоко — Википедия.html' mode='r' encoding='cp1251'>

io.UnsupportedOperation: неподдерживаемая операция ввода-вывода...

"not writable": то есть сюда нельзя писать.

Дело вот в чём. Когда мы открываем файл на чтение или на запись, мы питону говорим о своих намерениях. И во-первых, он сам готовится к такому режиму общения с файлом, а во-вторых, и нам от греха подальше запрещает делать то, на что мы вначале не намеревались.

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

   1 >>> file = open('Молоко — Википедия.html', encoding='cp1251')
   2 >>> text = file.read()
   3 Traceback (most recent call last):
   4   File "<pyshell#95>", line 1, in <module>
   5     text = file.read()
   6   File "C:\Python34\lib\encodings\cp1251.py", line 23, in decode
   7     return codecs.charmap_decode(input,self.errors,decoding_table)[0]
   8 UnicodeDecodeError: 'charmap' codec can't decode byte 0x98 in position 34254: character maps to <undefined>

Знакомое: ошибка кодировки. Нужно открывать файл с другой кодировкой.

Implicit literal concatenation

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

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

   1 key = (
   2     '8430912384012380481230401923409123094809509347340612'
   3     '3641209387423489017230874129037490812390471098234098'
   4     '1723980471298034709812350602368941230741892374980123'
   5     '070'
   6 )

Надо заметить, что:

   1 >>> a = 'a'
   2 >>> b = 'b'
   3 >>> a b
   4   File "<ipython-input-5-7557d2f3a6ad>", line 1
   5     a b
   6       ^
   7 SyntaxError: invalid syntax

Это правило работает только для строк в кавычках.

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

Регулярки: re.sub, re.split

Скачаем из википедии страничку про молоко.

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

Кунг-фу html состоит в том, чтобы выучить, какие бывают тэги, и как ими правильно пользоваться – но это сейчас за пределами наших ближайших интересов.

Нам главное, что кроме вот этих (вполне читаемых) дополнений html – текстовый файл.

Давайте попробуем из него все тэги выкинуть, и получить текст!

В этом нам поможет re.sub(регулярка, на-что-заменить-находку, текст)

   1 >>> file = open("Молоко — Википедия.html", encoding='utf-8')
   2 >>> text = file.read()
   3 >>> import re
   4 >>> print(text[:100])
   5 <!DOCTYPE html>
   6 <!-- saved from url=(0066)https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BB%D0%BE%D0%
   7 
   8 >>> smile = re.sub('[&][^;]*;', ':)', text)
   9 >>> len(smile)
  10 191437

Для начала избавились от смайликов.

Смотреть на такой большой текст в консоли не удобно, сохраним в файл.

Для этого функции open мы можем вторым аргументом сказать одно из:

После того, как мы открыли файл на запись, мы можем пользоваться у него методом write(). Он очень тупой и ест только строки. Нужно не забывать закрыват файл или ползоваться with – только в тот момент, когда файл закрывается, изменения отправляются на жёсткий диск, а до этого они болтаются в оперативной памяти.

   1 >>> outfile = open('smiles.txt', 'w', encoding='utf-8')
   2 >>> outfile.write(smile)
   3 191437
   4 >>> outfile.close()

Смотрим на текст. Всё ок.

   1 >>> file = open('Молоко — Википедия.html', encoding='utf-8')
   2 >>> text = file.read()
   3 >>> x = file.read()
   4 >>> x
   5 ''

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

Убить тэги просто: открывающая скобка, всё, кроме закрывающей скобки, закрывающая скобка – заменяем это на пустую строку, и voila!

   1 >>> clean = re.sub('<[^>]*>', '', text)
   2 >>> outfile = open('clean.txt', 'w', encoding='utf-8')
   3 >>> outfile.write(clean)
   4 100722
   5 >>> outfile.close()

Только есть сколько-то мусора непонятного. Этот мусор почти весь обитает либо в тэге <script...>...</script>, либо в тэге <style...>...</style>. Уберём их! (Только это нужно делать _до_ того, как мы убрали все тэги, иначе там уже никаких <script> не осталось).

   1 >>> noscript = re.sub('<(style|script)[^<]*</(style|script)>', '', text)
   2 >>> clean = re.sub('<[^>]*>', '', noscript)
   3 >>> with open('clean.txt', 'w', encoding='utf-8') as outfile:
   4         outfile.write(clean)
   5 
   6         
   7 63472

Ага, хорошо только много пустых строк.

   1 >>> cleaner = re.sub(r'\n\s*\n', '', clean)
   2 >>> with open('clean.txt', 'w', encoding='utf-8') as outfile:
   3         outfile.write(cleaner)
   4 
   5         
   6 62081

Что-то некоторые строки склеились. И вправду, если у нас между строками a и b была одна пустая строка, то это было a\n\nb, а мы его превратили в ab. Ошибочка, заменять два переноса строки нужно на один:

   1 >>> cleaner = re.sub(r'\n\s*\n', '\n', clean)
   2 >>> with open('clean.txt', 'w', encoding='utf-8') as outfile:
   3         outfile.write(cleaner)
   4 
   5         
   6 62265

Так-то лучше.

Из приятностей регулярных выражений. Мы можем говорить:

   1 >>> re.findall('a{2,5}', 'a')
   2 []
   3 >>> re.findall('a{2,5}', 'aaaaaaaaaaaasa')
   4 ['aaaaa', 'aaaaa', 'aa']
   5 >>> re.findall('a{2,}', 'aaaaaaaaaaaasa')
   6 ['aaaaaaaaaaaa']
   7 >>> re.findall('a{,3}', 'aaaaaaaaaaaaa')
   8 ['aaa', 'aaa', 'aaa', 'aaa', 'a', '']

Теперь попробуем разбить текст на предложения.

Для этого в питоне есть функция re.split()

   1 >>> cleaner = re.sub(r'&nbsp;',' ', clean)
   2 >>> cleaner = re.sub(r'\s+',' ', cleaner)
   3 >>> sentences = re.split('[.?!]', cleaner)
   4 >>> with open('sentences.txt', 'w', encoding='utf-8') as outfile:
   5         outfile.write('\n'.join(sentences))
   6 
   7         
   8 58011

Мы можем здесь добиваться более качественных результатов разными способами. Либо, как в прошлый раз, вернуться к паарадигме, что мы описываем не то, что делит предложение, а то, что может в предложение входить, и тогда пользуемся findall. Либо мы можем освоить ещё пару хитростей из регулярных выражений:

   1 re.split(r'(?<=[а-яa-z](?:[.?!]|[.]{3})\s+(?=[A-ZА-Я])', cleaner)

Запуск программ

Кусок теории. У каждой программы есть понятие папки, где она находится. Эта папка называется рабочей директорией или current working directory. Мы можем её узнавать (os.getcwd) или менять (os.chdir):

   1 >>> import os
   2 >>> os.getcwd()
   3 'C:\\Users\\dendik\\Desktop\\learn-python'
   4 >>> os.chdir('..')
   5 >>> os.getcwd()
   6 'C:\\Users\\dendik\\Desktop'

Кусок теории: абсолютные и относительные пути.

До каждого файла мы можем дотянуться по его абсолютному пути – начиная от C: (или D: и т.п.). Но это почти никогда не удобно (например, если вы пересылаете программу кому-то, то у него всё почти наверняка лежит в других папках).

Второй способ указать имя файла – относительный путь. Относительный путь – это та часть пути, которая, если её присоединить к рабочей директории, станет абсолютным путём.

   1 >>> open('clean.txt')
   2 Traceback (most recent call last):
   3   File "<pyshell#147>", line 1, in <module>
   4     open('clean.txt')
   5 FileNotFoundError: [Errno 2] No such file or directory: 'clean.txt'

Действительно, мы сейчас не в learn-python, а файл там.

   1 >>> open('learn-python\\clean.txt')
   2 <_io.TextIOWrapper name='learn-python\\clean.txt' mode='r' encoding='cp1251'>

Работает!

NB. 'C:\\Users\\dendik\\Desktop' + '\\' + 'learn-python\\clean.txt' == 'C:\\Users\\dendik\\Desktop\\learn-python\\clean.txt'

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

   1 >>> open('learn-python\\Молоко — Википедия_files\\90px-Milk_glass.jpg')
   2 <_io.TextIOWrapper name='learn-python\\Молоко — Википедия_files\\90px-Milk_glass.jpg' mode='r' encoding='cp1251'>

Это работает и с большим числом уровней вложенности.

   1 >>> open(r'learn-python\Молоко — Википедия_files\90px-Milk_glass.jpg')
   2 <_io.TextIOWrapper name='learn-python\\Молоко — Википедия_files\\90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
   3 >>> open(r'learn-python/Молоко — Википедия_files/90px-Milk_glass.jpg')
   4 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files/90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
   5 >>> open('learn-python/Молоко — Википедия_files/90px-Milk_glass.jpg')
   6 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files/90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
   7 >>> open('learn-python/Молоко — Википедия_files\90px-Milk_glass.jpg')
   8 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files\\90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
   9 >>> open(r'learn-python/Молоко — Википедия_files\90px-Milk_glass.jpg')
  10 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files\\90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
  11 >>> open(r'learn-python/Молоко — Википедия_files\/90px-Milk_glass.jpg')
  12 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files\\/90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
  13 >>> open(r'learn-python/Молоко — Википедия_files\//////90px-Milk_glass.jpg')
  14 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files\\//////90px-Milk_glass.jpg' mode='r' encoding='cp1251'>
  15 >>> open(r'learn-python/Молоко — Википедия_files\90px-Milk_glass.jpg')
  16 <_io.TextIOWrapper name='learn-python/Молоко — Википедия_files\\90px-Milk_glass.jpg' mode='r' encoding='cp1251'>

На самом деле, писать пути через '\\' – это самый идиотский способ. Во-первых, мы можем использовать r'' строки, чтобы не дублировать слэши. Во-вторых, и это самый правильный способ, мы можем использовать '/' вместо '\\' – windows признаёт оба способа. Что характерно, UNIX и MacOS признают только '/', так что если вы используете его, то ваша программа работает в наибольшем числе систем.

Есть ещё одно специальное имя поддриректории: .. обозначает "уровень выше".

Соответственно ../.. обозначает двумя уровнями выше. ../learn-python обозначает подняться на уровень выше, найти там learn-python и зайти в него.

   1 >>> os.chdir('learn-python')
   2 >>> os.getcwd()
   3 'C:\\Users\\dendik\\Desktop\\learn-python'

Скачаем программу mystem и распакуем mystem.exe там же, где лежат наши питонские программы. Так нам будет проще с ним работать (теперь должно быть понятно, почему).

   1 >>> from subprocess import call
   2 >>> call(['mystem', '-h'])
   3 0

Вызвать программу из питона очень просто.

Только вот mystem запускается, показывает хелпы и закрывается.

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

Мы можем попросить питон сохранить в файл содержимое, которое программа пишет в один из этих потоков.

Очень просто:

   1 >>> helps = open('helps.txt', 'w')
   2 >>> call(['mystem', '-h'], stdout=helps, stderr=helps)
   3 0
   4 >>> helps.close()

Теперь в файле helps.txt оказались наши хелпы.

Почитав их, расшифровываем, что:

Попробуем запустить:

   1 >>> lemmas = open('lemmas.txt', 'w')
   2 >>> call(['mystem', '-e', 'utf-8', 'senteces.txt', '-l'], stdout=lemmas)
   3 1

Когда call возвращает 0, это значит, что программа завершилась успешно. Если он возвращает что угодно, кроме 0, скорее всего, произошла какая-то ошибка.

И вправду, мы написали senteces вместо sentences!

   1 >>> call(['mystem', '-e', 'utf-8', 'sentences.txt', 'lemmas.txt', '-l', '-n'])
   2 0

Ура, работает! Но подробнее о том, что делает mystem, и кто такой морфологический разбор – на лингвистике.

  1. У меня тоже не бывает. Просто у меня из-за кучи опыта понятие "большая программа" чуть-чуть другое. (1)

  2. Даже на мой век уже не досталось ни отправить ни получить ни одной телеграммы. Но какая-то романтика вокруг этого слова всё равно витает. (2)

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