3 марта 2009 в 20:58

Атрибуты: взгляд внутрь

Perl*
Это продолжение статьи "Введение в атрибуты". Если Вы не знакомы с идеей атрибутов и их синтаксисом — советую начать с нее. Ну а в этой статье рассматривается, как устроены атрибуты изнутри, как с ними обращаться, и какие могут возникнуть проблемы.

Guts


Когда во время компиляции Perl встречает атрибут, он пытается вызвать у текущего класса один из хендлеров вида MODIFY_SCALAR_ATTRIBUTES, MODIFY_CODE_ATTRIBUTES и т.д. — в зависимости от типа помеченных атрибутом данных, примерно так:

__PACKAGE__->MODIFY_CODE_ATTRIBUTES(\&mySub, 'myAttribute1', 'myAttribute2');

Еще раз повторюсь: эти вызовы выполняются прямо во время компиляции (на стадии BEGIN), а точнее — сразу после того, как perl докурит закрывающую функцию фигурную скобку (справедливый UPD от xames: не все хэндлы вида MODIFY_*_ATTRIBUTES выполняются на стадии BEGIN. В частности MODIFY_SCALAR_ATTRIBUTES выполнится на этапе инициализации переменной (my $tmp: attr = 0;)). Поскольку атрибутов может быть несколько, а наследование никто не отменял, считается хорошим тоном прошерстить внутри хендлера полученный список, отобрать оттуда те атрибуты, которые обработать можно, и отдать суперклассу на растерзание то, что осталось.

Аналогично, если вы хотите получить список атрибутов функции/переменной и вызываете для этого функцию get из модуля attributes, происходит обращение к хендлеру FETCH_SCALAR_ATTRIBUTES (ну или FETCH_CODE_ATTRIBUTES — думаю, вы уже поняли).

Собственно, все особенности работы с атрибутами на низком уровне можно посмотреть в perldoc attributes. Кстати, для тех, кому такая реализация показалась кривоватой, там есть важное замечание: "The mechanisms described here are still experimental. Do not rely on the current implementation". Ну что ж, нет ничего постояннее экспериментальных механизмов :-)

Рецепт счастья


Очевидно, ковырять атрибуты при помощи вышеупомянутых хендлеров — занятие не слишком-то приятное. Слава Богу, в поставке того же Perl 5.6 появился модуль Attribute::Handlers, который значительно упрощает написание обработчиков для атрибутов и вводит дополнительные интересные возможности — атрибуты могут иметь параметры и обрабатываться не только во время BEGIN-стадии, но и во время CHECK, INIT, END. Кстати, работает этот модуль опять же при помощи атрибутов.

Итак, чтобы можно было использовать атрибут myAttribute для пометки функций, достаточно написать следующий код:

use Attribute::Handlers;
sub myAttribute : ATTR(CODE) {
my ($package, $symbol, $referent, $attr, $data, $phase, $filename, $linenum) = @_;
....
}


Обработчик — метод myAttribute — будет вызван для помеченной функции приблизительно так же, как и хендлеры из предыдущих примеров. Параметров ему передается заметно больше: кроме ссылки на функцию и имени атрибута могут присутствовать также ссылка на элемент таблицы символов (GLOB), название фазы BEGIN/CHECK/INIT/END, дополнительные параметры атрибута, указание на имя файла и номер строки. По умолчанию обработчик запускается во время стадии компиляции CHECK, когда все что можно уже загружено и переварено.

Если нам захочется передавать обработчику дополнительную информацию — можно сделать это например так:

sub mySub : myAttribute(p1,p2) {:}

В этом случае обработчик получит в переменную $data массив со значениями p1 и p2.

Из других забавных возможностей Attribute::Handlers стоит отметить альтернативный интерфейс к функции tie, который сам по себе является довольно интересным примером применения атрибутов. Останавливаться на нем здесь особого смысла нет, все предельно понятно из документации.

Стоит отметить, что большинство модулей CPAN, использующих атрибуты, опирается именно на модуль Attribute::Handlers. Тем не менее, поможет он не всегда. Более того, не надейтесь что все так гладко и красиво, как кажется :)

Грабли


Фаза CHECK не зря выбрана в Attribute::Handlers, как дефолтная. На этой фазе интерпретатор уже закончил обработку кода и разместил функции и все остальное в таблице символов, так что теперь есть техническая возможность получить ссылку на соответствующий GLOB и пошаманить над ним — например, подменить функцию. Сделать это на фазе BEGIN не получится — GLOB еще не заполнен, и в обработчик атрибута передан не будет. И вот тут начинается самое интересное. Если вы занимаетесь разработкой под mod_perl, все это к вам не относится — у вас нет фазы CHECK. Согласно perldoc perlmod, внутри вызовов eval и под mod_perl стадии компиляции CHECK и INIT не работают, есть только BEGIN и еще UNITCHECK. С UNITCHECK'ом тоже не все гладко — глобально эта фаза не перехватывается, только на уровне соответствующего модуля. Мне не удалось найти модуля, который бы решал эту проблему и давал возможность добраться до GLOB'а под mod_perl столь же тривиально, как это можно сделать в обычном скрипте. Можно было бы добавить на уровне Attribute::Handlers возможность вводить свои собственные триггеры для запуска обработчиков и дергать их вручную — но модуль написан так, что про подобные патчи можно сразу забыть. Я смог решить эту задачу фактически введя дополнительное ограничение при написании кода — пришлось отказаться от require при загрузке классов в пользу use. В результате работа делается в два захода — сначала на этапе BEGIN собирается список функций, помеченных нужным мне атрибутом, а после вызова метода import делается основная работа. Если кто-то предложит более человечный способ, я буду рад.

