Kodomo

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

Форматирование строк. Функции

Напоминание

Для понимания материала ОБЯЗАТЕЛЬНО каждый пример пытаться исполнить, понять, почему он работает или не работает, и попробовать в нём что-то поменять, и посмотреть, что получится.

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

with

В питоне есть способ не забывать написать close для файлов – конструкция with. Она гарантирует, что файл будет открыт, пока питон исполняет тело этой конструкции, и закрыт в любом случае, когда мы тело этой конструкции покинем1:

   1 import codecs
   2 with codecs.open("hello.txt", "w", "utf-8") as file:
   3    file.write("Hello, world!")

эквивалентно:

   1 import codecs
   2 file = codecs.open("hello.txt", "w", "utf-8")
   3 file.write("Hello, world!")
   4 file.close()

(as имя_переменной в конце строки с with говорит то же самое, что и имя_переменной = в начале строки с open).

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

Разбор домашнего задания

Напишите программу, которая открывает файл words.txt (например, оставшийся с прошлого раза), посчитайте число цифр в нём
Эта задача почти идентична задаче, данной в прошлый раз про число символов, разница только в том, что перед тем, как очередной символ посчитать, нужно проверить, что он является цифрой:
   1 import codecs
   2 with codecs.open("words.txt", "r", "utf-8") as file:
   3     text = file.read()
   4 
   5 count = 0
   6 for char in text:
   7     if char.isdigit():
   8         count = count + 1
   9 
  10 print "Number of digits in words.txt:", count
Напишите программу, которая открывает файл words.txt (лучше для этой задачи положить в него фрагмент содержательного текста и потестировать программу на нескольких разных текстах) и выводит на экран все слова, написанные с заглавной буквы (но не слова, написанные всеми заглавными буквами) и не короче двух букв

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

   1 import codecs
   2 with codecs.open("words.txt", "r", "utf-8") as file:
   3     text = file.read()
   4 chunks = text.split()
   5 
   6 for chunk in chunks:
   7     token = chunk.strip(",.!?\"'-+")
   8     if len(token) >= 2 and token.istitle():
   9         print token
Напишите программу, которая открывает файл numbers.csv, в котором лежат числа, и выводит на экран сумму чисел в нём
Задача делится на две части. Во-первых, нужно прочитать CSV-файл, например, просто скопировать кусок кода из конспектов:
   1 import codecs
   2 
   3 separator = ";"
   4 
   5 with codecs.open("numbers.csv", "r", "utf-8") as file:
   6     rows = []
   7     for line in file:
   8         line = line.rstrip()
   9         cells = line.split(separator)
  10         rows.append(cells)

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

   1 sum = 0
   2 for row in rows:
   3     for cell in row:
   4         sum = sum + int(cell)
   5 
   6 print "Total sum:", sum
Мы можем ещё немножко усложнить эту программу, и добавить проверку, чтобы суммировать только числовые значения в таблице:
   1 sum = 0
   2 for row in rows:
   3     for cell in row:
   4         if cell.isdigit():
   5             sum = sum + int(cell)
   6 
   7 print "Total sum:", sum
Напишите программу, которая открывает файл words.txt, и создаёт файл word-counts.csv, в котором есть три колонки: слово, длина слова, сколько раз это слово встретилось в тексте

Единственная новая по существу идея – это как посчитать количество вхождений слова. На эту тему можно задуматься – но тут мы вспоминаем, что мы знаем метод count, которым мы можем считать число вхождений слова в текст.:

   1 import codecs
   2 
   3 separator = ";"
   4 
   5 with codecs.open("words.txt", "r", "utf-8") as file:
   6     text = file.read()
   7 chunks = text.split()
   8 
   9 with codecs.open("word-counts.csv", "w", "utf-8") as outfile:
  10     for chunk in chunks:
  11         token = chunk.strip(".,?!\"'-+")
  12         if token != "":
  13             parts = [token, str(len(token)), str(text.count(token))]
  14             line = separator.join(parts)
  15             outfile.write(line + "\n")
