Kodomo

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

Взаимодействие с ОС

– Гоги, што такое осъ?

– Ос – это балшой паласатый мухъ!

– Нет, Гоги. Балшой паласатый мухъ – это шмелъ! А осъ – это то, вакруг чего Земля вертитса!

— Баян

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

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

Алгоритм такой.

Если у нас есть объект, про который мы не знаем, что это, или знаем, что это но не помним, как с ним работать, то наш помощник – функция help.

Например, если мы забыли, как работать с функцией sorted, говорим питону help(sorted). Если мы импортировали модуль sys и хотим в нём отыскать что-то, что мы приблизительно помним: help(sys). Если мы помним, что мы хотим разобраться с функцией sys.exit, так и говорим: help(sys.exit).

Второй случай: если мы знаем или надеемся, что питон умеет работать с заданной темой, но не знаем о том, как это делать в принципе, идём на сайт питона (docs.python.org), ищем Library Reference, и пытаемся на этой длинной странице с довольно логичным, но неудобным разбиением на разделы отыскать что-то с похожим содержанием. Большинство страниц про отдельные модули в Library Reference организованы как довольно внятное введение в тему (а не просто как справочник), и если речь идёт о чём-то нетривиальном (по мнению авторов, разумеется), то на них приводятся примеры.

Сегодня я вам чуть-чуть упрощу задачу, и сразу назову все модули, в которых вам нужно будет искать помощи. Это: os, os.path, shutil, glob, fnmatch.1

Тесты

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

Начиная с этого раза вы пишете полноценные программы. Для полноценных программ тесты писать гораздо труднее (можно не всегда, всегда труднее).

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

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

Снова про циклы

Я обнаружил у себя в курсе небольшую оплошность.

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

Цикл while.

Пишется так:

   1 while condition:
   2     body

Семантика: перед исполнением такого цикла проверяется условие condition, если оно истинно, исполняется body и всё начинается сначала, если оно ложно, цикл прекращается.

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

Ещё две стандартные конструкции для циклов, которые есть во многих языках программирования: break и continue.

break обозначает прервать цикл. continue – прервать текущую итерацию цикла (и перейти к следующей итерации сразу, или завершить, если следующей итерации таки нету).

Примеры:

   1 >>> for x in [1, 2, 3]:
   2 ...     if x == 2:
   3 ...         break
   4 ...     print(x)
   5 1
   6 >>> for x in [1, 2, 3]:
   7 ...     if x == 2:
   8 ...         continue
   9 ...     print(x)
  10 1
  11 3

На этом рассказ про то, как пользоваться циклами в питоне исчерпывается окончательно!

Скрипты

Слово скрипт является очень свежим заимствованием из английского, происходит от слова script. Дословный перевод: сценарий. Это слово тоже применяют, в том же смысле. Примерно тот же смысл вкладывают сейчас, зачастую и в слово программа3. Сейчас я буду использовать эти слова вперемешку, как синонимы.

Некоторые из вас уже заметили, что если создать файл hello.py, и написать туда:

   1 print("Hello")

и потом выполнить этот файл с помощью питона, то питон напишет на экран эту самую строку Hello, как мы его и попросили.

В принципе, так можно и писать программы на питоне.

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

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

   1 #!/usr/bin/python
   2 """ A tool for sepulking.
   3 """
   4 import optparse
   5 # import this
   6 # ...
   7 
   8 def main(options, args):
   9     print("options.arg = %s" % options.arg)
  10     print("options.flag = %s" % options.flag)
  11     print("args = %s" % args)
  12     print("Use sepulkarium!")
  13 
  14 # def not_main():
  15 #     ...
  16 
  17 if __name__ == "__main__":
  18     parser = optparse.OptionParser(description=__doc__)
  19     parser.add_option("-a", "--arg",
  20         help="Option with argument")
  21     parser.add_option("-f", "--flag", action="store_true",
  22         help="Flag, i.e. an option without argument")
  23     options, args = parser.parse_args()
  24 
  25     main(options, args)

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

Итак, строка 1. Это шаманская строка, которая имеет отношение к UNIX и только к UNIX. Она нужна, чтобы программу можно было вызывать без того, чтобы пользователь знал, что она написана на питоне. (Пока что вам трудновато понять даже пользу от этой строки, полагаю. Тупо копируйте).

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

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

Строка 4. Импортируем какой-то там модуль, называющийся optparse. Более-менее понятно. (Модуль optparse отвечает за разбор командной строки. Подробнее об этом дальше).

Строки 5 – 6. В приличной питонской программе все import должны находиться именно в этом месте. Гвидо даже специфицировал довольно строго, в каком порядке они должны идти: сначала встроенные в питон, потом доустановленные пакеты, потом модули за нашим же авторством.

Строки 8 – 15. Здесь находится основное содержимое программы. Всё, что идёт после import'ов и до if __name__ == "__main__" (который у нас на 17 строке), должно быть определениями функций и только определениями функций. Все определения функций в нашей программе должны быть в этих строках. Так и только так. Принимайте на веру или вникайте в глубинный смысл 17-й строки. Если вы пишете не так, вы должны мне объяснить, почему.