Заключение


По сути, атрибуты позволяют привязать к привычным составляющим программы дополнительные возможности. Интерес этих возможностей в том, что на самом деле их в коде не видно. Это чем-то похоже на tie — вы выполняете элементарные действия над простейшими структурами данных, за которыми на самом деле скрыты нетривиальные обработчики. Если вам понадобился некий универсальный механизм, но вы не хотите, чтобы особенности его реализации были видны в коде — посмотрите в сторону атрибутов.
Bambr @Bambr
карма
65,2
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (14)

  • 0
    когда я хотел воспользоваться custom аттрибутами в 5.6, 2 года назад, они еще не работали… Причем по идее они подходили замечательно — пропускать вызовы только тегированным определенным образом методам пришлось с дополнителнительными некрасивыми проверками.
    А теперь уже возврщаться к ним и руки не доходят
    • 0
      Довольно странно. Я специально рылся в документации, чтобы понять, как давно я игнорирую эту тему. Я понял так что Attribute::Handlers уже входил в поставку perl 5.6…
      • 0
        в документации они были — а вот использовать можно было только какой-то список «встроенных» аттрибутов. Выдавалось достаточно невнятное сообщение при проверке синтаксиса.
  • +2
    ого, целый кусок перла прошел мимо меня :)
    • 0
      И мимо меня тоже. Вообще не знал про аттрибуты.
  • +1
    Я активно использую Perl6::Export::Attrs — достаточно простую и удобную альтернативу монструозному Exporter работающую как раз на атрибутах:
      sub something :Export { ... }
      sub another :Export(:DEFAULT) { ... }
    

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

    Вообще, мы потихоньку приходим к тому, что выполнение кода в блоках BEGIN/CHECK/INIT это плохая идея. При подгрузке модуля, до того как из него явным образом не вызвали функцию, он не должен ничего делать! Как-то раз я наступил на восхитительную граблю: при записи файла в vim переставал напрочь работать разрабатываемый сайт. Проблема оказалась в том, что в момент проверки синтаксиса (вызываемой автоматически при записи в vim) подгружался модуль, который «инициализировал» FastCGI-шный unix-сокет — разумеется, убивая при этом сокет на котором ждал подключений от апача сервер FastCGI этого проекта. Так же код в этих блоках имеет тенденцию вызывать странности при подгрузке тестируемого модуля/скрипта в тестах.
    • 0
      >Вообще, мы потихоньку приходим к тому, что выполнение кода в блоках BEGIN/CHECK/INIT это плохая идея.

      Ммм… Тут надо, наверное, все-таки оговорить явно, что это относится именно к модулям. Сам вызывающий скрипт может использовать BEGIN и прочие вполне успешно.
      • 0
        К сожалению, нет. Это тоже может вызывать проблемы при подгрузке этого скрипта require-ом в тестах и при операциях вроде проверки синтаксиса (я выше приводил пример из личного опыта).

        Безусловно, речь не идёт о том, чтобы категорически отказаться от использования этих блоков (или оператора goto :)). Речь идёт о том, что по возможности желательно обходиться без них, т.к. у них есть неприятные побочные эффекты. А при использовании иметь эти побочные эффекты в виду.
  • 0
    Люди, тут проблема у товарища как раз с аттрибутами, можно ее прокомментировать?
    • +2
      Скорее всего при наличии такого пробела скобка не распознается как часть атрибута Method, и в результате перл вообще не понимает что это за скобочки. Вопреки мнению вопрошающего, в доке на модуль пробела в этом месте нет.
      По поводу варнингов — use warnings стоит в самом модуле, возможно ему там что-то не по душе.
      Могу еще раз посоветовать не забивать гвозди микроскопом и не использовать этот класс. Непрактично это.
      • 0
        Да, на счет пробела ошибся.

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

        Attribute::Method значительно сокращает количество строчек и уже по этому есть ситуации, когда оно оправдано. Так же оно хорошо, если есть необходимость набрать и посадить кодить студентов и другую дешевую рабочую силу, которые проме паскаля умеют в лучшем случае еще только си.
        • 0
          Попробуйте указать интерпретатору ключик -X.

          По поводу Attribute::Method — не думаю, что строчка вида my ($self, $a, $b) = @_; сильно увеличит размер кода. Если студенты с даже с этим не могут справиться, я Вам не завидую.
  • +2
    > Еще раз повторюсь: эти вызовы выполняются прямо во время компиляции (на стадии BEGIN)…

    1. Не все хэндлы вида MODIFY_*_ATTRIBUTES выполняются на стадии BEGIN. В частности MODIFY_SCALAR_ATTRIBUTES выполнится на этапе инициализации переменной ( my $tmp: attr = 0; )
    Данные методы вызываются в момент объявления переменных и функций. На это стоит обратить внимание.
    2. В коде MODIFY_*_ATTRIBUTES должны быть описаны раньше чем встретятся атрибуты.
    • +1
      Вы правы :( На момент написания материала был богатый опыт трахотни именно с атрибутами функций, отсюда и глюк. Сейчас поправим.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.