Если посмотреть пристально на выдачу этой программы, в ней оказываются существенные недочёты: во-первых, каждый токен повторяется столько раз, сколько он идёт в тексте, во-вторых, для коротких токенов значение завышено. Первый недочёт мы править не будем, так как этого не требовалось в задании (настоящие частотные словари делаются чуточку сложнее). Второй недочёт происходит вот от чего: мы ищем токен в тексте и выводим количество находок. Например, text.count('к') выдаст нам количество всех букв "к" в тексте, а не только количество токенов "к". Если же мы будем искать количество в списке chunks, то, например, для в этом предложении токен "то" у нас не найдётся (в chunks будут только строки "то" и то,). Значит, нам нужно отдельно сохранять список токенов:
   1 import codecs
   2 
   3 separator = ";"
   4 
   5 with codecs.open("words.txt", "r", "utf-8") as file:
   6     text = file.read()
   7 chunks = text.split()
   8 
   9 tokens = []
  10 for chunk in chunks:
  11     token = chunk.strip(".,?!\"'-+")
  12     if token != "":
  13         tokens.append(token)
  14 
  15 with codecs.open("word-counts.csv", "w", "utf-8") as outfile:
  16     for token in tokens:
  17         parts = [token, str(len(token)), str(tokens.count(token))]
  18         line = separator.join(parts)
  19         outfile.write(line + "\n")
Напишите программу, которая открывает файл in.csv, транспонирует таблицу в нём (т.е. меняет ролями столбцы и строки), и записывает результат в out.csv
Читаем таблицу как и раньше (NB. почему-то многие из вас увидели в условиях слово "числа" и задавали вопрос, а почему здесь удаётся обойтись без конвертации чисел в строки и обратно. Прочитайте условия внимательно ещё раз):
   1 import codecs
   2 
   3 separator = ";"
   4 
   5 with codecs.open("in.csv", "r", "utf-8") as file:
   6     rows = []
   7     for line in file:
   8         rows.append(line.rstrip().split(separator))
Дальше, если мы допускаем два предположения: о прямоугольности таблицы и о том, что она содержит больше одной строки, мы можем читать таблицу по строкам, и заполнять новую в это время по столбцам:
   1 outrows = []
   2 for cell_n in range(len(rows[0])):
   3     outrow = []
   4     for row in rows:
   5         outrow.append(row[cell_n])
   6     outrows.append(outrow)
Если мы не можем сделать таких предположений, то нам нужно добавить два усложнения: сначала посчитать длину самого длинного ряда, а потом при добавлении очередной ячейки в выходную таблицу проверять, не вышли ли мы за границы ряда:

   1 maxrow = 0
   2 for row in rows:
   3     if len(row) > maxrow):
   4         maxrow = len(row)
   5 
   6 outrows = []
   7 for cell_n in range(maxrows):
   8     outrow = []
   9     for row in rows:
  10         if len(row) > cell_n:
  11             outrow.append(row[cell_n])
  12         else:
  13             outrow.append("")
  14     outrows.append(outrow)
В любом случае, дальше нам осталось вывести получившуюся таблицу:
   1 with codecs.open("out.csv", "w", "utf-8") as file:
   2     for row in outrows:
   3         file.write(separator.join(row) + "\n")

Форматирование строк

Кроме склеивания строк с помощью + и превращения чего угодно в строку с помощью str в питоне есть и ещё одно, более могучее средство формирования строк: метод format. Он предполагает такую парадигму работы: мы сначала описываем шаблон того, как в общем должна выглядеть строка, а затем заполняем шаблон значениями.

Для примера вернёмся к песенке про бутылки. Мы могли бы реализовать вывод одной строки из неё так:

   1 template = "{} bottles of beer on the wall, {} bottles of beer\nTake one down, pass it around, {} bottles of beer on the wall"
   2 print template.format(99, 99, 98)

В шаблоне мы символами {} обозначаем место, куда нужно вставить очередное значение. После этого метод format получает аргументами те значения, которые нужно подставить, в том же порядке.

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

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

   1 template = "{1} bottles of beer on the wall, {1} bottles of beer\nTake one down, pass it around, {0} bottles of beer on the wall"
   2 print template.format(98, 99)

За счёт этого мы получаем заодно и свободу перестановки подставляемых значений местами, что в данном примере выглядит чуточку более наглядно.

Если мы хотим подставлять числа, то мы можем делать с ними то же, что и в экселе:

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

   1 template = "{1:2} bottles of beer on the wall, {1:2} bottles of beer\nTake one down, pass it around, {0:2} bottles of beer on the wall"
   2 print template.format(98, 99)

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

(Их сильно больше – отсылаю вас к документации, чтобы обнаружить их).