Строка 17. Магия. Истерически слежалось, что эта магия должна иметь именно такой вид. Повторять, не задумываясь. if __name__ == "__main__" должен быть последней конструкцией в программе.

Строки 18 – 23. Тут всё плохо. Это магия. Это магия объяснимая, но не с тем багажом знаний, которые дал вам я. В ней нет ничего сложного, вы скоро сами всё поймёте. Плохо же то, что в этом месте вам нужно редактировать.

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

Самый простой случай:

   1 ...
   2     parser = optparse.OptionParser(description=__doc__)
   3     options, args = parser.parse_args()
   4 ...

Такая программа принимает один параметр командной строки: -h / --help. Об этом здесь не сказано ничего. Это происходит у нас за спиной потому, что иначе бы мы об этом забывали, а нужно оно всё равно всегда.

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

Разъясняю терминологию на примере утилиты ls:

Флаг описывается так:

   1     parser.add_option("-f", "--flag", action="store_true",
   2         help="Flag, i.e. an option without argument")

Нам можно и нужно править -f, --flag (это те имена, под которыми флаг будет виден пользователю), и текст хелпа.

Параметр описывается так:

   1     parser.add_option("-p", "--param",
   2         help="Parameter that takes an argument")

Разница в том, что бы не указали action="store_true". Править нужно снова -p, --param и хелп.

