Personal tools
You are here: Home Новости Как использовать рекурсию, программируя адаптеры
Document Actions

Как использовать рекурсию, программируя адаптеры

by cray last modified 2007-11-19 18:58

На примере простых адптеров IPath дается подробное описание методологии рекурсивного программирование адаптеров, которая используется в большей части Zope3.

Как использовать рекурсию, программируя адаптеры :

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

Краткое введение:

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

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

Адаптер "Путь к объекту":

Самый простой пример алгоритма, вызывающего последовательный перебор объектов - это составление пути к объекту. В простейшем случае, в итеративной форме, решение выглядит так:

            def f(ob) :
                lp = [ob.__name__]
                while hasattr(ob,"__parent__") and ob.__parent__ :
                    ob = ob.__parent__
                    lp.append(ob.__name__)

                lp.reverse()
                return "/".join(lp)                                        

Здесь нет ни слова про компоненты: обычные объекты и никаких интерфейсов. Однако на самом деле компоненты есть: код неявно предполагает, что все объекты предоставляют интерфейс IContained. Попробуем реализовать нормальную коллекцию адаптеров для составления пути к объекту, назовем его адаптер IPath.

Введем интерфейс IPath:

            class IPath(Interface) :
                path = TextLine()
                titledpath = TextLine()

И сделаем из функции f(ob) адаптер:

            class Contained2PathAdapter(object) :
                implements(IPath)
                adapts(IContained)

                def __init__(self, ob) :
                    self.context = ob

                @property
                def path(self) :
                    ob = self.context
                    lp = []
                    while IContained.providedBy(ob) and IContained(ob).__parent__ :
                        lp.append(IContained(ob).__name__)
                        ob = IContained(ob).__parent__
                    lp.reverse()
                    return "/".join(lp)                                        

Здесь уже вполне решена по крайней мере главная проблема: что делать, если промежуточный объект не содержит __parent__ и __name__: для него будет написан специальный адаптер, во вполне компонентной модели. Однако, это по-прежнему монолитный код, который не особенно возможно сильно изменить (например, пропустить фрагмент пути или сделать еще что-то в этом роде). Решение таких проблем должно быть основано на тезисе: "каждый промежуточный объект отвечает за то, чтобы просчитать участок пути до следующего объекта". Итак, переходим к рекурсии. Адаптеров в перспективе будет много, поэтому начнём с базового класса :

            class PathAdapterBase(Interface) :
                implements(IPath)

                def __init__(self, ob) :
                    self.context = ob

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

Рассмотрим для начала некомпонентный случай. Для его реализации код адаптера следует сдедать следующим:

            class Contained2PathAdapter(PathAdapterBase) :
                implements(IPath)
                adapts(IContained)

                @property
                def path(self) :
                    if IContained(self.context).__parent__ is None :
                        return "/"
                    else :
                        return "/".join([ 
                            IPath(IContained(self.context.__parent__)).path,
                            IContained(self.context).__name__])

Как видно, это классическая реализация рекурсии: при достижении такого объекта, у которого отсутствует __parent__, то есть корневого объекта Zope, возвращается "/".

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

            class Any2PathAdapter(PathAdapterBase) :
                adapts(Interface)

                path = "/"

Помимо указанного, нам понадобится ещё один адаптер, который будет отличаться от указанного лишь тем, что его необходимо будет вызывать только для объекта с интерфейсом IRootFolder. Всё, что для этого потребуется, это зарегистрировать уже существующий адаптер (Any2PathAdapter), адаптирующий всё подряд (Interface) для объектов с интерфейсом IRootFolder с помощью следующего zcml-кода (приятно. что код адаптера при этом совершенно не меняется):

            <adapter
                factory=".any2pathadapter.Any2PathAdapter"
                for="zope.app.folder.interfaces.IRootFolder"
            />

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

            class Contained2PathAdapter(PathAdapterBase) :
                implements(IPath)
                adapts(IContained)

                @property
                def path(self) :
                    return "/".join([
                        IPath(IContained(self.context.__parent__)).path,
                        IContained(self.context).__name__])

Что характерно как для рекурсии, так и компонентной модели, получилось более коротко, и более запутано. Зато можно гибко настраивать. Начнем со "сторожа". Пусть путь к объекту будет заканчиваться у ближайшего объекта, для которого создан сайт-менеджер. Код соответствующего адаптера будет выглядеть следующим образом (аналогичного эффекта, как и в вышеприведённом примере, можно добиться простой регистрацией имеющегося адаптера в zcml для интерфейса ISite, вместо IRootFolder, как это сделано в примере выше):

            class Site2PathAdapter(Any2PathAdapter) :
                adapts(ISite)

