вторник, декабря 23, 2008

Все форматы документа из одного исходника: asciidoc сотоварищи

Я уже давно использую asciidoc для написания сколько-нибудь больших текстов. Почти все статьи в этом блоге, включая эту, подготовлены с помощью Asciidoc.

Asciidoc - это транслятор простейшего языка разметки текста в любой другой язык разметки. Разметка asciidoc очень простая, практически вы пишете plain text, только выделяете заголовки знаками = в начале строки, полужирный текст - *звёздочками*, курсив - 'кавычками', итд. Абзацы разделяются пустой строкой. А на выходе может быть всё что угодно, это зависит от так называемого backend-a, поведение которого описывается в конфиге. В поставке доступны бэкенды для xhtml, html4 и docbook. Docbook, в свою очередь, теоретически можно отконвертировать во что угодно.

На днях я готовил доклад для одного семинара, и мне хотелось получить его сразу в нескольких форматах: html и pdf, как минимум. И ещё бы надо к нему презентацию… И хорошо бы план доклада. И, конечно, не хочется для каждого формата готовить текст.

HTML (точнее, xhtml 1.1) делается с помощью asciidoc. Все остальные форматы, теоретически, можно получить из docbook, который можно получить с помощью asciidoc. Только вот на практике мне так и не удалось за полдня заставить ни один из конверторов docbook нормально работать с русскими буквами. Также в комплекте asciidoc есть экспериментальный бэкенд latex, но он как-то странно работает с кусками кода, которые мне нужно поместить в tex-файл в неизменном виде (речь идёт о формулах): половина формул куда-то проглатываются.

Кроме всего прочего, мне нужно в доклад включать фрагменты диалога с консольными программами (в данном случае - с maxima и с R). Так как в ходе подготовки доклада что-то может меняться, неохота каждый раз делать copy-paste из консоли. Надо бы, чтобы в исходник вставлять только запросы к программам - а вызываются программы и вставляется вывод пусть автоматически.

В общем, в итоге я написал скрипт lmaxima.py, который делает следующее: читает входной файл, и копирует его в выходной. Если встречает строку вида "program>> команды", то по пайпу передаёт эти команды указанной программе, и её ответ вставляет в выходной файл. Если встречает строку вида "program|tex>> команды" - то указанные команды оборачивает в функцию tex(). Таким образом, lmaxima.py работает как препроцессор для asciidoc. Одна из тонкостей состоит в том, как вставлять в документ формулы, которые выдаёт maxima. Если выводить надо в html, то формулы пропускаются через tex, и в выходной файл вставляется картинка (строка image:chtoto.png[]). Если же выводить надо в pdf, то lmaxima указывается ключ -i, и в выходной файл вставляется непосредственно tex-код.

Т.к. latex-бэкенд к asciidoc работает странно, пришлось писать свой конвертер из подмножества asciidoc-разметки в tex (благо, основная часть разметки asciidoc очень простая). Называется он у меня vsml.py. Заодно vsml.py умеет следующее:

  • С ключом -c - добавляет в документ оглавление (latex-овская команда \tableofcontents),

  • с ключом -p - "выдирает" из исходника только заголовки, и составляет содержание документа (план доклада, в моём случае),

  • с ключом -b - создаёт исходник для презентации (класс beamer); в презентацию попадают заголовки и картинки.

vsml понимает ещё и некоторые "надстройки" над синтаксисом asciidoc. Так, с помощью строчек "//{" и "//}" (asciidoc их воспринимает как комментарии) можно создавать вложенные куски текста. По умолчанию они выводятся как обычно, однако vsml.py можно задать ключ -l с числовым параметром, и он будет выводить только текст с "уровнем вложенности" не больше заданного; это позволяет оформлять более и менее необязательные части текста, и из одного исходника создавать документы разной степени подробности. А с помощью строчки вида "//.Тут заголовок" можно создавать (под)заголовки, которые не будут видны нигде, кроме презентации.

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

all: report.pdf report.html presentation.pdf plan.pdf
clean:
rm report.pdf report.html presentation.pdf
rm presentation.tex report.asciidoc report.vsml
rm plan.tex plan.pdf
plan.pdf: plan.tex
pdflatex $<
plan.tex: report.vsml
vsml.py -p < $< > $@
report.pdf: report.tex
pdflatex $<
presentation.pdf: presentation.tex
pdflatex $<
report.html: report.asciidoc
asciidoc $<
report.asciidoc: math-report
lmaxima.py $< $@
presentation.tex: report.vsml
vsml.py -b < $< > $@
report.tex: report.vsml
vsml.py < $< > $@
report.vsml: math-report
lmaxima.py -i $< $@


PS: мне тут подсказывают: добавь ещё festival, оно за тебя и доклад прочитает :)

воскресенье, ноября 23, 2008

Создание собственных виджетов в PyGTK с помощью cairo

Свободная библиотека Gtk, как известно, не отличается очень большим выбором виджетов. Но никто не мешает создавать свои собственные.

Gtk, как известно, построена на принципах ООП, что хорошо ложится на объектную модель Python. В данном случае это означает, что наследование виджетов естественным образом соответствует наследованию классов в Питоне. Так, создав класс-потомок gtk.VBox, мы получим виджет со всеми свойствами VBox, и сможем добавлять в него нужную функциональность.

Покажу простейший пример. Пусть мы хотим создать виджет, выглядящий как комбинация gtk.Label и gtk.Entry, т.е. поле для ввода сразу с подписью. Чтобы сделать такое непосредственно средствами gtk, нужно создать gtk.HBox, а в него поместить Label и Entry. Т.е. HBox окажется родительским виджетом для всей конструкции. Вот от него и будем наследоваться:

class LabeledEntry(gtk.HBox):

Но наш виджет довольно сильно отличается от простого HBox, поэтому нужно переопределить инициализатор:


  def __init__(self,label=None):
gtk.HBox.__init__(self) # Вызываем инициализатор родительского класса
self.label = gtk.Label(label) # Создаём текстовую метку с нужной подписью
self.entry = gtk.Entry() # И поле для ввода текста
self.pack_start(self.label, expand=False) # Добавляем label в создаваемый виджет
self.pack_start(self.entry, expand=True) # Поле для ввода - туда же

Теперь можно дописывать методы по собственному усмотрению. Например, логично было бы видеть методы set_text и get_text:

  def get_text(self):
return self.entry.get_text()
  def set_text(self,text):
self.entry.set_text(text)

При желании можно добавить, например, get_label и set_label. Пример использования нашего виджета:

...
entry = LabeledEntry("Enter some text")
...

Таким образом, наследуясь от HBox или VBox, можно создавать виджеты, состоящие из нескольких готовых. Но иногда нужны виджеты, внешне не похожие ни на один из стандартных. И вот тогда выручает то, что все виджеты gtk отрисовываются с помощью Cairo, который имеет весьма простой API.

API этот имеет много общего со многими другими рисовальными API. Прежде всего, нужно получить контекст Cairo - объект, содержащий состояние изображения. Далее для собственно рисования вызываются методы этого объекта. Наиболее часто используемые:

  • cr.move_to(x,y) - переместить графический указатель в нужную точку холста,

  • cr.line_to(x,y) - провести линию от положения указателя до данной точки (указатель сдвинется в указанную точку),

  • cr.path_close() - делает текущую линию замкнутой,

  • cr.rectangle(x,y,w,h) - рисует прямоугольник; задаются координаты левого верхнего угла и размеры,

  • cr.set_source_rgb(r,g,b) - выбрать цвет для рисования; компоненты r,g,b измеряются от 0 до 1,

  • cr.stroke() - нарисовать контур текущей линии (выбранным цветом),

  • cr.fill() - закрасить текущую линию.

Координаты измеряются как обычно - от левого верхнего угла вправо и вниз, в пикселах.

Пусть нам, скажем, нужен виджет, который будет отображать простейшие линейные диаграммы. Должна быть возможность добавлять в него данные, а он должен соответственно перерисовывать диаграмму. Такие виджеты удобнее всего наследовать от gtk.DrawingArea:

  class Diagram(gtk.DrawingArea):

Сам виджет DrawingArea выглядит как белый прямоугольник. И на нём, в соответствии с названием, можно рисовать. Пока сделаем инициализацию нашего виджета:

  def __init__(self,max=10,color=(0.8,0.8,0.6)):
