воскресенье, 27 декабря 2009 г.

OptionParser и UnitTest в python скриптах

Небольшой пример использования OptionParser и UnitTest

Предыстория


Пишется скрипт для обработки и графического вывода неких исследовательских данных. Но история не об этом. Хотелось бы показать как именно используется комбинация OptionParser и UnitTest модулей. А за одно узнать способы улучшить код и его читаемость. Так что гуру Pythonоводства, я буду премного благодарен Вашей критике и предложениям.

Модули


Об методе Test Driven Development (TDD) разработке программных продуктов каждый слышал хотя бы раз в своей жизни. Непосредственно с реализацией такого подхода для питона я столкнулся в книге Макра Пилгрима «Diving into Python 3». В девятой главе своей книги Марк подробно описывает способ реализации юнит тестов для своего модуля преобразования римских чисел. Основой этого принципа можно назвать написание теста для проверки правильности выполнения кода до написания самого кода.

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

Проектирование же новой задачи показало, что скрипт будет достаточно сложный. И написание Unit testов к нему будет оправдано. Т.к. последующий рефакторинг и отладка будет намного упрощена.

Следующим на очереди идёт OptionParser, который используется мной давно, и похоже, ещё долго будет использоваться. Читаемость кода при его использовании увеличивается многократно. Парсеров подобных данному — несколько штук. И в своё время велись активные холивары, про то, какой из них лучше. Были обвинения в том, что он навязывает свою философию в организации и обработке опций. Честно говоря, ничего «странного» в этой самой организации мной замечено не было. И опять таки, это будет в первую очередь зависеть от программиста как он реализует вчитываемость той или иной опции. Так что холивар отложим пока в сторонку.

Source code


Давайте сразу перейдём к исходному коду. В выполняемом модуле пока присутствует только одна рабочая функция readin_monitor(monitor).

Copy Source | Copy HTML
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*- 
  3.  
  4. version = '0.0.1'
  5. version_name = 'gamma'
  6. modify_date = '2009-12-26'
  7.  
  8. from optparse import OptionParser
  9.  
  10. import matplotlib
  11. import numpy as np
  12. import scipy.stats as stats
  13.  
  14. import warnings
  15. warnings.filterwarnings('ignore', '', DeprecationWarning)
  16. # turning off deprecation warning in python 2.6
  17.  
  18. kB = 8.31441e-3 / 4.184
  19.  
  20. def readin_monitor(monitor):
  21.     '''Read in monitor file. Ignoring all strings starting with # symbol.
        Function returns all stored data from the strings as list of lists of 
        floats.'''
  22.     num =  0
  23.     data = []
  24.     for line in open(monitor, 'r'):
  25.         try:
  26.             if line[ 0] != '#':
  27.                 data.append([float(i) for i in line.split()])
  28.                 num = num + 1
  29.         except:
  30.             pass
  31.     if options.verbose:
  32.         print('Read in %i data points from monitor file %s' % (num, monitor))
  33.     return data
  34.  
  35. def main():
  36.     return  0
  37.  
  38.  
  39. global options
  40. global args
  41. parser = OptionParser("usage: %prog [options] [monitors]",
  42.                   version='%prog ' +version+ ' from '+modify_date)
  43. parser.add_option("-v", "--verbose",
  44.                   action="store_true", dest="verbose", default=False,
  45.                   help="Print status messages to stdout")
  46. parser.add_option("-C", "--combine", dest="combine",
  47.                   action="store", default="",
  48.                   help='Combine all monitor files passed as arguments \
                      to the UC.py script to one COMBINE file')
  49. parser.add_option('-D', '--dimentions', dest='dimentions',
  50.                   default = '1:2',
  51.                   help='String of DIMENTIONS for monitor files to be \
                      read in. (defaut = %default)')
  52. (options, args) = parser.parse_args()
  53. if __name__ == '__main__':
  54.     main()



Из особенностей расположения кода хочется отметить определение опций парсера в конце самого модуля. Т.е. этот кусок кода будет выполняться всегда, даже если модуль вызывается другим скриптом. Таким образом в глобально определённых переменных options и args будут значения опций по умолчанию (default values), args будет пустой. Т.к. переменные глобальные, то доступ к ним будет возможен из любого окружения.

Запуск скрипта с опцией -h выдаст подробную справку об использовании опций:

Copy Source | Copy HTML
  1. Usage: UC.py [options] [monitors]
  2.  
  3. Options:
  4.   --version show program's version number and exit
  5.   -h, --help show this help message and exit
  6.   -v, --verbose Print status messages to stdout
  7.   -C COMBINE, --combine=COMBINE
  8.                         Combine all monitor files passed as arguments
  9.                         to the UC.py script to one COMBINE file.
  10.                         (defaut = out)
  11.   -D DIMENTIONS, --dimentions=DIMENTIONS
  12.                         String of DIMENTIONS for monitor files to be
  13.                         read in. (defaut =  0:1:2)



Далее сами unit tests:

Copy Source | Copy HTML
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. '''Unit tests for UC.py module.'''
  4.  
  5. import UC
  6. import unittest
  7.  
  8. global monitor
  9. monitor = '''#
    # MD time (ps), CV #1, CV #2
    #
          0.9990     9.2349535263     7.7537518211
          1.9990     9.4331321327     7.9555258177
          2.9990     9.5368308183     8.1341402536
          3.9990     9.4468066031     7.9086253193
          4.9990     9.1565151681     8.0027457962
          5.9990     9.2310306859     7.9872398398
          6.9990     9.1540695183     7.5236796623
          7.9990     9.0727576308     7.8499035889
          8.9990     9.3113419250     8.1227557439
          9.9990     8.9597834513     8.3754973753
         10.9990     9.5761421491     8.3053224696
         11.9990     9.5178829977     8.1660258902'''
  10.  
  11. class Combine_monitors(unittest.TestCase):
  12.     def test_readin_monitor(self):
  13.         with open('test_mon', 'w') as MON:
  14.             MON.write(monitor)
  15.         UC.options.verbose = False
  16.         self.assertEqual([[ 0.999, 9.2349535263, 7.7537518210999998],
  17.             [1.9990000000000001, 9.4331321327000008, 7.9555258176999999],
  18.             [2.9990000000000001, 9.5368308183000003, 8.1341402536],
  19.             [3.9990000000000001, 9.4468066031000006, 7.9086253192999996],
  20.             [4.9989999999999997, 9.1565151681000003, 8.0027457961999993],
  21.             [5.9989999999999997, 9.2310306859000004, 7.9872398398],
  22.             [6.9989999999999997, 9.1540695183, 7.5236796623000002],
  23.             [7.9989999999999997, 9.0727576308, 7.8499035889000002],
  24.             [8.9990000000000006, 9.3113419250000007, 8.1227557439000009],
  25.             [9.9990000000000006, 8.9597834512999999, 8.3754973753000002],
  26.             [10.999000000000001, 9.5761421491000007, 8.3053224696000001],
  27.             [11.999000000000001, 9.5178829976999992, 8.1660258902000002]],
  28.             UC.readin_monitor('test_mon'))
  29.  
  30. def main():
  31.     unittest.main()
  32.     return  0
  33.  
  34. if __name__ == '__main__':
  35.     main()
  36.  



К написанному тесту стоит добавить удаление временных файлов. Ну и конечно же увеличить число тестов, по мере реализации новых функций скрипта. Запуск скрипта приводит к такому вот выводу:

$ ./test-UC.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK


Для написания этого небольшого куска кода мне пришлось немного “обмануть всех” (кажется так можно перевести глагол cheat). Тест был написан уже после написания самой функции readin_monitor() из основного модуля. Результат функции просто был выкинут print’ом в stdout. И от туда перекачивал в исходный код тест-модуля.

Что не нравиться — вроде бы обманываем сами себя. Сначала пишем код, потом тест, нарушая тем самым философию TDD разработки. Ещё и результаты на выходе, из-за специфики языка получились не точными (имеется в виду 5.9989999999999997 = 5.9990 округление). Если запустить этот же юнит тест в другой версии питона, то может получиться ошибка теста. Для Python 3.1 тест был пройден положительно, но меня всё равно настораживают такие тесты на точность. Можно, конечно, самому организовать округление до, скажем, 5ого знака после запятой, и сравнивать уже округлённые данные. Но это чревато сильным утяжелением кода, и, как результат, плохочитаемостью оного.

Итого


На примере двух коротких зародышей скриптов мы показали как можно использовать возможности OptionParser и UnitTest модулей. Целью стати не являлось полноценное описание всех их возможностей, поэтому любопытному читателю дана возможность разобраться в них самому, использую ссылки из начала статьи.

Комментариев нет:

Отправить комментарий