Готово. Быстро и элегантно. Отныне работа адаптера будет прекращаться на объеках, предоставлящих интерфейс ISite. Теперь, пусть на сайте есть просто папки (IFolder), которые совсем ни на что не годные, и показывать их в пути нет ну никакого смысла (легко убедится что IFolder предоставляет также IContent), а раз так, Any2PathAdapter годится как суперкласс :

            class Folder2PathAdapter(Any2PathAdapter) :
                adapts(IFolder)

                @property
                def path(self) :
                    ### 
                    return IPath(IContained(self.context).__parent__).path

Усложним немножко задачу, вспомним, про атрибут titledpath интерфейса IPath, суть которого - путь, составленный из названий объектов. Доработаем базовый адаптер:

            class Contained2PathAdapter(PathAdapterBase) :
                adapts(IContained)   

                [.. пропущено ..]

                @property
                def titledpath(self) :
                    return "/".join([ 
                        IPath(IContained(self.context.__parent__)).titledpath, 
                        ITitle(self.context).title])

Проблема заключается в том, что объект, предоставляющий IContained совсем не обязательно адаптируется к IZopeDublinCore. А раз так, у нас налицо проблема, решить которую можно введя комплиментарный адаптер ITitle, который гарантировано для любого объекта найдет название, причем наилучшим образом. Тогда Contained2PathAdapter перепишется вот так:

            class Contained2PathAdapter(PathAdapterBase) :
                adapts(IContained)

                [.. пропущено ..]

                @property
                def titledpath(self) :
                    return "/".join[ 
                        IPath(IContained(self.context).__parent__).path, 
                        ITitle(self.context).title 
                        ]

А в Any2PathAdapter добавится атрибут titledpath:

            class Any2PathAdapter(PathAdapterBase) :
                adapts(Interface)

                [.. пропущено ..]

                titledpath = "/"

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

            class ITitle(Interface) :
                title = TextLine()

Как и в предыдущем случае, адаптеров будет много, поэтому есть смысл создать базовый адаптер, содержащий основные функции :

            class TitleAdapterBase(object) :
                implements(ITitle)

                def __init__(self, ob) :
                    self.context = ob

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

            class Any2TitleAdapter(TitleAdapterBase) :
                adapts(Interface)

                @property
                def title(self) :
                    return self.context.__class__.__name__

Адаптер IPath основан, в первую очередь, на объектах IContained, поэтому имеет смысл определить адаптер для нег :

            class Contained2TitleAdapter(TitleAdapterBase) :
                adapts(IContained)

                @property
                def title(self) :
                    return IContained(self.context).__name__

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

            class ZDC2TitleAdapter(Contained2TitleAdapter) :
                adapts(IAttributeAnnotaible)

                @property
                def title(self) :
                    title = IZopeDublinCore(self.context).title
                    if not title:
                        try:
                            return super(ZDC2TitleAdapter, self).title
                        except TypeError:
                            ### 
                            return title
                    return title

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

            class Note2TitleAdapter(TitleAdapterBase) :
                adapts(INote)

                @property
                def title(self) :
                    title = INote(self.context).title
                    if not title :
                        try:
                            return super(Note2TitleAdapter, self).title
                        except TypeError:
                            ### 
                            return title
                    return title

На этом экскурсию в адаптер IPath можно считать законченной. Интересно отметить возможное применение этого адаптера - индексация для поиска.

Другие случаи рекурсивных адаптеров :

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

Это уже реализовано в Zope:

IAbsoluteUrl
построение абсолютного URL к объекту или breadcrumbs;

Заключение :

Прочитав эту статью, вы должны были получить навык по программированию рекурсии при использовании адаптеров. Это очень важная техника, которой всегда следует отдавать предпочтение перед другими альтернативами, так как она позволяет легко воспользоваться всеми преимуществами компонентной модели. Кроме того, в силу специфики проблемы, в приведенном примере было построено два "полных" множества адаптеров - основного (IPath) и комплиментарного ему (ITitle). Подробнее о комплиментарных адаптерах можно прочитать здесь: HOWTO use adding and namechooser.txt.

Хочется особенно отметить первый пример в этой статье, функцию f(ob). Такой стиль программирования в условиях компонентной модели (и даже просто ООП) - прямая дорога в преисподню. В таких языках программирования как C++, его использование ограничивается самим языком, что спасает от ошибок. Python в этом отношении значительно более гибок и от ошибок спасет только строгое следования компонентной модели.


Powered by Plone CMS, the Open Source Content Management System