gtk.DrawingArea.__init__(self)
self.data = [1] # Это будут данные, отображаемые виджетом
self.max = max # Сколько максимум данных будет рисовать виджет
self.color = color # Цвет диаграммы
# Вот это, можно сказать, самое главное: привязываем рисующую процедуру к событию перерисовки виджета
self.connect('expose-event', self.on_expose)

Определяем собственно метод, который будет отрисовывать виджет:

  def on_expose(self, widget, event):

В аргументе widget передаётся сам виджет. Первое, что нам от него нужно - это размеры и положение:

    x,y, width,height,_ = widget.window.get_geometry()

Кроме того, нам понадобится контекст Cairo:

    cr = widget.window.cairo_create()

Вычислим некоторые размеры:

    xpad = 0.03*self.width           # Поля по горизонтали
ypad = 0.07*self.height # И по вертикали
w = float(self.width-2*xpad) # Ширина 'рабочей' части виджета
h = float(self.height-2*ypad) # и высота
M = max(self.data) # Максимум данных - он нужен, чтобы выставить масштаб по оси Y
n = len(self.data) # Количество данных
    cr.rectangle(0,0,self.width,self.height)   # Обозначаем прямоугольник, закрывающий весь виджет
cr.set_source_rgb(1,1,1) # Выбираем белый цвет
cr.fill() # Закрашиваем наш прямоугольник - это будет фон
    cr.move_to(xpad, ypad+h-h*float(self.data[0])/M)  # Ставим указатель в верхний левый угол будущей диаграммы
for x,y in enumerate(self.data[1:]): # Пробегаемся по всем данным
cr.line_to(xpad+w*float(x+1)/(n-1), ypad+h-h*float(y)/M) # Проводим очередной отрезок ломанной
cr.line_to(xpad+w, ypad+h) # Проводим правую границу диаграммы
cr.line_to(xpad,ypad+h) # Теперь нижнюю границу
cr.close_path() # Замыкаем ломанную - это проведёт левую границу диаграммы
cr.set_source_rgb(*self.color) # Выбираем цвет
cr.fill() # Закрашиваем ломанную

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

Ну и допишем метод для добавления данных в диаграмму:

def accept(self,n):
if len(self.data) == self.max:
del self.data[0] # Если данных слишком много - забываем самое старое значение
self.data.append(float(n)) # Добавляем число в список
self.queue_draw() # Этот вызов заставит виджет перерисоваться, т.е. лишний раз вызовет on_expose().

Пример использования:

...
dg = Diagram(max=20)
...
dg.accept(10)
dg.accept(20)
...

понедельник, ноября 10, 2008

Typewriter-like раскладки

По совету http://vonderer.blogspot.com/, решил попробовать использовать для русского языка раскладку пишущей машинки. Главное преимущество (для меня) - в том, что знаки препинания обычно в тексте встречаются гораздо чаще, чем цифры, а в typewriter набирать их становится проще. Заодно точка и запятая получают по отдельной клавише, и буква Ё - более удобное место. Вобщем, действительно, удобно. Правда, на привыкание ушло около недели.

Но кроме русской раскладки есть ещё и английская. Для неё в X-ах не предусмотрено tyewriter-варианта, а хочется, потому что на переключение режима в мозгах требуется слишком много времени (цифры набирать то с шифтом, то без, и знаки препинания скачут по всей клавиатуре). Раскладка Дворака (у которой есть вариант с цифрами на верхнем уровне) - слишком другая, а я не так много набираю англоязычных текстов, чтобы изучать совсем новую раскладку (да ещё и надписи на клавишах будут мешать). Вобщем, сделал я себе typewriter-вариант английской раскладки. Выглядит это так:



Соответствующий код (/usr/share/X11/xkb/symbols/ustw):

partial alphanumeric_keys
xkb_symbols "typewriter" {
include "us(basic)"
name[Group1]= "US - Typewriter";
key <AE01> { [exclam, 1 ] };
key <AE02> { [numbersign,2 ] };
key <AE03> { [slash, 3 ] };
key <AE04> { [semicolon, 4 ] };
key <AE05> { [colon, 5 ] };
key <AE06> { [comma, 6 ] };
key <AE07> { [period, 7 ] };
key <AE08> { [asterisk, 8 ] };
key <AE09> { [question, 9 ] };
key <AE10> { [percent, 0 ] };
key <BKSL> { [parenleft, parenright ] };

key <AC10> { [at, ampersand ] };
key <AB08> { [asciicircum, less ] };
key <AB09> { [dollar, greater ] };
key <AB10> { [bar, backslash ] };
};


Кроме того, ещё с давних пор я использую CapsLock как специальный модификатор, превращающий некоторые буквенные клавиши в стрелки итп. Сейчас ещё захотелось на Shift-Caps повесить переключение такого режима (чтоб в браузере тексты читать, листая кнопками j/k, итп). И ещё захотелось временный переключатель из русской раскладки в английскую - иногда >/< или ещё чего набрать быстро. И, раз уж пошла такая пьянка, чтоб можно было греческие буквы побыстрее набирать (временный переключатель в греческую раскладку) (правда, я не верю, что греки пользуются фонетической раскладкой, которая в иксах под именем gr, ну да это их проблемы).

Итак, текущие мои настройки, если кому интересно.

/usr/share/X11/xkb/symbols/addkeys - мои раскладки:


partial alphanumeric_keys
xkb_symbols "en" {
include "ustw"
name[Group1]= "US - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ a, A, Home, Home ] };
key <AD03> { [ e, E, End, End ] };
key <AC05> { [ g, G, Home, End ] };
key <AC06> { [ h, H, Left, Left ] };
key <AC07> { [ j, J, Down, Down ] };
key <AC08> { [ k, K, Up, Up ] };
key <AC09> { [ l, L, Right, Right ] };
key <AC03> { [ d, D, Delete, Delete ] };
key <AD10> { [ p, P, XF86ScrollUp, XF86ScrollUp ] };
key <AB06> { [ n, N, XF86ScrollDown, XF86ScrollDown ] };
include "addkeys(caps_switch)"
};

partial alphanumeric_keys
xkb_symbols "ru" {
include "ru(typewriter)"
name[Group1]= "Russia - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ Cyrillic_ef, Cyrillic_EF, Home, Home ] };
key <AD03> { [ Cyrillic_u, Cyrillic_U, End, End ] };
key <AC05> { [ Cyrillic_pe, Cyrillic_PE, Home, End ] };
key <AC06> { [ Cyrillic_er, Cyrillic_ER, Left, Left ] };
key <AC07> { [ Cyrillic_o, Cyrillic_O, Down, Down ] };
key <AC08> { [ Cyrillic_el, Cyrillic_EL, Up, Up ] };
key <AC09> { [ Cyrillic_de, Cyrillic_DE, Right, Right ] };
key <AE11> { [ minus, underscore, emdash, hyphen ] };
key <AE12> { [ equal, plus, notequal, plusminus ] };
key <AC03> { [ Cyrillic_ve, Cyrillic_VE, Delete, Delete ] };
key <AD11> { [ Cyrillic_ha, Cyrillic_HA, bracketleft, braceleft ] };
key <AD12> { [Cyrillic_hardsign,Cyrillic_HARDSIGN, bracketright, braceright ] };
key <AD10> { [ Cyrillic_ze, Cyrillic_ZE, XF86ScrollUp, XF86ScrollUp ] };
key <AB06> { [ Cyrillic_te, Cyrillic_TE, XF86ScrollDown, XF86ScrollDown ] };
include "addkeys(caps_switch)"
};

partial alphanumeric_keys
xkb_symbols "gr" {
include "gr"
name[Group1]= "Greek - Additional";
key.type[group1]="FOUR_LEVEL";
key <AC01> { [ Greek_alpha, Greek_ALPHA, Home, Home ] };
key <AD03> { [ Greek_epsilon, Greek_EPSILON, End, End ] };
key <AC05> { [ Greek_gamma, Greek_GAMMA, Home, End ] };
key <AC06> { [ Greek_eta, Greek_ETA, Left, Left ] };
key <AC07> { [ Greek_xi, Greek_XI, Down, Down ] };
key <AC08> { [ Greek_kappa, Greek_KAPPA, Up, Up ] };
key <AC09> { [ Greek_lambda, Greek_LAMBDA, Right, Right ] };
key <AC03> { [ Greek_delta, Greek_DELTA, Delete, Delete ] };
include "addkeys(caps_switch)"
};

