Искусство программирования для Unix.

9.1.3. Учебный пример: программирование метаклассов в fetchmail.

Утилита fetchmailconf(1), конфигуратор файлов профилей, поставляемая с программой fetchmail(1), содержит полезный пример передовой программы, управляемой данными, написанной в объектно-ориентированном языке очень высокого уровня.

В октябре 1997 года серия вопросов в списке рассылки пользователей fetchmail продемонстрировала тот факт, что конечные пользователи сталкивались с возрастающими проблемами при создании конфигурационных файлов для fetchmail. В файле используется простой классический для Unix свободный формат синтаксиса. Однако данный файл может оказаться неприступно сложным, когда пользователь имеет учетные записи POP3 и IMAP на нескольких узлах. Несколько упрощенная версия конфигурационного файла fetchmail приведена в примере 9.1.

Цель создания fetchmaikonf заключалась в том, чтобы полностью скрыть синтаксис управляющего файла за стильным, эргономичным GUI-интерфейсом с кнопками выбора, бегунками и формами для заполнения. Однако в бета-версии была проблема: программа могла легко создавать конфигурационные файлы, исходя из действий, предпринятых пользователем в GUI, но не могла считывать и редактировать уже существующие файлы.

Пример 9.1. Синтаксис файла fetchmailrc.

Set postmaster "esr".

Set daemon 300.

Poll imap.ccil.org with proto IMAP and options no dns.

 aka snark.thyrsus.com locke.ccil.org ccil.org.

  user esr there is esr here.

   options fetchall dropstatus warnings 3600.

Poll imap.netaxs.com with proto IMAP.

 user "esr" there is esr here options dropstatus warnings 3600.

Анализатор для синтаксиса конфигурационного файла fetchmail является довольно сложным. Фактически он написан с использованием программ yacc и lex, двух классических инструментов Unix для создания кода синтаксического анализатора на языке С. Вначале разработчикам показалось, что для того чтобы с помощью fetchmailconf можно было редактировать существующие конфигурационные файлы, понадобится воспроизвести тот же сложный синтаксический анализатор в языке реализации fetchmailconf — Python.

Такая тактика выглядела бесперспективной. Даже если не принимать во внимание объем необходимой дублирующей работы, очень трудно быть уверенным в том, что два синтаксических анализатора, написанных на двух различных языках, допускают использование одной и той же грамматики. Обеспечить в дальнейшем их синхронизацию по мере развития конфигурационного языка будет чрезвычайно сложно. Данный подход нарушал бы правило SPOT, описанное в главе 4.

На какое-то время автор был поставлен в тупик данной проблемой. Разрешило ее интуитивное понимание того, что программа fetchmailconf может использовать собственный синтаксический анализатор fetchmail в качестве фильтра. В результате в fetchmail был добавлен параметр --configdump, который позволял бы анализировать файл .fetchmailrc и отправлять результаты на стандартный вывод в формате Python-инициализатора. Для приведенного выше файла результаты выглядели бы приблизительно так, как показано в примере 9.2 (для экономии места некоторые данные, не связанные с примером, опущены).

Основное препятствие было преодолено. Интерпретатор Python мог затем оценить вывод fetchmail --configdump и прочесть доступную для fetchmailconf конфигурацию как значение переменой "fetchmail".

Однако описанное препятствие было не последним. Было действительно необходимо не только предоставить fetchmail существующую конфигурацию, но превратить ее в связанное дерево действующих объектов. В данном дереве было бы 3 вида объектов: Configuration (объект верхнего уровня, представляющий всю конфигурацию), Site (представляющий один из серверов для опроса) и User (представляющий пользовательские данные, связанные с узлом). Файл в примере описывает 3 объекта Site, каждый из которых связан с одним пользовательским объектом.

Данные 3 класса объектов уже существовали в fetchmailconf. Каждый из них имел метод, который заставлял его выводить на экран GUI-панель редактирования для модификации своего экземпляра данных. Последняя проблема сводилась к некоторому преобразованию статических данных в Python-инициализаторе в действующие объекты.

Пример 9.2. Дамп Python-структуры для конфигурации fetchmail.

