Kodomo

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

Учебная страница курса биоинформатики,
год поступления 2011

Запуск внешних программ. Списочные сокращения.

Запуск внешних программ

В питоне, как и во многих других языках программирования, есть возможность запускать другие (внешние) программы прямо из питоновского кода. Для этого лучше всего использовать библиотеку subprocess (http://docs.python.org/2.7/library/subprocess.html).

call

Для того, чтобы просто запустить программу и узнать, с каким кодом она завершилась, лучше использовать функцию call:

   1 >>> r=subprocess.call(["dir", "-l"])
   2 total 2566
   3 -rwx------+ 1 SYSTEM система     324 Feb  2  2011 ClearPluginsCache.cmd
   4 drwx------+ 1 SYSTEM система       0 Sep  4  2012 Documentation
   5 drwx------+ 1 SYSTEM система       0 Sep  4  2012 FExcept
   6 -rwx------+ 1 SYSTEM система 1380352 Feb  2  2011 Far.exe
   7 -rwx------+ 1 SYSTEM система  206129 Feb  2  2011 FarEng.hlf
   8 -rwx------+ 1 SYSTEM система   36232 Feb  2  2011 FarEng.lng
   9 -rwx------+ 1 SYSTEM система  331978 Feb  2  2011 FarRus.hlf
  10 -rwx------+ 1 SYSTEM система   62704 Feb  2  2011 FarRus.lng
  11 -rwx------+ 1 SYSTEM система     561 Feb  2  2011 File_id.diz
  12 drwx------+ 1 SYSTEM система       0 Sep  4  2012 Plugins
  13 -rwx------+ 1 SYSTEM система     772 Feb  2  2011 RestoreSettings.cmd
  14 -rwx------+ 1 SYSTEM система     734 Feb  2  2011 SaveSettings.cmd
  15 -rwx------+ 1 SYSTEM система  585638 Feb  2  2011 far.map
  16 >>> print(r)
  17 0

Первым аргументом передается список строк, первым элементом которого должно быть имя запускаемой программы, а остальные – аргументами этой программы (если они есть). Эта функция возвращает код, с которым завершилась программа (0 как правило означает, что программа завершилась корректно). По умолчанию, стандартные потоки ввода-вывода программы (STDIN, STDOUT и STDERR) перенаправляется из/в стандартные потоки родительского процесса (то есть питоновской программы). Если нужно перенаправить STDIN, STDOUT и/или STDERR в файл, то нужно передать файл в качестве аргументов stdin, stdout и/или stderr в функцию call:

   1 >>> out=file("out.txt","w")
   2 >>> subprocess.call(["dir", "-l"], stdout=out)
   3 0

check_call

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

   1 import subprocess
   2 try:
   3    pr =subprocess.check_call(["dir","not_existing_dir"])
   4 except Exception, e:
   5     print e.cmd
   6     print e.returncode
   7     print e

Если запустить этот скрипт, на консоль распечатается следующее:

['dir', 'not_existing_dir']
2
Command '['dir', 'not_existing_dir']' returned non-zero exit status 2

check_output

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

   1 >>> d = subprocess.check_output("dir")
   2 >>> print d
   3 ClearPluginsCache.cmd  Far.exe     FarRus.hlf   Plugins              far.map
   4 Documentation          FarEng.hlf  FarRus.lng   RestoreSettings.cmd
   5 FExcept                FarEng.lng  File_id.diz  SaveSettings.

При этом check_output, также как и check_call, выбрасывает исключение в случае некорректного завершения внешней программы.

Popen

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

   1 import subprocess
   2 subpr = subprocess.Popen(["infoseq"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   3 out, err = subpr.communicate("proteins.fa")

Аналогично, строка, передаваемая на вход одной программе, может быть выдачей другой программы. Вот несколько искусственный пример (работает только под Linux или под Windows с cygwin, т.к. использует программки grep и wc):

   1 import subprocess
   2 
   3 subpr1 = subprocess.Popen(["grep",">"], stdin=file("proteins.fa"), stdout=subprocess.PIPE)
   4 out1, err1 =subpr1.communicate()
   5 subpr2 = subprocess.Popen(["wc", "-l"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
   6 out2, err2 = subpr2.communicate(out1)
   7 print out2 

В этом примере программа grep находит в переданном файле все строчки, содержащие знак '>', которые в виде одной длинной строки (с переносами строк) попадают в переменную out1. Эту строчку обрабатывает программка wc -l, которая просто считает и возвращает количество строк. Таким образом, переменная out2 должна содержать количество строк во входном файле, содержащих знак '>'. В случае файла в формате fasta, это соответствует количеству последовательностей в этом файле. Но так можно делать только если передаваемая строка (out1) не очень большая и может влезть в оперативную память. В противном случае лучше создавать pipe между программами (как | в shell): выдачу одной программы напрямую передавать на вход другой программе:

   1 import subprocess
   2 
   3 subpr1 = subprocess.Popen(["grep",">"], stdin=file("proteins.fa"), stdout=subprocess.PIPE)
   4 subpr2 = subprocess.Popen(["wc", "-l"], stdin=subpr1.stdout, stdout=subprocess.PIPE)
   5 out, err = subpr2.communicate()
   6 print out 

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

   1 import subprocess
   2 
   3 subpr1 = subprocess.Popen(["grep",">"], stdin=file("proteins.fa"), stdout=subprocess.PIPE)
   4 subpr2 = subprocess.Popen(["wc", "-ll"], stdin=subpr1.stdout, stdout=subprocess.PIPE)
   5 subpr1.stdout.close()
   6 out, err = subpr2.communicate()
   7 print out 

List comprehensions

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

Эта выразительная штука в питоне начала зарождаться очень давно. Где-то в начале-середине прошлого века сначала Эрнст Фрэнкель, а потом и Адольф Авраам Халеви Цермело занимались сочинением аксиоматики теории множеств и одна из их аксиом звучала так: если S – множество и p – предикат (т.е. функция, которая возвращает либо истину, либо ложь), то { x | x ∈ S, p(x) } – это тоже множество. Эту аксиому назвали аксиомой выделения, а по-английски the Axiom of Comprehension. Собственно, возможность писать такие конструкции { x | x ∈ S, p(x) } определяется в значительной степени этой аксиомой, поэтому эту нотацию стали называть (в некоторых узких областях математики) set comprehensions. А по-русски никак её не называют. Эта нотация множеств вдохновила авторов функциональных языков программирования сотворить подобную ей нотацию в своих языках. В функциональных языках эта нотация использовалась для преобразований списков, поэтому её стали называть list comprehensions. Одно из наиболее ярких последних творений на эту тему было в языке haskell, откуда, по его собственным словам, Гвидо и позаимствовал эту идею для питона3. По-русски "list comprehensions" можно называть разве что выделениями списков или списочными сокращениями.

Итак, в питоне можно писать такое: [ выражение for переменная in список ] и значить это будет следующее: для каждого элемента списка, указать на него этой переменной и вычислить в этом контексте выражение, собрать все результаты таких вычислений и положить снова в список.

   1 >>> [ x ** 2 for x in [1, 2, 3]]
   2 [1, 4, 9]

Эта конструкция эквивалентна трём строкам питонского кода:

result = []
for x in [1, 2, 3]:
        result.append(x ** 2)

Знакомая конструкция, не правда ли? Теперь мы знаем, как это писать короче!

Сразу несколько примеров применения.

Мы хотим разобрать csv файл, в котором записаны числа. Подход первый, самый старый:

   1 def parse_numbers(file):
   2         result = []
   3         for line in file:
   4                 numbers = []
   5                 for word in line.split(","):
   6                         numbers.append(int(word))
   7                 result.append(numbers)
   8         return result

Тут мы сразу видим, что внутренний цикл можно представить в виде "выделения списка":

   1 def parse_numbers(file):
   2         result = []
   3         for line in file:
   4                 numbers = [int(word) for word in line.split(",")]
   5                 result.append(numbers)
   6         return result

И снова упираемся в точно такую же конструкцию. Значит, и её можно свернуть:

   1 def parse_numbers(file):
   2         return [[int(word) for word in line.split(",")] for line in file]

Нельзя назвать эту запись самой очевидной, но зато она короче исходной в 4 раза (или вообще в 7 раз, смотря как считать).

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

Что может быть проще!

def parse_keys(line):
        return dict([word.split('=', 1) for word in line.split()])

line.split() – превратили строку в список слов. word.split("=", 1) – разбили слово по первому вхождению "=" (если никакого равенства в слове нет, нам вернётся список из одного элемента, и это вызовет ошибку, что хорошо; если есть больше одного равенства, то мы предполагаем, что второе равенство – это часть текста значения; split(..., 1) возвращает список длины не более 2). Полученный список (списков длины 2) превращаем в словарь и возвращаем.

Следюущий элемент сложности, который мы можем внести в list comprehensions: мы можем ходить не по одному списку, а по нескольким. Это будет эквивалентно нескольким вложенным циклам. Пример:

   1 >>> [a + str(b) for a in ["a", "b"] for b in range(3)]
   2 ['a0', 'a1', 'a2', 'b0', 'b1', 'b2']

То есть эта конструкция эквивалентна четырём строкам:

   1 result = []
   2 for a in ["a", "b"]:
   3         for b in range(3):
   4                 result.append(a + str(b))

Способ запомнить, в каком порядке случается обход: если в list comprehension воткнуть переносов строк, отступов и двоеточий, и убрать немного лишнего, то получатся вложенные циклы, обходящие списки ровно в том же порядке. Иными словами: первый for внешний (меняется реже всего), внутри него второй for (пробегает все значения для каждой итерации первого), внутри него третий, и так далее.

Наконец, в list comprehensions есть и ещё одна вещь, которую можно вставлять: проверки. Синтаксис столь же простой, как и раньше:

   1 >>> [x for x in range(20) if random.random() > 0.5]
   2 [3, 4, 7, 10, 12, 17]

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

   1 result = []
   2 for x in range(20):
   3         if random.random() > 0.5:
   4                 result.append(x)

Надеюсь, в этом месте не нужно никаких больше пояснений.

Как и for, if'ы можно добавлять в любом количестве, и их можно чередовать. Эффект будет таким же, как ровно в той же последовательности записанные вложенные for'ы и if'ы. В более вложенных конструкциях можно использовать переменные, определённые в более внешних (т.е. раньше по тексту).