xkb_symbols "caps_switch" {
key <CAPS> {
type[Group1]="ONE_LEVEL",
symbols[Group1] = [ ISO_Level3_Shift ]
};
modifier_map Mod5 { ISO_Level3_Shift };

replace key <II65> {
type[Group1]="ONE_LEVEL",
actions[Group1] = [ SetGroup(group=3) ],
actions[Group2] = [ SetGroup(group=3) ],
actions[Group3] = [ ],
actions[Group4] = [ SetGroup(group=3) ]
};

replace key <I21> {
type[Group1]="ONE_LEVEL",
type[Group2]="ONE_LEVEL",
actions[Group1] = [ SetGroup(group=1) ],
actions[Group2] = [ SetGroup(group=1) ],
actions[Group3] = [ SetGroup(group=1) ],
actions[Group4] = [ ]
};

replace key <RCTL> {
actions[Group1] = [ SetGroup(group=2) ],
actions[Group2] = [ SetGroup(group=1) ],
actions[Group3] = [ SetGroup(group=1) ],
actions[Group4] = [ ],
locks = yes
};
};


Ну и в /etc/X11/xorg.conf:


Option "XkbLayout" "addkeys(en),addkeys(ru),gr"
Option "XkbOptions" "grp_led:caps,compose:ralt"



Рус/лат переключается правым Ctrl, индикация лампочкой Caps. Временный переключатель в английскую раскладку на клавише <I21> (у меня она рядом с левым Ctrl). На клавише <II65> (у меня над <I21>) - временный переключатель в третью раскладку (греческие буквы иногда набрать). По Caps+буква - некоторые спецклавиши: Caps-hjkl - стрелки, Caps-a - Home, Caps-e - End, Caps-g - Home, Caps-G - End. На правом Alt - Compose.

четверг, июня 19, 2008

Deployment и Git

Сперва - что такое deployment?

Буквальный перевод - развертывание. Речь идет о том, чтобы заставить код, написанный разработчиком в своей песочнице, заставить работать в реальных условиях на "боевых серверах". И вот во что это выливается даже в случае с одним разработчиком:

Мой нынешний проект я разрабатываю/тестирую на своем домашнем сервере (dev-сервер, это называется). А недавно выложил на хостинг - это, так сказать, production-сервер (ну, на самом деле, это пока что разновидность тестирования). И тут появляются некоторые ньюансы:

  • на dev- и production- серверах нужны разные настройки кода (например, разные пароли для коннекта к БД);

  • на dev-сервере я продолжаю разработку, добавляю новые фичи и пр. Хотелось бы, чтобы новые фичи и в production-варианте появлялись;

  • на production-сервере вылезают некоторые баги, которые по разным причинам на dev не вылезали, их приходится фиксить. Хотелось бы, чтобы эти же баги были пофиксены и в dev-версии;

  • Т.к. код еще не доведен "до кондиции", при переносе выясняется, что кое-где он написан неуниверсально (зависит от специфики того места, где работает). Для работы production-сервере его приходится после выкладывания обобщать. Конечно, нужно, чтобы и в dev-версии он был не менее общим;

  • Некоторые изменения, сделанные в коде production-версии, всё-таки специфичны именно для данного конкретного хостинга. Они в версии на dev-сервере мне совсем не нужны;

  • Кроме всего этого, я хочу держать актуальную версию кода в публичном месте, чтобы любой желающий мог его скачать.

Несложно догадаться, что при попытке выполнять все эти требования "вручную" - очень быстро запутаешься в трех соснах (то бишь, версиях). Для упрощения deployment-а существует довольно много всяких разных решений. Одно из возможных - использовать Git.

Предположим, код на dev-сервере уже под контролем git. Тогда для разворачивания проекта делаем:

user@production.server:/project$ git init
user@dev.server:~/project$ git push ssh://production.server/project master

Собственно, теперь на production.server в директории /project/ имеем копию кода с dev-сервера. Далее:

user@production.server:/project$ git branch production
user@production.server:/project$ git checkout production

Это мы создали новую ветвь репозитория и переключились в нее. Теперь изменяем настройки, тестируем, фиксим баги… не забываем после каждого логически завершенного изменения делать "git commit -a" - фиксировать изменения в репозитории.

При внесении изменений в код на dev-сервере их, конечно, тоже фиксируем. Когда захотим обновить версию на production, делаем:

user@dev.server:~/project$ git push
user@production.server:/project$ git merge master

— вливаем изменения в ветви master в production. Если нужно применить только последнее изменение в master (последний коммит), вместо git merge делаем git cherry-pick.

Чтобы применять нужные изменения, сделанные на production, к dev-версии, делаем:

user@dev.server:~/project$ git remote add prod ssh://production.server/project

— создаем ссылку на удаленный репозиторий,

user@dev.server:~/project$ git fetch prod/production

— получаем код из ветви production,

user@dev.server:~/project$ git cherry-pick prod/production

— это если нужно применить последнее изменение в production, или

user@dev.server:~/project$ git cherry-pick идентификатор-коммита

— если нужно применить произвольный коммит.

Системы контроля версий. Git.

Решил всё-таки написать некое введение в тему, чтобы бы было куда давать ссылки ;) Примечание: информация расчитана на не очень опытных разработчиков. Профессионалам, думаю, все освещенные здесь вопросы покажутся тривиальными. Товарищам, знакомым с контролем версий, но не знакомым с Git, предлагаю первые разделы пропустить.

Что такое управление версиями и зачем оно?

Итак, предположим, вы разрабатываете программу. Для простоты сначала предположим, что вы разрабатываете ее в одиночку и для собственного удовольствия. Как это выглядит?

Создаем директорию MyApplication, в ней - собственно файлы программы (скажем, *.c). Далее начинается итерационный процесс: написали некоторое количество кода → скомпилировали → запустили → обнаружили ошибки или недоработки → отредактировали исходники → … Всё просто, в целом. Но через некоторое время начинаются нетривиальные вещи. Например:

  • Последнее изменение сделало программу хуже, чем она была до того. Хочется вернуть всё "как было". Хорошо, если не успели закрыть редактор - делаем Undo. А если закрыли?

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

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

Первое, что приходит в голову для решения всех этих проблем - резервные копии. Скажем, каждую работающую версию программы упаковывать в MyApplication_v0.001.tar.gz, в случае чего - откатываться. Для подготовки к релизу - сделать копию директории MyApplication, в которой только фиксить баги, а в основной - добавлять функционал. При этом подходе через некоторое время разработки у вас появится куча *.tar.gz и не меньше копий рабочей директории. Очень быстро вы начнете путаться, где что.

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

  • Вы добавили в программу (в свою копию) возможность A, а ваш товарищ (в свою) - возможность B. Хотелось бы, чтобы у обоих была версия с обоими возможностями.

  • Вы добавили в программу некоторую функцию, ваш товарищ добавил ее же, но реализовал по-другому. Какую версию выпускать и поддерживать дальше?

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

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

Все эти проблемы призваны решить специальные программы - системы контроля версий (version control systems, vcs).

Общие свойства VCS

Если вы работаете с vcs, то у вас есть место, где хранится вся история изменений в программе - это место называется репозиторий (repository). Для внесения изменений в программу вам нужно получить копию текущей разрабатываемой версии - она называется вашей рабочей копией (working copy). Процесс получения рабочей копии традиционно называется словом "чекаут" (checkout). В случае коллективной разработки, вам придется поддерживать актуальность вашей рабочей копии (проверять, что там остальные наработали) - это обычно называется "апдейт" (update). Чтобы ваши изменения попали в репозиторий, вам нужно сделать коммит (commit) (еще это называется словом checkin). Часто словом commit называют собственно сам набор изменений, зафиксированный этой операцией (более точный термин - changeset). Каждое зафиксированное изменение называют ревизией. Вы можете в любой момент откатить свою рабочую копию к любой предыдущей ревизии - это называется revert.

Для того, чтобы отслеживать такие вещи, как релизы, или варианты рефакторинга программы, или тестировать версии с новыми функциями - в репозитории создаются ветви разработки (branches). Для согласования хода разработки в разных ветках (она может вестись, и обычно ведется, параллельно) время от времени делают слияние веток (merge) - когда к одной ветке применяются те же изменения, что были проделаны в другой. Если какое-то место какого-то файла было по-разному изменено в разных ветках, при слиянии возникает конфликт (merge conflict) - его приходится решать вручную, выбирая один из вариантов или как-то логично их объединяя.

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