Fetchmailrc = {

 'poll_interval':300,

 "logfile":None,

 "postmaster":"esr",

 'bouncemail':TRUE,

 "properties":None,

 'invisible':FALSE,

 'syslog' :FALSE,

 # List of server entries begins here.

 # (Ниже начинается список серверов).

 'servers': [

   # Entry for site `imap.ccil.org' begins:

   # (Начало записи для узла imap.ccil.org:).

   {

    "pollname":"imap.ccil.org",

    'active':TRUE,

    "via":None,

    "protocol":"IMAP",

    'port':0,

    'timeout':300,

    'dns':FALSE,

    "aka":["snark.thyrsus.com","locke.ccil.org","ccil.org"],

    'users': [

     {

      "remote":"esr",

      "password":"masked_one",

      'localnames':["esr"],

      'fetchall':TRUE,

      'keep':FALSE,

      'flush':FALSE,

      "mda":None,

      'limit':0,

      'warnings':3600,

     }

    ' ].

   }

  '.

  # Entry for site `imap.netaxs.com' begins:

  # (Начало записи для узла imap.netaxs.com:).

  {

   "pollname":"imap.netaxs.com",

   'active':TRUE,

   "via":None,

   "protocol":"IMAP",

   'port':0,

   'timeout':300,

   'dns':TRUE,

   "aka":None,

   'users': [

    {

     "remote":"esr",

     "password":"masked_two",

     'localnames':["esr"],

     'fetchall':FALSE,

     'keep':FALSE,

     'flush':FALSE,

     "mda":None,

     'limit':0,

     'warnings':3600,

    }

   ' ].

  }

  '.

 ]

}

Рассматривалась идея написания связующего уровня, который имел бы явную информацию о структуре всех 3 классов и использовал бы данную информацию для просмотра инициализатора при создании соответствующих объектов. Однако данная идея была отклонена, поскольку существовала вероятность добавления со временем новых членов класса, по мере создания новых функций в конфигурационном языке. Если бы код создания объектов был написан таким очевидным путем, то он также был бы хрупким и склонным к рассинхронизации при изменении определения классов либо структуры инициализатора, выводимой с помощью генератора отчетов --configdump. Подобный подход приводит к бесконечному появлению ошибок.

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

Программисты, работающие с Lisp и Java, называют данную методику интроспекцией (introspection). В некоторых других объектно-ориентированных языках она называется программированием метаклассов (metaclass hacking) и, как правило, считается "черной магией", понятной только "посвященным". В большинстве объектно- ориентированных языков данная методика не поддерживается вообще, а в тех языках, где она поддерживается (среди них Perl и Java), она часто сложна и ненадежна. Однако в языке Python средства интроспекции и программирования метаклассов исключительно доступны.

В примере 9.3 приведен фрагмент кода приблизительно со строки 1895 в версии 1.43.

Пример 9.3. Код метакласса copy_instance.

Def copy_instance(toclass, fromdict):

#Make a class object of given type from a conformant dictionary.

 class_sig = toclass.__dict__.keys(); class_sig.sort().

 dict_keys = fromdict.keys(); dict_keys.sort().

 common = set_intersection(class_sig, dict_keys).

 if 'typemap' in class_sig:

  class_sig.remove('typemap').

 if tuple(class_sig) != tuple(dict_keys):

  print "Conformability error".

#print "Class signature: " + `class_sig`

#print "Dictionary keys: " + `dict_keys`

  print "Not matched in class signature: "+ \

   `set_diff(class_sig, common)`

  print "Not matched in dictionary keys: "+ \

   `set_diff(dict_keys, common)`

  sys.exit(1).

 else:

  for x in dict_keys:

   setattr(toclass, x, fromdict[x]).

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

Def copy_instance(toclass, fromdict):

 for x in fromdict.keys():

  setattr(toclass, x, fromdict[x]).

Если разрабатываемый код настолько же прост, то весьма вероятно, что он верен. В примере 9.4 приведен код, вызывающий данный метакласс.

Ключевым моментом в данном коде является то, что он проходит 3 уровня инициализатора (конфигурация/сервер/пользователь), устанавливая корректные объекты каждого уровня в списки, содержащиеся в следующем объекте более высокого уровня. Поскольку метакласс copy_instance управляется данными и является полностью общим, его можно использовать во всех 3 уровнях для 3 различных типов объектов.

Данный пример — пример новой школы. Язык Python был создан гораздо позднее 1990 года. Однако пример отражает идеи, которые возвращаются к Unix-традициям 1969 года. Если бы размышления над Unix-программированием, практикуемым предшественниками, не научили бы автора "конструктивной лени" — настаивая на повторном использовании кода и отказе от написания дублирующегося связующего кода в соответствии с правилом SPOT — он мог бы "удариться" в программирование синтаксического анализатора на языке Python. Главное понимание того, что сама программа fetchmail могла бы превратиться в синтаксический анализатор конфигурации fetchmailconf, возможно, никогда бы не пришло.

Пример 9.4. Вызывающий контекст для copy_instance.

#Сложная часть - инициализация объектов из глобального класса.

#`configuration'.

#`Configuration' - верхний уровень объектного дерева,

#который планируется изменить.

Configuration = Controls().

Copy_instance(Configuration, configuration).

Configuration.servers = [];

For server in configuration['servers']:

 Newsite = Server().

 copy_instance(Newsite, server).

 Configuration.servers.append(Newsite).

 Newsite.users = [];

 for user in server['users']:

  Newuser = User().

  copy_instance(Newuser, user).

  Newsite.users.append(Newuser).

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

Интуитивное понимание подобное описанному может быть необыкновенно действенным. Рассматриваемый код был написан в течение приблизительно 90 минут, был работоспособен при первом запуске и с тех пор в течение многих лет оставался стабильным (единственный сбой произошел при обработке исключительной ситуации в присутствии действительного перекоса версий). Данный код содержит менее 40 строк и великолепно прост. Не существует способа, при котором примитивный подход полного создания второго синтаксического анализатора мог бы привести к созданию такого же удобства сопровождения, такой же надежности или компактности. Повторное использование кода, упрощение, обобщение, ортогональность — Дзэн операционной системы Unix в действии.

В главе 10 рассматривается синтаксис файла конфигурации fetchmail в качестве примера стандартного shell-подобного метаформата для конфигурационных файлов. В главе 14 fetchmailconf используется как пример, демонстрирующий мощность языка Python в быстрой разработке GUI-интерфейсов.