А теперь пример почти из жизни: как это могло бы выглядеть, скажем, для утилиты touch (зайдите на kodomo и посмотрите, что вам выдаст touch --help; я делал именно так, чтобы составить эту таблицу).

   1 ...
   2     parser = optparse.OptionParser(description=__doc__)
   3     parser.add_option("-a", "--atime", action="store_true",
   4         help="change only the access time")
   5     parser.add_option("-c", "--no-create", action="store_true",
   6         help="do not create any files")
   7     parser.add_option("-d", "--date",
   8         help="parse DATE and use it instead of current time")
   9     parser.add_option("-f", action="store_true",
  10         help="(ignored)")
  11     parser.add_option("-m", "--mtime", action="store_true",
  12         help="change only the modification time")
  13     parser.add_option("-r", "--reference",
  14         help="use this file's times instead of current time")
  15     parser.add_option("-t", "--timestamp",
  16         help="use [[CC]YY]MMDDhhmm[.ss] instead of current time")
  17     parser.add_option("--time",
  18         help="change the specified time:
  19             TIME is access, atime, or use: equivalent to -a
  20             TIME is modify or mtime: equivalent to -m")
  21     options, args = parser.parse_args()
  22 ...

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

Такой программе можно передавать все те же флаги, что и touch. Как можно touch вызвать touch hello.txt world.txt -acfd "Oct 22", и это будет синоним touch -a --no-create -f --date="Oct 22" hello.txt world.txt, так и нашу программу можно будет вызывать с такими же аргументами, и оба описанные способа вызова будут допустимы.

Возвращаемся к заготовке для скрипта.

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

Итак. Мы описали, какие параметры командной строки программа может получать. Куда она их получает?

Строка 23 гласит: options, args = parser.parse_args(). Это tuple unpacking (менее коряво это название после перевода на русский звучать не станет), конструкция нам уже знакомая. Мы получаем две переменные – options и args. args – это список позиционных параметров. options – это объект, в котором хранятся значения опций. Например, если мы описали опцию -f / --flag, которая является флагом, то options.flag будет хранить None, если флага в командной строке не было и True, если флаг в строке был. Если мы описали опцию -p / --param, которая является параметром, то options.param будет хранить None или строку.

Пример: ls

И всё же, один пример лучше тысячи слов. Попробуем. (Заодно примеры тех функций, которые вам потребуются).

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

   1 #!/usr/bin/python
   2 """List files in current directory"""
   3 import optparse
   4 import os
   5 import os.path
   6 import fnmatch
   7 
   8 def main(options, args):
   9     if args == []:
  10         args = ["."]
  11     for dir in args:
  12         print("%s:" % (dir))
  13         for file in os.listdir(dir):
  14             if file[0] == ".":
  15                 continue
  16             if options.ignore is not None:
  17                 if fnmatch.fnmatch(file, options.ignore):
  18                     continue
  19             if options.classify:
  20                 if os.path.isdir(file):
  21                     file += "/"
  22                 elif os.path.islink(file):
  23                     file += "@"
  24             print(file)
  25 
  26 if __name__ == "__main__":
  27     parser = optparse.OptionParser(description=__doc__)
  28     parser.add_option("-F", "--classify", action="store_true",
  29         help="append indicator (one of /@) to entries")
  30     parser.add_option("-I", "--ignore",
  31         help="do not list entries matching shell PATTERN")
  32     options, args = parser.parse_args()
  33 
  34     main(options, args)

Прочитаю текст функции main по-русски:

Строки 9 – 10. Если позиционных параметров нет, мы работем с текущей директорией.

Строка 11. Для каждой директории из позиционных параметров (или списка из текущей директории), выполнять:

Строка 12. Напечатать имя рассматриваемой директории.

Строка 13. Для каждого файла в директории.

Строки 14 – 15. Если это скрытый файл (в UNIX скрытыми считаются файлы, у которых первая буква имени – это точка), игнорировать его. (Т.е. перейти к следующему файлу).

Строки 16 – 18. Если параметр -I или --ignore в командной строке был, и если файл попадает под переданную маску, игнорировать этот файл. (Перейти к следующему).

Строки 19 – 23. Если в командной строке был флаг -F или --classify, приписать к имени файла букву: если это директория, приписать "/", если это ссылка, приписать "@".

Строка 24. Распечатать получившееся имя файла.

Пример: find

Ещё один пример приведу не ради разъяснения про optparse, а ради функции os.walk, которая довольно универсальна, выразительна, удобна, и потому её тяжко понять даже из весьма подробного описания в руководстве.

Сначала определю понятие рекурсивного обхода директорий. Рекурсивный обход директорий – это когда мы перебираем каждый файл и директорию внутри данной директории, внутри каждой её поддиректории, внутри каждой поддиректории каждой её поддиректории и так далее.

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

Её можно было бы реализовать не очень большими мучениями через os.listdir, например, но есть путь проще. Рекурсивный обход за нас уже реализован в питоне, и это как раз os.walk.

Как могла бы выглядеть функция main для утилиты find в наших условиях:

   1 def main(options, args):
   2     for triplet in os.walk("."):
   3         root, dirs, files = triplet
   4         for file in dirs + files:
   5             print(os.path.join(root, file))

То, что возвращает os.walk, не является списком, однако это есть то, что можно скормить в for. На каждой итерации в triplet оказывается тройка: путь рассматриваемой директории, список поддиректорий в ней и список файлов в ней. Их можно было бы игнорировать и обходиться выдачей os.listdir(root), например.

Исторически сложилось, что в DOS и Windows части пути разделяются \ (хотя в последних Windows можно писать и /), в UNIX части пути разделяются /, а в каких-то верскиях MacOS части пути разделялись ::. Функция os.path.join объединяет две компоненты пути с учётом того, в какой ОС работает программа. Например, в UNIX os.path.join(a, b) есть синоним a + "/" + b.

Текущая директория

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

В питоне её можно узнавать и менять. Читайте документацию про: os.getcwd() и os.chdir(path). (os.getcwd() ведёт себя как команда UNIX pwd, os.chdir() – как cd).

Код возврата

Ещё всё в тех же ОС есть некоторое количество информации, которое после завершения программы сообщается тому, кто эту программу запускал. Нас из этого интересует только нечто под названием "код возврата". Это число. (Обычно от -128 до 127 или от 0 до 255).

Код возврата ещё принято называть кодом ошибки. Потому, если ошибки не было, и программа завершилась успешно, то значение его 0. Как правило факт ошибки обозначают тем, что возвращают из программы 1 (иногда разводят и больше разнообразия).

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

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

Скажу иначе. Если вы определили в программе, что в этом месте случилась непоправимая ошибка, то вы должны сделать следующее: sys.exit(1) – и никак иначе. (Может быть, можно ещё пользователю сообщить об ошибке).

Исполнение программ

В модуле os имеется большое море функций для запуска других программ из вашей программы. os.*exec*, os.*spawn*, os.popen (звёздочками я символизирую, что там бывают разные буквы в начале и конце слова).

Ими пользоваться я не советую. Муторно.

Я советую вам пользоваться функцией os.system. Читайте документацию.

Напоминание про модули

Много где выше я использовал нотацию os.system и т.п. Эта нотация обозначает, что мы из модуля os вытаскиваем функцию system. Возможно это только в том случае, если мы уже сказали где-то раньше import os.

Объясню, что тут происходит. Всё, чему мы можем дать имя (записать в переменную), – это объект. Можно считать именно это определением объекта. В объекте хранится много всего. Когда мы пишем объект.имя, это значит, что мы лезем внутрь объекта и достаём из него то, что хранится под именем имя. Когда мы говорим import модуль, питон просто-напросто создаёт переменную модуль, которая указывает на этот самый модуль. Ну а модуль – это тоже объект – и это просто свалка всяких функций, которые в нём определены.

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

  2. Кто-нибудь из вас смотрел, как выглядят тесты? Кто-нибудь из вас смотрел, как выглядит система тестирования и пытался разобраться, как она устроена? Кто-нибудь из вас пробовал тесты самостоятельно запускать? (2)

  3. Слово программа обычно имеет более общий смысл. Скрипты в разных контекстах могут иметь подтекст либо программы маленькой, из одного модуля, либо программы, занимающейся, в основном, обработкой строк и взаимодействием с ОС (3)