Автоматизация всех перечисленных операций - дело VCS.

Централизованные vs распределенные VCS

Всё множество VCS можно разделить на два класса - централизованные и распределенные.

В случае с централизованной VCS репозиторий хранится на одном сервере, и все разработчики работают с ним. Очевидное преимущество: простое управление выпуском релизов и вообще ходом развития программы, раз весь код в одном месте. Очевидный недостаток: если с сервером что-то случится, работа всех разработчиков пропадет (даже в случае регулярных бэкапов - пропадет работа всех разработчиков, скажем, за последнюю неделю). Известные примеры централизованных vcs - CVS, Subversion, Perforce.

В случае с распределенной VCS, каждый разработчик имеет полную копию репозитория и работает с ним, время от времени синхронизируясь с репозиториями остальных. Таким образом, для организации целенаправленной разработки потребуются какие-то не технические, а организационно-административные средства. Зато получаем множество преимуществ:

  • Раз у каждого есть полная копия репозитория, работа всех разработчиков пропасть не может вообще.

  • Часто выполняемые операции - прежде всего, commit - происходят почти мгновенно, т.к. не требуют соединения по сети.

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

  • Т.к. в распределенных VCS предполагается регулярная синхронизация репозиториев, в них гораздо более эффективно реализована операция слияния веток (здесь это одна из базовых операций, в отличие от централизованных VCS, где это делается нечасто).

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

Git

Git - это одна из распределенных VCS (distributed version control system, dvcs). Другими примерами dvcs являются Mercurial, Bazaar, Monotone.

Git выделяется на их фоне главным образом из-за того, что он применяется для разработки ядра Linux (http://www.kernel.org). Собственно, именно для этого Git и разрабатывался. Ядро Linux - весьма немаленький проект, как по объему исходного кода, так и по числу участников, поэтому при его разработке большое внимание было уделено производительности и легкости слияния веток.

Далее я привожу часто используемые команды git. Я опускаю кучу тонкостей и многие возможности Git. Всё это можно посмотреть в документации (git help имя-команды, например git help commit). Здесь - скорее отправная точка, свод основных возможностей Git.

Для начала работы с Git вам понадобится репозиторий. Его можно либо создать с нуля, либо клонировать какой-нибудь существующий репозиторий.

$ cd project
$ git init

— создаем пустой репозиторий.

$ git clone git://remote.server/repo.git

— клонируем существующий репозиторий. Кроме собственного протокола, git поддерживает http://, ssh:// и пр.

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

Если просто положить какие-нибудь файлы в директорию проекта, они сами собой не попадут под контроль Git. Чтобы Git учитывал изменения в них, их нужно добавить в репозиторий:

$ git add имя-файла-или-директории

Некоторые файлы не принято держать под контролем версий. Прежде всего, это перегенерируемые файлы, временные файлы, файлы журналов итп. Чтобы Git их вообще не замечал, нужно в корневой директории проекта создать файл .gitignore, и в него вписать список игнорируемых файлов, по одному на строку. Можно использовать маски - например, *.pyc. Сам файл .gitignore нужно добавить в репозиторий (git add .gitignore).

Чтобы Git учел изменения в файле, его нужно заново добавить (git add).

Коммит (фиксация изменений в репозитории) делается командой

$ git commit

При этом вам будет предложено ввести "сообщение коммита" (commit message) - пояснение, что вы изменили и зачем.

Чтобы предварительно учесть все изменения в файлах, нужно добавить ключ -a:

$ git commit -a

Если сразу после коммита вы обнаружили, что что-то забыли включить в него, можно поправить:

$ git commit --amend      # Добавить последние изменения в последний коммит, "заодно".

Особенность Git состоит в том, что ревизии (коммиты) обозначаются не номерами, как, например, в svn или cvs, а 40-значными идентификаторами типа a21b96…. Идентификаторы можно сокращать до первых нескольких знаков, лишь бы они однозначно определяли коммит. Кроме того, если нужный вам коммит помечен тегом, то можно использовать имя тега вместо идентификатора коммита. На последний коммит в текущей ветке можно также ссылаться по имени HEAD, на предпоследний - HEAD^, пред-предпоследний - HEAD^^ итп. Существуют и другие способы обозначить нужный коммит, подробности см. в документации.

Увидеть, какие файлы были изменены, добавлены итп после последнего коммита можно командой

$ git status

Сами изменения, сделанные с момента последнего коммита, можно посмотреть командой

$ git diff

У этой команды есть варианты: git diff <идентификатор> - посмотреть изменения с указанного коммита по текущее состояние, git diff <идентификатор1> <идентификатор2> - различия между двумя ревизиями.

Посмотреть журнал последних коммитов - git log.

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

$ git pull

Если вам разрешено вносить изменения в исходный репозиторий (это называется push access, предполагает договоренность с владельцем репозитория, и, в зависимости от протокола доступа (ssh или еще что-то), какие-то пароли итп), это делается командой

$ git push

При этом ваши изменения вливаются в репозиторий, который вы клонировали.

Пометить только что созданный коммит можно командой

$ git tag имя-тега

Любой другой коммит помечается так:

$ git tag имя-тега идентификатор-коммита

Посмотреть список веток в репозитории -

$ git branch

(изначально создается одна ветка под названием master).

Создать новую ветку -

$ git branch имя-ветки

или

$ git branch имя-ветки идентификатор-коммита-начинающего-ветку

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

$ git checkout имя-ветки

С тем же успехом этой командой можно переключиться на любой коммит - git checkout идентификатор-коммита.

Влить изменения из другой ветки в текущую -

$ git merge имя-ветки-с-которой-сливаем

Можно взять из другой ветки один конкретный коммит (например, багофикс) -

$ git cherry-pick идентификатор-коммита

воскресенье, июня 15, 2008

Всякое разное: Drupal, Git, Django...

Давно я что-то не писал постов… За это время в голове много чего накопилось, теперь попытаюсь изложить.

Drupal

Вот, за прошедшее время успел более-менее поглубже изучить эту штуку. Кто не слышал - это CMS, а точнее, CMF (content management framework) такой. Я когда-то давно на него смотрел и даже сайты на нем делал, но по принципу "поставил движок, настроил - сдал". Недавно вот чуть посложнее сайты делать пришлось, даже модули к нему писал. Вобщем, drupal хоть и на PHP, но мне понравился ;) Чем именно:

  • посмотрел код - на редкость для php-приложений чистый и понятный;

  • система прав на основе ролей;

  • куча модулей, под каждую задачу, прямо unix-way;

  • эти модули на удивление хорошо дружат меджу собой, конфликтов почти нет;

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

  • удивительно вменяемое русскоязычное сообщество на drupal.ru.

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

Git

До недавнего времени я весь свой программный код хранил в svn. Впрочем, это в основном были совсем небольшие программки. А тут появился на горизонте некий более крупный проект, который предполагалось разрабатывать командой (по крайней мере - не в одиночку). И именно в этот момент появился пост Ивана Сагалаева - жалоба на проблемы с merge в subversion. Мне почему-то сразу расхотелось использовать svn :) Стал смотреть на альтернативы, благо их довольно много. Git приглянулся во-первых своей распределенностью со всеми вытекающими, во-вторых - это мейнстрим (среди dvcs), что ни говори ;). Вобщем, стал изучать. Больше всего понравилось:

  • репозиторий создается фактически одной командой (git init), второй командой в него добавляются файлы (git add .), ну и третьей делается первый коммит (git commit). Получается быстрее, чем с subversion, так что имеет смысл использовать его даже для программы из пары файликов или конфига.

  • собственно факт распределенности, git получается удобнее использовать, когда команда разбросана по городу и не у всех не всегда даже интернет есть.

  • графическая морда, gitk, хорошо показывает ветви, коммиты и всё остальное.

Вобщем, перевел я весь свой код на git, благо тулза для импорта из svn в git есть (git-svn, так и называется).

Django

Эту вещь я когда-то уже тоже изучал, и даже что-то писал, но уже даже не помню, что писал :) Блог, кажется, по традиции :) Сейчас вот изучаю заново (в придачу, с предыдущего подхода многое в django успело поменяться). Кто еще про django не слышал (а тут такие есть? :D) - это фреймворк (т.е., по большому счету, набор библиотек + определенный способ их использования) для создания веб-приложений на python.