Указывать количество знаков после десятичной точки мы можем только для дробных чисел. Синтаксис: точка и сколько знаков:

   1 template = "{1} bottles of beer on the wall, {1:.2f} bottles of beer\nTake one down, pass it around, {0} bottles of beer on the wall"
   2 print template.format(98, 99)

Это самые полезные примеры.

Ещё несколько:

   1 print "{0:6.2f}".format(42) # всего 6 знаков, из них 3 на дробную часть: точка и два знака после неё
   2 print "{0:+d}".format(42) # всегда писать + для положительных чисел (обычно пишется только - для отрицательных
   3 print "{0:06}".format(42) # заполнить пространство перед числом нулями
   4 print "{0:1}".format(42) # если величина не вписывается в требуемую длину, то оно вылезает за рамки -- так что заводите ширину всегда достаточной!

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

Experts only

Мы можем использовать подстановки и внутри самих подстановок: print "{0:{1}}".format(value, width)

Передача в функции значений по имени

Некоторые функции принимают много параметров, некоторые из которых не обязательны. (Например, у codecs.open больше трёх обязательных параметров, но обязательный только один). У таких функций мы можем подсмотреть в документации, как именно называются их параметры (ещё их выводит в подсказке IDLE), и указывать только нужные из них по имени:

   1 import codecs
   2 with codecs.open("words.txt", encoding="utf-8") as file:
   3     ...

Документация нам подсказывает, что значение по умолчанию для второго параметра: 'rb', т.е. эта строка абсолютна эквивалентна такой:

   1 import codecs
   2 with codecs.open("words.txt", "rb", encoding="utf-8") as file:
   3     ...

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

Позиционные аргументы в вызове функции всегда идут перед именованными.

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

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

   1 print "{more} bottles of beer on the wall, {more} bottles of beer,\nTake one down, pass it around, {less] bottles of beer on the wall".format(more=99, less=98)

Создание своих функций

Чем дальше мы будем программировать, тем более сложные программы мы будем писать.

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

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

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

   1 def hello():
   2     who = "world"
   3     print "Hello", who

Все эти три строки значат для питона следующее: "знай, что теперь у тебя есть функция hello(); пока что ничего, кроме этого не делай".

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

   1 hello()
   2 hello()

Для остальных функций в питоне мы умеем передавать аргументы. Значит, и для тех, которые мы делаем сами, это тоже можно:

   1 def hello(who):
   2     print "Hello", who

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

   1 hello("Bob")

мы можем считать (это сильное упрощение), что эту строку питон заменит на:

   1 who = "Bob"
   2 print "Hello", who

Аргументов может быть сколько угодно:

   1 def hello(who, how):
   2     print "Hello", who, how
   3 
   4 hello("Bob", "sincerely")

Ещё одно, что мы умеем – это получать из функции результат (например, для того, чтобы положить его в переменную). Для этого функция должна результат возвращать:

   1 def increase(number):
   2     return number + 1

Команда return обозначает два действия:

  1. её аргумент будет результатом функции
  2. в этот момент исполнение функции завершается. Т.е. если в функции есть какие-то команды после return, то они просто не будут исполнены. Это очень существенная возможность. Она позволяет нам выходить из функции раньше времени, если мы уже поняли, что мы сделали всё, что нужно.

   1 x = increase(2)
   2 print x

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

tokenize

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

   1 def tokenize(text):
   2     chunks = text.split()
   3     tokens = []
   4     for chunk in chunks:
   5         token = chunk.strip(u".,?!\"'-+")
   6         if token != u"":
   7             tokens.append(token)
   8     return tokens

Многие задания в этом модуле будут включать либо использование этой функции, либо её доработку.

repr

В некоторых случаях питон старается воспроизвести строку не так, чтобы она была читаема у человека, у которого установлены все шрифты, а так, чтобы у человека с очень ограниченными возможностями экрана она отображалась так же, как у человека со всеми нужными шрифтами. При таком отображении он заменяет русские буквы на сочетания вида \xцифры или \uцифры. Ещё при этом отображении питон добавляет пару кавычек снаружи от строки. Это отображение называется repr. И применяет его питон в случаях:

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

   1 print ", ".join(tokens)

Литература

  1. Включая исключения, прерывания, return и т.п. -- поясняю для тех из вас, кому какие-нибудь из этих слов имеют смысл. (1)

  2. А вот свободы выбора десятичного разделителя у нас нет, только точки (2)