Сейчас активно пишу на django что-то типа groupware, назвал пока незамысловато - Projects (надо будет еще придумать подходящее название). Описание тут: http://iportnov.ru/projects/projects.

Еще до этой штуки стал писать библиотечку, которую пока назвал HMS (hooks modules system). Это, собственно, эксперимент: питоновский пакет, с помощью которого в любое питон-приложение можно добавить систему модулей, работающую "примерно как в друпале" (см. выше, про хуки). По-моему, оно довольно красиво смотрится в связке с Django. Сейчас у меня эта HMS входит в состав Projects, хотя пока и мало используется там (я пока что в ядре не все функции написал, не до модулей пока).

Еще в комплекте с Projects у меня есть pygit - веб-интерфейс к Git, написанный на django. При желании его можно почти безболезненно выдернуть из Projects и использовать в других проектах. Для связи с Git использует пакет GitPython. Вот только в этом gitpython есть пока баги, и тормозит он в некоторых местах… Ну, баги, надеюсь, скоро починят, а тормоза можно и стороной обойти.

вторник, мая 20, 2008

Сделал себе домашнюю страничку :)

Страница на narod.ru давно и безнадежно устарела, обновлять ее неудобно (ftp), так что решил сделать себе новую, более "основательную". К тому же давно хотел выложить куда-нить в svn свои проектики, на googlecode кучу проектов заводить не хотелось. Так что решил взять себе vds, поставить на него всё что надо. Возможно, еще какие-нибудь сайты вскоре на том же vds появятся. Вобщем, см. сюда: http://iportnov.ru.
А тут еще образовались проблемы с хостингом у нашего LUG-а, так я его тоже у себя приютил (благо, посещаемость небольшая, ресурсов хватает): http://lug-mgn.ru.
Оба сайта - на Drupal.

среда, марта 19, 2008

Доклад по Python: часть III

9. Функциональное программирование.

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

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


def add(x,y):
return x+y

inc = lambda x: add(x,1)

def incrementer(n):
return lambda x: add(x,n)

inc(3) # выдаст 4

f = incrementer(3)

f(5) # выдаст 8.

Другие типичные конструкции, позаимствованные из функциональных языков - это стандартные функции map,filter,zip и специальная форма, называемая списочным сокращением (list comprehension).

Функция map(f,list) возвращает список значений, полученный применением функции f к каждому элементу списка list. Пример:

>>> def sqr(x):
... return x*x
...
>>> map(sqr,[1,2,3])
[1, 4, 9]


Функция filter(p,list) возвращает список из только тех элементов списка list, для которых функция p возвращает истину. Пример:


>>> def even(n):
... return n%2 == 0
...
>>> filter(even,[1,2,3,4,5,6])
[2, 4, 6]

Функция zip принимает два (или больше) списка и возвращает список пар (кортежей), составленных из соответствующих элементов списков:

>>> zip([1,2,3,4],["A","B","C","D"],['x','y','z','t'])
[(1, 'A', 'x'), (2, 'B', 'y'), (3, 'C', 'z'), (4, 'D', 't')]

List comprehensions позволяют одной строкой создать списки по какому-нибудь правилу:

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

Можно тут же отбирать нужные элементы списка:

>>> [x*x for x in [1,2,3,4,5] if even(x)]
[4, 16]

Можно перебирать одновременно несколько последовательностей:

>>> l = [1,2,3,4,5]
>>> [x*y for x in l for y in l]
[1, 2, 3, 4, 5, 2, 4, 6, 8, 10, 3, 6, 9, 12, 15, 4, 8, 12, 16, 20, 5, 10, 15, 20, 25]

10. Декораторы.

Декораторы - это одна из самых мощных особенностей Питона (особенно в сочетании с интроспекцией). В простейшем случае декоратор - это функция, принимающая в качестве аргумента функцию и возвращающая новую функцию.

Применяются декораторы так:

@decor
def f(x,y):
...

и это эквивалентно такой записи:

def f(x,y):
...
f = decor(f)

Классический пример использования декораторов - это отслеживание вызовов функции:
def trace(func):
name = func.func_name # имя переданной функции
def wrapper(*args,**kwargs): # создаем новую функцию
print "Function called: %s(%s,%s)" % (name,args,kwargs)
result = func(*args,**kwargs) # вызываем исходную функцию
print "Function %s returned %s" % (name,result)
return result
return wrapper # возвращаем созданную функцию

@trace
def f(x,y):
return x*y+2

Обычно декоратор выполняет какую-то работу дополнительно к тому, что делает сама функция. Например, в веб-фреймворке Django есть декоратор login_required - он проверяет, что пользователь уже авторизовался, и только в этом случае вызывает исходную функцию.

Таким образом, декораторы в Питоне обеспечивают возможность аспектно-ориентированного программирования.

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

Собственно функция-декоратор должна принимать только один аргумент - исходную функцию. Но можно писать функции, которые возвращают декораторы:

def print_on_call(text):
def decorator(func):
def wrapper(*args,**kwargs):
print ">> "+text
return func(*args,**kwargs)
return wrapper
return decorator

@print_on_call("F called!")
def f(x):
...

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

def delegate(cls):
def decorator(meth):
name = meth.func_name
def wrapper(*args,**kwargs):
m = object.__getattribute__(cls,name)
result = m(*args,**kwargs)
return result
return wrapper
return decorator

class A(object):
x = 0
def two(self,z):
r = self.x*z
self.x = z
return r

class B(object):
x = 3

@delegate(A)
def two(self,z):
pass

b = B()
print b.two(5) # Выводит 15

11. Дескрипторы

Дескриптор — это класс с определенными методами __get__() и __set__(). Предполагается, что __get__() возвращает значение экземпляра, а __set__() — соответственно, устанавлнивает.

Особенность дескрипторов состоит в том, что они нормально работают только как атрибуты классов.

В Питоне есть стандартный класс-дескриптор property, конструирующий свойство объекта из getter-а и setter-а. Типичный пример использования property:


class A(object):
def getx(self):
print "Getter called."
return self._x

def setx(self,value):
print "Setting .x to %s" % value
self._x = value

x = property(getx,setx)
Пользовательские классы-дескрипторы могут выполнять более сложную работу: например, можно хранить количество присваиваний (или более сложное состояние) внутри класса-дескриптора.

Таким образом, дескрипторы - это значительное обобщение свойств (properties), имеющихся в C#.

12. Метаклассы

В Питоне всё есть объект. В том числе, классы - это тоже объекты.
Самые часто используемые объекты — это экземпляры классов. А классы, в свою очередь, являются экземплярами метаклассов.

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

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

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

>>> class Meta(type):
... def whoami(cls):
... print "I'm a", cls.__name__
...
>>> class A(object):
... __metaclass__ = Meta # Указываем используемый метакласс
... def do_something(self):
... print "Doing something."
...
>>> a = A()
>>> A.whoami()
I'm a A
>>> a.do_something()
Doing something.

Также можно переопределять процесс создания классов:

>>> class Meta2(type):
... def __new__(cls,name,bases,dct): #Конструктор
... print "Creating class",name
... return type.__new__(cls,name,bases,dct)
... def __init__(cls,name,bases,dct): #Инициализатор
... print "Init'ing class", name
... super(Meta2,cls).__init__(name,bases,dct)
... cls.x = 25 # Добавляем атрибуты к классу
... cls.y = 30
...
>>> class B(object):
... __metaclass__ = Meta2
... def f(self):
... print "Calling F."
...
Creating class B
Init'ing class B
>>> b = B()
>>> b.x
25
>>> b.f()
Calling F.

Таким образом, метаклассы позволяют часть логики каждого класса вынести за пределы самого класса. Это еще одна возможность для аспектно-ориентированного подхода.

Доклад по Python: часть II

3. Объекты.

Питон - это объектно-ориентированный язык. Среди всего прочего, это означает: всё есть объект.
В C++, на примере которого (к сожалению) обычно обучают объектно-ориентированному программированию, объектами являются только экземпляры классов. Числа, например, объектами не являются.
В Java значения атомарных типов тоже являются объектами, но несколько искуственно: для них создаются так называемые boxed значения, то есть для каждого (например) числа создается экземпляр спецциального класса, содержащий это число.
В Питоне же всё является объектом. Например: экземпляры классов, собственно классы, типы, атомарные объекты (числа и строки), а также функци. Пример с числом:

>>> a = 1
>>> a.__str__() # этот метод дает строковое представление объекта
'1'

Функции принимают в качестве аргументов объекты и возвращают объекты. Например, функция может принимать функцию в качестве аргумента и возвращать класс, или принимать строку и возвращать функцию.

Рассмотрим помаленьку все традиционные принципы ООП.


4. Инкапсуляция.

Инкапсуляция подразумевает: алгоритмы работы с данными хранятся вместе с данными. Для атомарных значений мы это уже видели в предыдущем разделе (a.__str__()). Для экземпляров пользовательских классов это реализуется образом, очень похожим на C++ или Java:


class A(object): # object - класс, стоящий в вершине иерархии наследования
x = 0
y = 0

def move(self,x,y):
self.x = x
self.y = y

a = A()
a.move(2,3)


Здесь можно увидеть, что в Питоне указатель на экземпляр класса передается в методы явным образом, как первый аргумент (из стандарта С++ известно, что там это реализовано так же, только этот первый аргумент явно не выписывается). Здесь видно следование первому принципу философии Питона: явное лучше неявного.

Главное в инкапсуляции, как мы увидим позже - не надо путать инкапсуляцию и сокрытие данных.


5. Наследование.

Наследование подразумевает возможность создания классов объектов, ведущих себя почти также, как "родительский" класс, но "немного по-другому". В простейшем случае реализация наследования в Питоне похожа на C++:


class A(object):
x = 0

class B(A):
y = 1


Возможно и множественное наследование: class A(B,C,D):...

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

Эта проблема решается различными способами. В С++ для этого используется множественное наследование. В Java - интерфейсы. В Ruby - mix-ins (примеси).

В Питоне используется концепция, называемая Duck Typing: «Если ЭТО ходит, как утка, и крякает, как утка - значит, это утка». То есть, если у объекта есть все нужные функции свойства и методы, то он подходит в качестве аргумента. Например, в функцию


def f(x):
return x.get_value()


можно передавать объект любого типа, лишь бы у него был метод get_value().

Еще одна типичная проблема, возникающая в связи с множественным наследованием - не всегда очевидно, в каком порядке будут просматриваться родительские классы в поисках нужного свойства или метода. В Питоне для упрощения этой проблемы у каждого класса есть свойство __mro__ (method resolution order):

>>> class A(object): pass
...
>>> class B(object):
... x = 0
...
>>> class C(A,B):
... z = 3
...
>>> C.__mro__
(<class 'C'>, <class 'A'>, <class 'B'>, <type 'object'>)


6. Полиморфизм.

Полиморфизм - это способность функции работать с аргументами разных типов.
В C++ и Java полиморфизм тесно связан с наследованием. Например, в C++, если объявлено, что функция f принимает экземпляр класса A, то она может принимать экземпляр любого класса, унаследованного от A. В Java это поведение расширено: за счет интерфейсов (interfaces) есть возможность передавать в функцию экземпляры классов, не связанных "генетически" (но реализующих один интерфейс).

В Питоне полиморфизм реализован за счет Duck Typing: любая функция может принимать объекты любого типа, но если она попытается использовать свойства, которых у данного объекта нет, возникнет исключение (exception) (функция может перехватить его в конструкции try...except и сделать в этом случае что-то другое). За счет этого, например, функция, работающая с файлами, может принимать в качестве аргумента имя файла или дескриптор открытого файла - и действовать по обстоятельствам.

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


7. Интроспекция.

Про этот принцип регулярно забывают, когда рассказывают об ООП на примере С++, а между тем это один из основополагающих принципов.

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

У каждого объекта есть некоторое количество атрибутов. Атрибуты, имена которых, начинаются с подчеркивания, считаются приватными (private), хотя это и не влияет на область видимости - это только соглашение. "Более приватными" являются атрибуты, имена которых начинаются с двух подчеркиваний - снаружи они винды только как __имя-объекта__имя-атрибута__. Атрибуты, начинающиеся с двух подчеркиваний и заканчивающиеся двумя подчеркиваниями, имеют специальный смысл.

Список всех атрибутов любого объекта можно получить с помощью встроенной функции dir:

>>> a = 1
>>> dir(a)
['__abs__', '__add__', '__and__', '__class__', '__cmp__', '__coerce__', '__delattr__', '__div__', '__divmod__', '__doc__', '__float__', '__floordiv__', '__getattribute__', '__getnewargs__', '__hash__', '__hex__', '__init__', '__int__', '__invert__', '__long__', '__lshift__', '__mod__', '__mul__', '__neg__', '__new__', '__nonzero__', '__oct__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdiv__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__str__', '__sub__', '__truediv__', '__xor__']

Объект, имеющий атрибут __call__, можно вызывать как функцию (собственно, функции в Питоне отличаются от остальных объектов только наличием этого атрибута). Для проверки, можно ли использовать объект как функцию, используется стандартная функция callable(f). Таким образом, методы объекта - это атрибуты, которые можно вызывать.

У функций и классов есть атрибут __doc__, содержащий так называемый docstring - строку документации. При описании функции она пишется на отдельной строке после def, при описании класса - после class. Стандартная функция help() выдает информацию о любом объекте.

Атрибут __name__ любого объекта содержит его имя. У экземпляров классов атрибут __class__ содержит ссылку на класс этого объекта.

Стандартная функция type() возвращает тип объекта (тип - это тоже объект).

С помощью функции isinstance(obj,cls) можно выяснить, является ли объект экземпляром данного класса (или одного из дочерних классов). А функция issubclass(cls1,cls2) выясняет, является ли cls1 потомком cls2.

Модуль inspect, входящий в стандартную поставку Питона, содержит некоторые дополнительные возможности интроспекции. В частности, функция inspect.getargspec(func) сообщает, сколько и каких аргументов ожидает получить функция.


8. Динамизм.

Этот принцип не был сформулирован как один из основных для ООП, однако референсная реализация ООП - Smalltalk - этим свойством обладает.

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

>>> class A(object):
... pass

>>> a = A()
>>> a.x = 25 # создаем новый атрибут объекта
>>> b = A() # другой экземпляр того же класса
>>> print b.x # вызовет исключение: у объекта b нет атрибута x
>>> b.y = 30 # создаем другой атрибут
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'x']
>>> dir(b)
['__class__', '__delattr__', '__dict__', '__doc__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__str__', '__weakref__', 'y']

Можно создавать "на ходу" даже методы класса:

>>> A.method = lambda self,s: "<%s %s>" % (s,self.x)
>>> c = A()
>>> c.x = 25
>>> c.method("Text")
'<Text 25>'

Доклад по Python: часть I

1. Что такое?..

Python - это интерпретируемый алгоритмический объектно-ориентированный язык со строгой динамической типизацией, полиморфизм в нем реализован в виде Duck Typing.

Трансляция питона организована очень схожим с Java образом. Именно, исходник компилируется в байт-код, а затем этот байт-код исполняется. Это сходство настолько велико, что существует реализация Питона (Jython), генерирующая Java байт-код для исполнения виртуальной машиной Java. Различие состоит в политике, когда записывать байт-код на диск. Напомню, для Java традиционный способ запустить только что написанную программу такой: запускаем компилятор, подсовывая ему исходник - он генерирует байт-код и записывает его в файл. Затем запускаем виртуальную машину, подсовывая ей байт-код - и она его исполняет.
Питон же обычно не записывает байт-код на диск. В простейшем случае запуск программы происходит так: мы "скармливаем" исходник интерпретатору; он генерирует байт-код, но оставляет его в памяти, а затем передает виртуальной машине (являющейся частью интерпретатора). Это ускоряет запуск программы за счет отсутствия необходимости записывать байт-код на диск.
Однако, при загрузке (импорте) модулей Питон пытается сохранить байт-код, чтобы в следующий раз загрузка модуля происходила быстрее. Есть возможность и саму программу записать в виде байт-кода. Интерпретатору Питона можно подсовывать не только исходники, но и байт-код - он загрузится быстрее.
За счет такого сходства в устройстве с Java имеем и большое сходство в производительности (она примерно одинаковая, только Питон чуть быстрее загружает программы за счет того, что не пишет байт-код на диск).
И именно из-за этого сходства Питон обычно сравнивают именно с Явой.

Еще одно сходство с Явой (и многими другими интерпретируемыми и даже некоторыми компилируемыми языками) - это автоматическое управление памятью. В Питоне нет new[] и delete[], память отводится и освобождается автоматически. Алгоритм сборки мусора как бы "двухслойный": во-первых, сам интерпретатор реализует reference counting (удаляя объекты, на которые никто не ссылается), и во-вторых, есть время от времени запускаемый garbage collector, работающий по более замысловатым, но более быстрым и надежным алгоритмам (например, reference counting не удалит два объекта, ссылающихся друг на друга, даже если на них больше никто не ссылается).

Удобным свойством интерпретатора Питона является наличие REPL (read-eval-print loop), то есть возможности вводить языковые конструкции с консоли и тут же получать результат. Это часто используется для проверки каких-нибудь идей или для отладки.

1.1. Синтаксис.

В самых общих чертах синтаксис Питона напоминает С или Паскаль. Операторы записываются обычно по одному на строку. Присваивание записывается как в С, знаком =. Но при этом присваивание не возвращает значения, поэтому его нельзя использовать в условии. Для сравнений используются обычные для С знаки ==, !=, >,<.>=,<=. Небольшое отличие состоит в том, что Питон понимает "двойные" сравнения в "математическом" смысле, т.е. 0 < x < 10 понимается как 0 < x and x < 10. Основные конструкции проще показать на примерах:

if x < 0:
print "Yes"
elif x==0:
print "X is 0"
else:
print "No"

for i in [1,2,3]:
print i
else:
print "Loop was not break'ed"

while x>0:
print x
x -= 1

try:
z = x/y
except ZeroDivisionError,e:
print e
else:
print "Divided sucsessfully."
finally:
print "Some cleanup."

def fun(x,y):
return x+y

class Child(Parent):
x = 0
def method(self,x,y):
return self.x - x + y

f = open("abc.txt")
for line in f:
print "<%s>" % line
f.close()

В последнем примере показана замена функции sprintf: "строка" % (данные). В проектируемой сейчас версии Python3000 этот способ исчезнет, а вместо него появится другой: "Value {0}, Value {1}".format("one", 2).


1.2. Типы данных.

В Питоне выделяют атомарные и структурные (или ссылочные) типы данных. К атомарным типам относятся числа и строки. Структурные типы - это списки, кортежи (tuples), словари, функции, классы и экземпляры классов.

Данные некоторых типов (а именно - кортежи и строки) являются неизменяемыми.

Списки записываются так: [1, "one", 25]. Список может содержать любое количество объектов любого типа. Обращение к элементу списка - по индексу: a[2] (элементы нумеруются с нуля). Добавление элементов в список можно делать так:

a = a + [25]

или так (предпочтительней, т.к. быстрее):

a.append(25).

Кортежи записываются как значения через запятую: a,b,c. Часто для ясности кортежи приходится записывать в скобках: (a,b,c). Кортежи, как и списки, могут содержать значения любых типов, но, в отличие от списков, являются неизменяемыми.

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

>>> a = "abcde"
>>> a[3:5]
'de'
>>> a[:2]
'ab'
>>> a[4:]
'e'
>>> a[:-2]
'abc'
>>> a[::-1]
'edcba'
>>> a[5:2:-1]
'ed'
>>> a[:]
'abcde'
>>> a[::2]
'ace'

Словари в Питоне - это структура, соответствующая хэшам в Перле, массивам в PHP и std::map в C++. Записываются так:

{'a': 1, 'b': 2, 'c': 3}

или так:

dict(a=1,b=2,c=3).

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

print d['key']

Метод keys() возвращает список из ключей словаря, а values() - список из значений. Порядок следования ключей не определен:

>>> d = dict(a=1,b=2,c=3)
>>> d.keys()
['a', 'c', 'b']
>>> d.values()
[1, 3, 2]

Функции создаются так:

def f(x):
"Documentation string (docstring)"
return x*x

или так:

f = lambda x: x*x

О классах и их экземплярах будем подробно говорить дальше.

2. Типизация.

Главное отличие Питона от Явы (если брать только чисто "лингвистические" свойства) - это типизация. Напомню, в Яве используется строгая статическая типизация с явным объявлением типов переменных. Это означает, что типы всех переменных известны в момент компиляции, и тогда же происходит проверка типов. Это дает преимущество в том плане, что значительная часть ошибок отлавливается в момент компиляции. Зато это замедляет компиляцию. В Питоне используется строгая динамическая типизация. "Динамическая" означает, что типы переменных становятся известными только во время выполнения, и тогда же выполняются проверки типов. Это дает больше возможностей написать неработающий код, но ускоряет компиляцию и дает значительную гибкость. Пример:

>> a = 20 # теперь a - это число, тип int
>> a = "a string" # а теперь - строка, тип str.

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

>>> x = [1,2,3] # x - это список
>>> y = x # y указывает туда же, куда x (копируется указатель).
>>> y[1] = 5 # изменяем элемент из y
>>> x # он изменился и в x.
[1, 5, 3]

Таким образом, в терминах C++, имя переменной - это ссылка (указатель), (void*). Исключением являются атомарные типы данных - числа и строки. Пример:

>>> a = 1
>>> b = a # здесь копируется уже не ссылка, а собственно значение
>>> b = 3 # присваиваем другое значение...
>>> a # на переменную a это не повлияло.
1

По той же схеме передаются аргументы в функцию.

За счет того, что информация о типе хранится вместе со значением, она доступна во время исполнения. Пример:

>>> a = 1
>>> type(a)
<type 'int'>
>>> a = "a string"
>>> type(a)
<type 'str'>

Таким образом, имеет смысл запись вроде if type(a) == str:...

Доклад по Python: About

Сегодня прочитал для группы желающих обзорный доклад по Python. Выложу его здесь, в следующих трех постах.

Объем - около 3-х часов.
Что это такое. Особенности, отличия от других языков.
Плюсы и минусы. ООП в питоне. Сравнение реализации ООП в питоне и Ц++/Жаба.
Характерные приемы программирования и дизайна.

Главная задача - даже не "агитировать за питон", а просто расшевелить мозги.


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

суббота, февраля 23, 2008

Аутентификация: pam_usb

Это уж двухлетней давности моя статья, сейчас с удивлением обнаружил, что в блоге ее не опубликовал. Исправляюсь ;)

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

Во-первых, кто такой PAM?

PAM - это Plugable Authentithication Modules, т.е. подключаемые модули аутентификации. Идея состоит в том, что любая софтина, которой нужно аутентифицировать пользователя (напр.: login, passwd, sudo...) обращается к сервису PAM. Тот, в свою очередь, глядя в конфиг, по очереди обращается к указанным модулям, спрашивая у них - пускать или не пускать? Если напротив модуля стоит required, то "признание" этим модулем пользователя является условием необходимым, но не достаточным. Если стоит sufficient - значит, признания даже только одним этим модулем достаточно. Там еще много настроек указать можно...

Т.е. теоретически, ничего не стоит, написав дополнителный модуль для PAM, настроить систему так, чтобы она вместо пароля спрашивала сетчатку глаза там или отпечатки пальцев...

PAM_USB

Это, как вы уже поняли, один из таких модулей. При попытке логина он ищет на указанном в конфиге устройстве файл вида /.auth/user.host, в хомяке пользователя - файл ~/.auth/id_pub, и проверяет их соответствие по алгоритму DSA.

Почему-то pam_usb нету в Debian, хотя распространяется он по GPL, и исходник весит 27Kb. Взять его можно на pamusb.org, жалко только, сайт в состоянии редизайна, и информации там по минимуму...

И еще: вообще-то, этому модулю пофиг, что вы ему укажете в качестве дивайса. С тем же успехом это может быть дискета или CD.

А нафига оно?

А вы еще не поняли? Пароль-то придумать ведь надо еще! Придумаешь простой - просто подобрать, придумаешь сложный - просто забыть. Про пароли, написанные карандашом на мониторе, я молчу. А носить всегда в кармане флэшку и выдергивать ее, уходя - все-таки гораздо легче, чем помнить пароль типа $KHYhguh(7438!

Как это сделать?

Итак, предположим, вы скачали и поставили пакет pam_usb. Что дальше делать?

Во-первых, надо сгенерировать ключи для нужных юзеров. Монтируем флэшку, скажем, в /mnt/flash, и говорим

# usbadm keygen /mnt/flash username 2048,

где username - это имя интересующего нас юзера, а 2048 - длина ключа. Повторяем это для всех нужных юзеров и отмонтируем флэшку.

Теперь надо настроить сам PAM. В директории /etc/pam.d лежат файлы, соответствующие программам, которые используют PAM: login, xdm, ... У меня в Debian'е все они включают (директивой @include) файл common-auth. В случае другого дистра, вероятно, придется поискать, куда именно вставлять интересующие нас строчки:

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

auth sufficient pam_usb.so check_device=-1 check_if_mounted=-1 force_device=/dev/transcend fs=vfat

auth required pam_unix.so nullok_secure

(где /dev/transcend - дивайс флэшки, а vfat - тип файловой системы на ней).

б) если мы хотим, чтобы в систему можно было зайти только по флэшке, зато без пароля:

auth required pam_usb.so check_device=-1 check_if_mounted=-1 force_device=/dev/transcend fs=vfat

в) наконец, если мы хотим, чтобы можно было зайти только при вставленной флэшке, и спрашивался пароль:

auth required pam_usb.so check_device=-1 check_if_mounted=-1 force_device=/dev/transcend fs=vfat

auth required pam_unix.so nullok_secure

Например, у меня сейчас в файле common-auth записаны строки из варианта а).

четверг, февраля 21, 2008

Про обучение программированию

Такой вот рассуждательный пост.

Мне со следующего учебного года, по всей видимости, предстоит обучать студентов программированию. Пока что это спецкурс, скорее факультативный, чем основной (хотя и непосредственно относящийся к специальности), на первом курсе. Называется - спортивное программирование. Звучит конечно круто, но приходится принимать во внимание два обстоятельства: 1) курс - всего 2 часа в неделю (2 академических часа в нашем вузе равняются 1-му астрономическому), 2) большинство (ок. 70%) поступающих к нам на физмат либо плохо знают Паскаль, либо вообще ничего из области программирования не знают. Из-за второго факта на дисциплине "Теория программирования" на первом курсе изучают Паскаль (до прошлого года - Turbo Pascal, сейчас, кажется, перешли на Free Pascal).

В связи со всем этим возникает, конечно, вопрос - как тут можно обучать спортивному программированию? Некоторые вещи я для себя уже уяснил, и продолжаю уяснять, думаю по мере накопления публиковать. Итак, чего я пока нарешал:

  1. Кому не надо - пусть гуляют. Мне вообще обучать кого-либо невыгодно, тем более я не собираюсь возиться с теми, у кого нет желания учиться.
  2. Знание какого-нибудь ЯП в спортивном (да любом) программировании нужно, но не является главным. С учетом того, что Паскалю их обучать будут параллельно, программировать видимо будем все на том же Паскале.
  3. За 27 астрономических часов, размазанных на весь учебный год, подготовить с нуля программиста-олимпийца невозможно никак. Так что от этой цели придется отказаться. Буду делать возможное - постараюсь мало-мальски подготовить желающих стать программистами к самообучению в нужном направлении.
  4. В данной ситуации "спортивное программирование" приходится интерпретировать как знание алгоритмов. Так что будем изучать алгоритмы, сколько успеем. Все остальное, что успею - "дополнительные сведения".
  5. Вопреки всем обстоятельствам, я хочу дать шанс талантливым ребятам, по случайности оказавшимся у нас. Поэтому по возможности буду включать в курс какие-то сведения, помогающие сориентироваться в мире современного программирования.
  6. Какие это могут быть сведения? На изучение, например, современных технологий программирования, у меня нет времени (и подготовка учащихся недостаточна). Перебором вариантов аналогично показывается, что не проходят идеи об изучении каких-либо конкретных разделов/технологий современного программирования (сверх "знания алгоритмов"). Из "дополнительных сведений" конкретные знания приходится исключить.
  7. Таким образом, из "дополнительных" сведений остаются только 1) сведения обзорного характера - хотя бы просто перечисление самых динамично развивающихся сейчас технологий и какая-нибудь попытка классификации языков программирования; 2) какие-то основополагающие, фундаментальные вещи, афористические формулировки - квинтэссенция полувекового опыта программирования; 3) знания, относящиеся не к практике и даже не к теории, а скорее к "философии программирования".

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

Пока что больше вопросов, чем ответов...

понедельник, февраля 11, 2008

Интерпретатор lambda-исчисления

В порядке самообразования решил поподробнее изучить λ-исчисление. Нашел несколько книжек и пр. Забавной показалась теорема о существовании неподвижной точки у любой функции. Решил "проверить", переписал Y-комбинатор из книжки на знакомом мне Haskell... Угу, фиг - его невозможно типизировать. Хотел было попробовать на Scheme, но сообразил, что типизация она и в scheme типизация. Тогда стал искать какой-нибудь язык, реализующий нетипизированное λ-исчисление, чтобы на нем можно было "поиграться" с примерами из книжек. Нашел только unlambda, который, как оказалось, реализует вовсе даже не лямбду, а комбинаторную логику, а синтаксис у него похож более всего на brainfuck. Вздохнул и решил писать что-то такое сам...

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

Подробности про язык тут, само поделие тут (Depends: python, python-ply; запускать ./pylambda.py в терминале).

среда, января 23, 2008

Настройка Linux для kiosk-mode

За последнее время несколько раз пришлось делать сабж, так что решил задокументировать.

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

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

Конечно, для надежности нужны некоторые действия с аппаратной частью. Приводы CD/DVD (и floppy) убираются, разъемы usb/firewire на передней панели отключаются от материнки, кнопки reset и power также отключаются от материнки. Системник опломбируется. Только это все надо делать _после_ возни с программной частью - ато ребут в случае чего будет проблемой ;)

С программной частью так. Для автовхода в систему используем GDM (можно kdm/xdm, но они хуже настраиваются) - в настройках (gksudo gdmsetup) включаем Auto login или Timed login, на усмотрение. На вкладке "Пользователи" разрешаем вход только нужным пользователям (чаще всего - только одному; прав у него, конечно, самый минимум). На вкладке "Локальный вход" снимаем галочку "Показывать меню действий". На вкладке "Общие" включаем галочку "Перезапускать XServer..." и указываем нужный сеанс по умолчанию (мы его сейчас создадим, пока что выберите там что угодно).

Создаем файл /usr/share/xsessions/kiosk.desktop (имя любое):


[Desktop Entry]
Encoding=UTF-8
Name=Kiosk mode
Comment=Kiosk mode
Exec=/usr/local/bin/kiosk-session
Type=Application


Теперь опять запускаем gdmsetup и указываем Kiosk mode в качестве сеанса по умолчанию.
/usr/local/bin/kiosk-session будет такой:


#!/bin/bash

# Это приложение не должно позволять закрыть его
самое-нужное-приложение & xpid=$!
gnome-settings-daemon & # Это если хотим использовать настройки Gnome
metacity & # Или какой вам оконный менеджер больше нравится
чего-еще-хотим-запускать-автоматом &
# Ждем завершения основного приложения, т.е. предположительно до бесконечности.
wait $xpid


(не забудьте sudo chmod +x /usr/local/bin/kiosk-session).

Теперь правим /etc/X11/xorg.conf:


Section "ServerFlags"
Option "DontZap" "True" # Запрещаем Ctrl-Alt-Backspace
Option "DontVTSwitch" "True" # Запрещаем Ctrl-Alt-F#
EndSection


Ставим openssh-server, убеждаемся что он будет стартовать автоматом и что в sshd_config стоит "X11Forwarding yes". Если нужен будет доступ по vnc, ставим x11vnc и прописываем в автозапуск (в kiosk-session).

Ну вроде всё, ребутаемся и наблюдаем результаты своих трудов ;)
Вся дальнейшая настройка - по ssh.

PS. Выбору оконного менеджера стоит уделить некоторое внимание. Дело в том, что многие WM, в том числе и из самых "простых" и "легких" (icewm, ion3, ...) имеют встроенную "командную строку", позволяющую запускать что угодно. Так что надо либо отключить это в настройках, либо выбрать WM без такой возможности (тот же metacity; Alt-F2 в Gnome обеспечивает не он, а gnome-panel).