Питон - статьи
Когда объявлять
Декларативный стиль программирования почти всегда более прямой способ задания ограничений, чем императивный или процедурный. Разумеется, не все проблемы программирования связаны с ограничениями - или, по крайней мере, не всегда естественно формулируются в таких терминах. Но проблемы систем, основанных на правилах, таких как грамматики и системы логического вывода, решаются много проще, если они могут быть описаны декларативно. Императивная верификация соответствия грамматике быстро превращается в трудноотлаживаемый код "спагетти". Утверждения шаблонов и правил могут быть гораздо проще.Конечно, по крайней мере в Python, верификация или применение объявленных правил всегда сводится к процедурным проверкам. Но надлежащие место для таких процедурных проверок - это хорошо оттестированный код библиотек. Отдельные приложения должны полагаться на более простой декларативный интерфейс, обеспечиваемый библиотеками, как Spark, PLY или gnosis.xml.validity. Другие библиотеки, как xmlproc, SimpleParse или ft.4xslt, также допускают декларативный стиль, хотя без объявлений на Python (что, разумеется, уместно для их области).
Пример системы линейных уравнений
10x + 5y - 7z + 1 = 0 17x + 5y - 10z + 3 = 0 5x - 4y + 3z - 6 = 0Это весьма элегантная запись, которая устанавливает отношения между объектами (x, y и z). Возможно, вы сталкивались с этими фактами в различных ситуациях реальной жизни, но на самом деле нахождение x вручную, на листке бумаги - это кропотливая работа, чреватая ошибками. Но написание этих шагов на Python, возможно, еще хуже - с точки зрения отладки.
Prolog - это язык, который приближается к логике и математике. В нем вы просто записываете утверждения, которые, как вы знаете, должны быть истинны, а затем требуете, чтобы приложение делало для вас выводы. Утверждения записываются вне какой-либо определенной последовательности (так же как и у линейных уравнений отсутствует порядок следования), и программист/пользователь не имеет ни малейшего представления о том, какие шаги участвуют в получении результатов. Например:
Объявления правил gnosis.xml.validity
from gnosis.xml.validity import * class figure(EMPTY): pass class _mixedpara(Or): _disjoins = (PCDATA, figure) class paragraph(Some): _type = _mixedpara class title(PCDATA): pass class _paras(Some): _type = paragraph class chapter(Seq): _order = (title, _paras) class dissertation(Some): _type = chapterВы могли бы создать экземпляры из этих объявлений используя:
ch1 = LiftSeq(chapter, ("1st Title","Validity is important")) ch2 = LiftSeq(chapter, ("2nd Title","Declaration is fun")) diss = dissertation([ch1, ch2]) print diss
Заметьте, как близко эти классы соответствуют предыдущему DTD. Это отображение в основном "один к одному", с тем исключением, что необходимо использовать промежуточные имена для определение числа и чередования вложенных тегов (промежуточные имена помечены начальным символом подчеркивания).
Также обратите внимание, что, будучи созданы с использованием стандартного синтаксиса Python, эти классы являются необычными (и более лаконичными) в том, что не имеют ни методов, ни данных экземпляра. Классы определяются исключительно, чтобы наследовать некую структуру, причем эта структура ограничена единственным атрибутом класса. Например,
является последовательностью других тегов, а именно:
, за которым следует один или более тегов
. Но все, что нам необходимо сделать, чтобы обеспечить выполнение этого ограничение в экземпляре, это лишь объявить класс chapter.
Главная "хитрость", используемая при программировании таких родительских классов, как gnosis.xml.validity.Seq, это рассмотреть атрибут .__class__ экземпляра во время инициализации. Класс chapter не имеет собственной инициализации, поэтому вызывается метод __init__() его родителя. Но self, передаваемый в родительский __init__(), является экземпляром chapter, и он это знает. В качестве иллюстрации рассмотрим фрагмент реализации gnosis.xml.validity.Seq:
Класс gnosis.xml.validity.Seq
class Seq(tuple): def __init__(self, inittup): if not hasattr(self.__class__, '_order'): raise NotImplementedError, \ "Child of Abstract Class Seq must specify order" if not isinstance(self._order, tuple): raise ValidityError, "Seq must have tuple as order" self.validate() self._tag = self.__class__.__name__Если разработчик приложения пытается создать экземпляр chapter, код реализации контролирует, что chapter был объявлен с необходимым атрибутом класса ._order, и этот атрибут является кортежем. Метод .validate() выполняет еще несколько проверок, чтобы убедиться, что объекты, с которыми был инициализирован этот экземпляр, принадлежат соответствующим классам, указанным в ._order.
Пример на языке Prolog - family.pro
/* Adapted from sample at:This app can answer questions about sisterhood & love, e.g. (Это приложение может ответить на вопросы о родственных отношениях и любви, например): # Is alice a sister of harry? # (Алиса - сестра Гарри?) ?-sisterof( alice, harry ) # Which of alice' sisters love wine? # (Кому из сестер Алисы нравится вино?) ?-sisterof( X, alice ), love( X, wine) */ sisterof( X, Y ) :- parents( X, M, F ), female( X ), parents( Y, M, F ). parents( edward, victoria, albert ). parents( harry, victoria, albert ). parents( alice, victoria, albert ). female( alice ). loves( harry, wine ). loves( alice, wine ).
Не совсем идентично, но схоже по духу объявление грамматики EBNF (Extended Backus-Naur Form, Расширенная форма Бэкуса-Наура). Вы могли бы записать несколько следующих объявлений:
Пример для EBNF
word := alphanums, (wordpunct, alphanums)*, contraction? alphanums := [a-zA-Z0-9]+ wordpunct := [-_] contraction := "'", ("clock"/"d"/"ll"/"m"/"re"/"s"/"t"/"ve")Это компактный способ определения того, как могло бы выглядеть слово (word), если бы вы столкнулись с ним, без предоставления в действительности последовательных инструкций о том, как его узнать. На это похожи и регулярные выражения (и, фактически, для этой конкретной грамматической продукции их достаточно).
В качестве еще одного примера рассмотрим объявление типа документа, которое описывает диалект допустимых XML-документов:
Объявление типа XML-документа
Как и в других примерах, язык DTD не содержит никаких инструкций о том, что делать, чтобы распознать или создать допустимый XML-документ. Он просто описывает, каким мог бы быть документ, если бы он должен был существовать. Для декларативных языков характерно сослагательное наклонение.
Пример PLY
tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION','WHITSPACE') t_ALPHANUMS = r"[a-zA-Z0-0]+" t_WORDPUNCT = r"[-_]" t_CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)" def t_WHITESPACE(t): r"\s+" t.value = " " return t import lex lex.lex() lex.input(sometext) while 1: t = lex.token() if not t: breakЯ написал о PLY в моей готовящейся к публикации книге "Текстовая обработка в Python" ("Text Processing in Python") и рассказывал о Spark в этой рубрике (см. ). Не вдаваясь в подробности об этих библиотеках, замечу, что все, на что здесь следует обратить внимание, это то, что разбор (в данном примере, лексическое сканирование) непосредственно конфигурируется Питоновскими объявлениями. Просто модуль PLY знает достаточно, чтобы действовать на основании этих шаблонных описаний.
Как именно PLY выясняет, что делать, подразумевает весьма причудливое программирование на Python. На начальном этапе программист среднего уровня поймет, что можно исследовать содержимое словарей globals() и locals(). Было бы неплохо, если бы стиль объявления был слегка другим. Представьте, например, что код был бы таким:
Использование пространства имен импортированного модуля
import basic_lex as _ _.tokens = ('ALPHANUMS','WORDPUNCT','CONTRACTION') _.ALPHANUMS = r"[a-zA-Z0-0]+" _.WORDPUNCT = r"[-_]" _.CONTRACTION = r"'(clock|d|ll|m|re|s|t|ve)" _.lex()Этот стиль не стал бы ничуть менее декларативным, а модуль basic_lex гипотетически мог бы содержать что-нибудь простое вроде:
basic_lex.py
def lex(): for t in tokens: print t, '=', globals()[t]Это сгенерировало бы:
% python basic_app.py ALPHANUMS = [a-zA-Z0-0]+ WORDPUNCT = [-_] CONTRACTION = '(clock|d|ll|m|re|s|t|ve)
PLY ухитряется проникнуть в пространство имен импортирующего модуля, используя информацию о кадре стека. Например:
magic_lex.py
import sys try: raise RuntimeError except RuntimeError: e,b,t = sys.exc_info() caller_dict = t.tb_frame.f_back.f_globals def lex(): for t in caller_dict['tokens']: print t, '=', caller_dict['t_'+t]Это производит такой же результат, как и полученный в примере basic_app.py, но с объявлениями, использующими предыдущий стиль t_TOKEN.
В реальном модуле PLY присутствует больше волшебства. Мы видели, что лексемы, обозначенные шаблоном t_TOKEN, могут на самом деле быть либо строками, содержащими регулярные выражения, либо функциями, которые включают строки документации (docstring) регулярных выражений вместе с действующим кодом. Некий контроль типов обеспечивает полиморфное поведение:
polymorphic_lex
# ...determine caller_dict using RuntimeError... from types import * def lex(): for t in caller_dict['tokens']: t_obj = caller_dict['t_'+t] if type(t_obj) is FunctionType: print t, '=', t_obj.__doc__ else: print t, '=', t_objРазумеется, реальный модуль PLY делает кое-что более интересное, используя продемонстрированные подходы, чем эти игрушечные примеры, но и они демонстрируют некоторые задействованные технологии.
Магия интроспекции
Парсеры Spark и PLY разрешают пользователям объявлять Питоновские значения в Python, а затем используют немного магии, чтобы позволить среде исполнения Python выступать в качестве конфигуратора разбора. Рассмотрим, например, эквивалент предшествующей грамматики SimpleParse, описанный с помощью PLY. (пример для Spark почти аналогичен):Магия наследования
Разрешив библиотеке поддержки манипулировать пространством имен приложения, можно реализовать элегантный декларативный стиль. Но часто использование структур наследования вместе с интроспекцией позволяет достичь даже большей гибкости.Модуль gnosis.xml.validity - это библиотека для создания классов, которые отображаются прямо на результирующие DTD. Любой класс gnosis.xml.validity может быть создан только с аргументами, подчиняющимися ограничениям допустимости диалекта XML. На самом деле это не совсем верно; этот модуль также будет выводить правильные типы из более простых аргументов, если существует только один непротиворечивый способ "достроить" тип до корректного состояния.
Поскольку модуль gnosis.xml.validity написал я сам, я склонен считать, что его предназначение само по себе интересно. Но в этой статье я лишь хочу рассмотреть декларативный стиль, в котором создаются классы допустимости. Набор правил/классов, соответствующих предыдущему шаблону DTD, состоит из:
Программирование как утверждение, а не как инструкция
Объектно-ориентированные и прозрачные интроспективные возможности Python позволяют легко создавать декларативные мини-языки для задач программирования. В этой статье Дэвид рассматривает не столько использование Python для интерпретации и транслирования других специализированных языков (хотя такое возможно), сколько то, как сам Питоновский код может быть удобно сведен к ряду декларативных элементов. Он покажет, как разработчики могут пользоваться декларативной технологией, чтобы четко и ясно устанавливать требования к приложениям, позволяя при этом "закулисной" структуре выполнять тяжелую работу.Когда большинство программистов размышляет о программировании, при написании приложений они представляют себе императивные стили и технологии. Наиболее популярные языки программирования общего назначения - в том числе Python и другие объектно-ориентированные языки - превалирующе императивны по стилю. С другой стороны, существует множество языков программирования, которые декларативны по стилю, включая и функциональные, и логические языки, а также языки специализированные и общего назначения.
Позвольте мне перечислить несколько языков, которые относятся к различным категориям. Многие читатели уже используют большинство этих инструментов, не задумываясь о категориальных различиях между ними. Python, C, C++, Java, Perl, Ruby, Smalltalk, Fortran, Basic, xBase - всё это просто императивные языки программирования. Некоторые из них являются объектно-ориентированными, но это всего лишь вопрос организации кода и данных, а не основного стиля программирования. В этих языках вы даете программе команды выполнить последовательность инструкций: разместить некие данные в переменную; выбрать данные обратно из переменной; исполнить группу инструкций цикла пока не выполнено некоторое условие; сделать что-либо, если что-то истинно (true). Прелесть всех этих языков заключается в том, что о них удобно думать в рамках знакомых житейских метафор. Обычная жизнь состоит из выполнения одного действия, осуществления выбора, а затем исполнения другого действия, возможно с использованием некоторых инструментов. Легко представить себе компьютер, выполняющий программу, как повара, или каменщика, или шофера.
Языки, похожие на грамматики Prolog, Mercury, SQL, XSLT, EBNF, а на самом деле и конфигурационные файлы различных форматов, - все они объявляют, что имеет место ситуация, или что применяются определенные ограничения. Функциональные языки (такие как Haskell, ML, Dylan, Ocaml, Scheme) - подобны, однако, с приданием большего значения формулированию внутренних (функциональных) отношений между программными объектами (рекурсией, списками и т.д.). Наша обычная жизнь, по крайней мере, ее описательная сторона, не имеет прямого аналога программных конструкций этих языков. Однако, для тех проблем, которые вы легко можете описывать на этих языках, декларативные описания гораздо более лаконичны и в значительно меньшей степени подвержены ошибкам по сравнению с императивными решениями. Рассмотрим, например, систему линейных уравнений:
Python как интерпретатор в сравнении с Python как средой
Библиотеки Python могут использовать декларативные языки одним или двумя совершенно различными способами. Возможно, более общий подход - это разобрать и обработать непитоновские декларативные языки как данные. Приложение или библиотека может считать внешний источник (или строку, определенную внутренне, но только как "блоб"), затем определить ряд императивных шагов для выполнения, которые некоторым образом соответствуют внешним объявлениям. В сущности, эти типы библиотек являются системами, "управляемыми данными"; существует концептуальный и категориальный разрыв между декларативным языком и тем, что приложение Python делает, чтобы выполнить или использовать его объявления. К тому же, как правило, библиотеки, обрабатывающие эти идентичные объявления, реализуются также и для других языков программирования.Все приведенные выше примеры относятся к этому первому подходу. Библиотека PyLog - это Питоновская реализация системы Prolog. Она читает Прологовский файл данных как шаблон, затем создает объекты Python для моделирования объявлений Prolog. Пример для EBNF использует специфический вариант SimpleParse, который является библиотекой Python, преобразующей эти объявления в таблицы состояний, которые могут использоваться mx.TextTools. Сам mx.TextTools - это библиотека расширений для Python, которая, будучи написана на C, исполняет код, хранимый в структурах данных Python, но имеет весьма отдалённое отношение к собственно Python. Python - это великолепная склейка для таких задач, но сами склеенные языки сильно отличаются от Python. Большинство реализаций Prolog к тому же написано на языках, отличных от Python, как и большинство парсеров для EBNF.
DTD подобен остальным примерам. Если вы используете валидирующий парсер, как xmlproc, вы можете воспользоваться DTD, чтобы проверить диалект XML-документа. Однако язык DTD "непитоновский", и xmlproc просто использует его в качестве данных, которые необходимо разобрать. Более того, валидирующие парсеры XML написаны на многих языках программирования. Подобно этому и преобразование XSLT - оно "непитоновское", а такой модуль, как ft.4xslt, просто использует Python как связующее средство.
Хотя в упомянутых выше подходах и инструментах нет ничего дурного (я постоянно их использую), было бы более элегантно - и в некоторых отношениях более выразительно - если бы сам Python мог быть декларативным языком. Тогда библиотеки, которые выполняли бы это, не требовали бы от программистов думать о двух (или более) языках при написании одного приложения. Иногда естественно и обоснованно изучить интроспективные возможности Python, чтобы реализовать "родные" объявления.
Ресурсы
Создание декларативных мини-языков
Автор: Дэвид Мертц (David Mertz), разработчик, Gnosis Software Inc.Перевод:
Авторские права:
Что такое полиморфизм?
Большинство программистов, использующих полиморфизм - на Python или других языках объектно-ориентированного программирования -, находят ему весьма практическое и конкретное применение. Возможно, наиболее общий случай использования полиморфизма - это создание семейства объектов, которые придерживаются общего протокола. В Python это обычно просто вопрос нерегламентированного полиморфизма; в других языках чаще объявляются формальные интерфейсы, и/или эти семейства обладают общим предком.Например, существует множество функций, которые работают с объектами, "подобными файлам", где это подобие файлам определяется просто посредством поддержания нескольких методов, как .read(), .readlines() и, возможно, .seek(). Функция, как read_app_data(), может принимать аргумент src - когда мы вызовем эту функцию, мы, возможно, решим передать ей локальный файл, объект urllib, объект cStringIO или некий объект, определенный пользователем, который разрешает этой функции вызывать src.read(). Каждый тип объекта равнозначен с точки зрения того, как он функционирует в read_app_data().
Давайте вернемся немного назад, чтобы понять, что здесь действительно происходит. По существу, нас интересует, как выбрать надлежащую ветвь кода для выполнения в контексте; старомодный процедурный код может принимать эквивалентные решения, ООП просто придает элегантность. Например, фрагмент процедурного (псевдо) кода мог бы выглядеть следующим образом:
Процедурный выбор ветвей кода по типу объекта
...bind 'src' in some manner... if <>
: read_from_file(src) elif <
>
: read_from_url(src) elif <
>
: read_from_stringio(src) ...etc...
Организовав поддержку общих методов объектами различных типов, мы перемещаем решение о диспетчеризации в объекты из явного условного блока. Просматривая дерево наследования, данный объект src узнает, какие блоки кода ему нужно вызывать. Однако, по-прежнему происходит неявное переключение, но по типу объекта src.
Объект src привилегирован по отношению к любым аргументам, передаваемым в его методы. Из-за синтаксиса ООП эта привилегированность кажется неизбежной, но на самом деле это не так. Во многих случаях процедурное переключение просто переносится в тела методов классов. Например, мы могли бы реализовать совместимые по протоколу классы Foo и Bar следующим образом:
Реализация метода .meth() с помощью Foo и Bar
class Foo: def meth(self, arg): if <>
: ...FooFoo code block... elif <
>
: ...FooBar code block... class Bar: def meth(self, arg): if <
>
: ...BarFoo code block... elif <
>
: ...BarBar code block... # Function to utilize Foo/Bar single-dispatch polymorphism def x_with_y(x, y): if <
>
and <
>
: x.meth(y) else: raise TypeError,"x, y must be either Foo's or Bar's"
Имеется пять различных ветвей/блоков кода, которые могут выполняться при вызове x_with_y(). Если типы x и y не подходят, возбуждается исключение (разумеется, вы могли бы сделать что-нибуль другое). Но, предполагая, что с типами все в порядке, ветвь кода выбирается сначала посредством полиморфной диспетчеризации, а затем посредством процедурного переключения. Кроме того, переключения внутри определений Foo.meth() и Bar.meth() в значительной степени эквивалентны. Полиморфизм - в разновидности с единичной диспетчеризацией - решает лишь половину задачи.
Множественная диспетчеризация Foo и Bar
x_with_y = Dispatch([((object, object), <>
)]) x_with_y.add_rule((Foo,Foo), <
>
) x_with_y.add_rule((Foo,Bar), <
>
) x_with_y.add_rule((Bar,Foo), <
>
) x_with_y.add_rule((Bar,Bar), <
>
) #...call the function x_with_y() using some arguments... x_with_y(something, otherthing)
Я думаю, что эта симметричность полиморфной диспетчеризации по множеству аргументов гораздо более элегантна, чем предшествующий стиль. Кроме того, этот стиль позволяет документировать одинаковую роль этих двух объектов, задействованных в определении подходящей ветви кода.
Стандартный Python не разрешает конфигурировать этот тип множественной диспетчеризации; но, к счастью, вы можете сделать это, воспользовавшись написанным мною модулем multimethods. См. , чтобы скачать этот модуль отдельно или в составе утилит Gnosis. После того, как вы установили multimethods, все, что от вас требуется - включить в начало своего приложения следующую строку:
from multimethods import Dispatch
"Мультиметоды", как правило, это синоним множественной диспетчеризации; но термин мультиметод предполагает конкретную функциональную/объектную реализацию более абстрактной концепции множественной диспетчеризации.
Экземпляр Dispatch - это вызываемый объект, его можно конфигурировать с любым желаемым количеством правил. К тому же, можно использовать метод Dispatch.remove_rule(), чтобы удалять правила; благодаря этому множественная диспетчеризация с использованием multimethods становится несколько более динамичной, чем статическая иерархия классов (но вы также можете совершить некие замысловатые действия с классами Python во время исполнения). Также заметьте, экземпляр Dispatch может принимать переменное число аргументов; сопоставление выполняется сначала по числу аргументов, затем по их типам. Если экземпляр Dispatch вызывается с любым шаблоном, который не определен в правиле, возбуждается TypeError. Инициализация x_with_y() с запасным шаблоном (object,object) необязательна, если вы просто хотите, чтобы в неопределенных ситуациях возбуждалось исключение.
Каждый кортеж (pattern,function), перечисленный в инициализации Dispatch, просто передается далее в метод .add_rule();
это исключительно вопрос удобства программирования - устанавливать правила при инициализации или позже (можно комбинировать подходы, как в предшествующем примере). При вызове функции из диспетчера аргументы, используемые при вызове, передаются диспетчеру; вы должны обеспечить, чтобы функция, которую вы используете, могла принять то число аргументом, с которым она сопоставляется. Например, приведенные ниже вызовы эквиваленты:
Явный вызов и вызов функции при диспетчеризации
# Define function, classes, objects def func(a,b): print "The X is", a, "the Y is", b class X(object): pass class Y(object): pass x, y = X(), Y()# Explicit call to func with args func(x,y)
# Dispatched call to func on args from multimethods import Dispatch dispatch = Dispatch() dispatch.add_rule((X,Y), func) dispatch(x,y) # resolves to 'func(x,y)'
Очевидно, что если вы знаете типы x и y во время проектирования, алгоритм задания диспетчера - просто накладные расходы. Но то же ограничение справедливо и для полиморфизма - он удобен, лишь когда вы не можете ограничить объект единственным типом для каждой ветви исполнения.
Наследование для расширения возможностей
# Base classes class Circle(Shape): def combine_with_circle(self, circle): ... def combine_with_square(self, square): ... class Square(Shape): def combine_with_circle(self, circle): ... def combine_with_square(self, square): ... # Enhancing base with triangle shape class Triangle(Shape): def combine_with_circle(self, circle): ... def combine_with_square(self, square): ... def combine_with_triangle(self, triangle): ... class NewCircle(Circle): def combine_with_triangle(self, triangle): ... class NewSquare(Square): def combine_with_triangle(self, triangle): ... # Can optionally use original class names in new context Circle, Square = NewCircle, NewSquare # Use the classes in application c, t, s = Circle(...), Triangle(...), Square(...) newshape1 = c.combine_with_triangle(t) newshape2 = s.combine_with_circle(c) # discover 'x' of unknown type, then combine with 't' if isinstance(x, Triangle): new3 = t.combine_with_triangle(x) elif isinstance(x, Square): new3 = t.combine_with_square(x) elif isinstance(x, Circle): new3 = t.combine_with_circle(x)В частности, каждый существующий класс фигуры должен добавлять возможности потомку, что приводит к комбинаторной сложности и трудностям при сопровождении.
Напротив, метод множественной диспетчеризации более прост:
Мультиметоды для расширения возможностей
# Base rules (stipulate combination is order independent) class Circle(Shape): pass class Square(Shape): pass def circle_with_square(circle, square): ... def circle_with_circle(circle, circle): ... def square_with_square(square, square): ... combine = Dispatch() combine.add_rule((Circle, Square), circle_with_square) combine.add_rule((Circle, Circle), circle_with_circle) combine.add_rule((Square, Square), square_with_square) combine.add_rule((Square, Circle), lambda s,c: circle_with_square(c,s)) # Enhancing base with triangle shape class Triangle(Shape): pass def triangle_with_triangle(triangle, triangle): ... def triangle_with_circle(triangle, circle): ... def triangle_with_square(triangle, square): ... combine.add_rule((Triangle,Triangle), triangle_with_triangle) combine.add_rule((Triangle,Circle), triangle_with_circle) combine.add_rule((Triangle,Square), triangle_with_square) combine.add_rule((Circle,Triangle), lambda c,t: triangle_with_circle(t,c)) combine.add_rule((Square,Triangle), lambda s,t: triangle_with_square(t,s)) # Use the rules in application c, t, s = Circle(...), Triangle(...), Square(...) newshape1 = combine(c, t)[0] newshape2 = combine(s, c)[0] # discover 'x' of unknown type, then combine with 't' newshape3 = combine(t, x)[0]Определение новых правил (и поддержка функций/методов) в значительной степени эквивалентны. Но огромное преимущество стиля множественной диспетчеризации - это цельность, с помощью которой вы комбинировать фигуры неизвестных типов. Вместо того, чтобы возвращаться к явным (и длинным) условным блокам, определения правил автоматически решают эти вопросы. Что еще лучше, все комбинирование выполняется одним вызовом combine(), а не с помощью "зверинца" из разных комбинирующих методов.
Автоматическое воспроизведение диспетчеризации
class General(object): pass class Between(General): pass class Specific(Between): pass dispatch = Dispatch() dispatch.add_rule((General,), lambda _:"Gen", AT_END) dispatch.add_rule((Between,), lambda _:"Betw", AT_END) dispatch.add_rule((Specific,), lambda _:"Specif", AT_END) dispatch(General()) # Result: ['Gen'] dispatch(Specific()) # Result: ['Specif', 'Betw', 'Gen']Разумеется, в некоторых ситуациях (как для правила (General)) менее специфичное правило отсутствует. Для обеспечения единообразия, однако, каждое обращение к диспетчеру возвращает список значений из всех функций, которым передается управление таким образом. Если в правиле не определены ни AT_END, ни AT_START, распространение вызовов не производится (и возвращается список из одного элемента). Этим объясняется индекс [0] в примере с фигурами, который, вероятно, кажется загадочным .
Для тонкой настройки распространения вызовов применяется метод диспетчера .next_method(). Чтобы задать распространение вызовов вручную, нужно использовать для определения правил метод .add_dispatchable(), а не метод .add_rule(). Кроме того, диспетчеризованные функции сами должны принимать аргумент dispatch. При вызове диспетчера вы либо должны передать аргумент, задающий диспетчер, либо вы можете воспользоваться вспомогательным методом .with_dispatch(). Например:
Программирование с ручной передачей
def do_between(x, dispatch): print "do some initial stuff" val = dispatch.next_method() # return simple value of up-call print "do some followup stuff" return "My return value" foo = Foo() import multimethods multi = multimethods.Dispatch() multi.add_dispatchable((Foo,), do_between) multi.with_dispatch(foo) # Or: multi(foo, multi)Вызов менее специфичных мультиметодов вручную может оказаться запутанным - примерно так же, как и обращение к методам базовых классов. Чтобы эти вопросы стали управляемыми, обращение к .next_method() всегда возвращает простой результат вызова верхнего уровня - если вы хотите собрать такие результаты в список, как тот, что создает аргумент AT_END, вам нужно добавлять и обрабатывать те величины, которые вы считаете уместными. Наиболее общий вариант использования, однако - выполнение последовательности связанных инициализаций; в этом случае возвращаемые величины обычно неважны.
Клонирование для безопасности нити
def threadable_dispatch(dispatcher, other, arguments) dispatcher = dispatcher.clone() #...do setup activities... dispatcher(some, rule, pattern) #...do other stuff...Если внутри threadable_dispatch() не запускаются новые нити, все нормально.
Вам потребуется некоторое время, чтобы освоиться с идей множественной диспетчеризации, даже - или особенно - если вы весьма опытны в объектно-ориентированном программировании. Но после того, как вы немного с ней поэкспериментируете, вероятно, вы обнаружите, что множественная диспетчеризация обобщает и усиливает преимущества, которыми ООП обладает прежде всего над процедурным программированием.
Множественная диспетчеризация
Автор: Дэвид Мертц (David Mertz)Перевод:
Авторские права:
Обобщение полиморфизма с помощью мультиметодов
Во многом универсальность объектно-ориентрованного программирования (ООП) возможна благодаря полиморфизму: в надлежащем контексте объекты разных видов могут вести себя различным образом. Однако бПередача диспетчеризации
Не испытывая необходимости больше думать о диспетчеризации, класс multimethods.Dispatch будет выбирать "наилучшее совпадение" для данного обращения к диспетчеру. Однако, иногда стоит заметить, что "лучшее" не значит "единственное". То есть, обращение к dispatch(foo,bar) может давать точное совпадение с правилом (Foo,Bar) - но оно также может задавать менее точное совпадение (не промах!) для (FooParent,BarParent). Точно так, как иногда вы хотите вызывать методы базовых классов в методе производного класса, вы также иногда желаете вызывать менее специфические правила в диспетчере.Модуль multimethods позволяет задавать вызовы менее специфических правил как грубо, так и с тонкой настройкой. На грубом уровне, обычно вы просто хотите автоматически вызывать менее специфичное правило в начале, либо в конце выполнения блока кода. Подобным образом вы практически всегда вызываете метод надкласса в начале, либо в конце тела метода потомка. Общий вариант начального/конечного вызова менее специфичных методов может быть задан просто как часть правила. Например:
Полная реализация полиморфизма
В случае полиморфизма с единичной диспетчеризацией выделяется объект, который "владеет" методом. Синтаксически в Python его выделяют, располагая его имя перед точкой - все, что следует за точкой: имя метода и левая скобка - просто аргумент. Но семантически этот объект является особенным при использовании дерева наследования для выбора метода.А что если бы мы обрабатывали особым образом не один объект, а позволили бы каждому объекту, задействованному в блоке кода, участвовать в выборе ветви выполнения? Например, мы могли бы выразить наше пятистороннее переключение более симметрично:
Ресурсы
Улучшение наследования
Множественная диспетчеризация не просто обобщает полиморфизм, она предоставляет более гибкую альтернативу наследованию во многих контекстах. Рассмотрим в качестве иллюстрации следующий пример. Предположим, что вы пишете программу построения чертежей или автоматизированного проектирования, которая работает с различными фигурами (shape); в частности, вы хотите, чтобы вы могли комбинировать две фигуры таким образом, чтобы результат зависел от обеих задействованных фигур. Кроме того, набор рассматриваемых фигур будет расширяться производными приложениями или подключаемыми библиотеками. Расширение набора классов фигур является неизящным подходом при модернизации, например:Замечания выполнении в многонитевой среде
Стоит привести краткое замечание, пока читатель не столкнулся с проблемой. Из-за необходимости сохранения состояния для отслеживания, какие (последовательно менее специфичные) правила вызывались, диспетчер не является нитебезопасным. Если нужно использовать диспетчер в многонитевой среде, необходимо "клонировать" его для каждой нити. Это ненакладно с точки зрения ресурсов: памяти и процессора, так что клонирование диспетчеров не вызывает существенных неудобств. Например, предположим, что функция могла бы вызываться из разных нитей; вы можете написать:Практика
Итак, погружаемся в WSGI. Пишем простенькое WSGI-приложение:def app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return ['Hello here']
Все достаточно просто - как и говорилось выше, приложение принимает в качестве аргументов словарь переменных окружения (environ) и исполняемый объект выполнения запроса (start_response). Далее, посылаем начало ответа серверу и возвращаем сам ответ в виде итератора (в данном случае - в виде обычного списка).
Теперь встает вопрос о запуске нашего приложения. Для этого воспользуемся библиотекой wsgiref. Счастливчики с Python 2.5 в этом месте широко улыбаются, потому что у них wsgiref уже есть. Запускаем так:
from wsgiref import simple_server server = simple_server.WSGIServer( ('', 8080), simple_server.WSGIRequestHandler, ) server.set_app(app) server.serve_forever()
Тоже все достаточно просто - создаем объект сервера со стандартным обработчиком, задаем ему порт 8080 для ожидания соединений, указываем какое WSGI-приложение выполнять и запускаем сервер.
Пока что преимущества WSGI не ощущаются.
Теперь усложним задачу. Попробуем написать такой сервер, который бы работал с произвольным WSGI-приложением, и приложение, которое бы работало с произвольным WSGI-сервером. Что ж, приступим.
В начале определю, что значит "произвольный": скрипту, который реализует тот или иной компонент, передается в качестве аргумента "путь" к другому, парному, компоненту. И пусть они взаимодействуют. Чтобы не усложнять код, я написал маленький модуль, helper, который и делает всю "машинерию" по преобразованию полного имени компонента (пакет.модуль.объект) в компонент-объект. Итак, наше "тривиальное приложение" стало выглядеть так:
def app(environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) sorted_keys = environ.keys() sorted_keys.sort() result = ['
TrivialWSGIApp in action
'] + ['Sample WSGI application.Just show your environment.
'] + ['
'] return result
if __name__ == '__main__': import sys import helper
server = helper.get_arg(sys.argv, "Usage: trivial_wsgi_app.py package.wsgi.server_callable") server(app)
Немного "усложнили" приложение - теперь оно показывает доступные переменные окружения, ну и плюс код, запускающий парный компонент - WSGI-сервер, переданный как параметр.
А "тривиальный сервер" стал выглядеть так:
from wsgiref import simple_server, validate
class TrivialWSGIServer(object): def __init__(self, app): self.app = app self.server = simple_server.WSGIServer( ('', 8080), simple_server.WSGIRequestHandler, ) self.server.set_app(validate.validator(self.app))
def serve(self): self.server.serve_forever()
def runner(app): TrivialWSGIServer(app).serve()
if __name__ == '__main__': import sys import helper
app = helper.get_arg(sys.argv, "Usage: trivial_wsgi_server.py package.wsgi.app") runner(app)
У него добавились: "исполнитель" runner, чтобы в один шаг запускать приложение на запуск и код для запуска парного компонента - WSGI-приложения. Отмечу одну из "прослоек" (middleware), которая здесь используется - validator - проверяет, что "диалог" между сервером и приложением идет в рамках стандарта.
Запуск осуществляется следующим образом:
trivial_wsgi_app.py trivial_wsgi_server.runner
или так:
trivial_wsgi_server.py trivial_wsgi_app.app
Усложняем задачу. Теперь напишем WSGI-сервер средствами Twisted, но с таким же "интерфейсом запуска"
from twisted.internet import reactor from twisted.web2 import wsgi, channel, server
class TwistedWSGIServer(object): def __init__(self, app): self.app = app self.wsgi_res = wsgi.WSGIResource(app) self.site = server.Site(self.wsgi_res) self.factory = channel.HTTPFactory(self.site)
def serve(self): reactor.listenTCP(8080, self.factory) reactor.run()
def runner(app): TwistedWSGIServer(app).serve()
if __name__ == '__main__': import sys import helper
app = helper.get_arg(sys.argv, "Usage: twisted_wsgi_server.py package.wsgi.app") runner(app)
Здесь мы воспользовались WSGI-сервером, встроенным в Twisted Web2, ну а процедура старта Twisted-приложения описана здесь.
Теперь пробуем запустить с нашим приложением:
twisted_wsgi_server.py trivial_wsgi_app.app
Работает. Еще больше усложним задачу и напишем Nevow-приложение с WSGI-интерфейсом (правда, с некоторыми оговорками):
from nevow import rend, loaders, wsgi, tags, inevow
class NevowPage(rend.Page):
addSlash = True
docFactory = loaders.stan( tags.html[ tags.head[tags.title['Nevow WSGI hello app']], tags.body[ tags.h1(id='title')['Nevow WSGI hello app'], tags.p(id='welcome')['Welcome to the Nevow (WSGI powered). Just show your environment.'], tags.p(id='environment')[tags.invisible(render=tags.directive('environ'))] ] ] )
def render_environ(self, context, data): environ = inevow.IRequest(context).environ sorted_keys = environ.keys() sorted_keys.sort() inner = [tags.li[k, " => ", str(environ[k])] for k in sorted_keys] return tags.ul[inner]
app = wsgi.createWSGIApplication(NevowPage())
if __name__ == '__main__': import sys import helper
server = helper.get_arg(sys.argv, "Usage: nevow_wsgi_app.py package.wsgi.server_callable") server(get_wsgi_app())
Особо углубляться в код не буду, тем более, что есть желание сделать Nevow одной из тем разговора.
Теперь можно комбинировать WSGI-сервера и WSGI-приложения в любых сочетаниях - результат будем идентичным. Естественно, что часть возможностей, которые не укладываются в WSGI, будут недоступны. Напримерб в Twisted Web2, WSGI-приложение выполняется в отдельном потоке, так что воспользоваться асинхронными "фишками" Twisted не получится. Поэтому использовать Nevow с Twisted через WSGI - нонсенс. Об использовании Twisted в веб-приложениях, я думаю, расскажу в ближайшее время.А приведенный код можно получить с code.google.com.
Теория
WSGI - стандарт обмена данными между веб-сервером (backend) и веб-приложением (frontend). Под это определение попадают многие вещи, тот же самый CGI. Так что поясню.Во-первых, WSGI - Python-специфичный стандарт, его описывает PEP 333. Во-вторых, он еще не принят (статус Draft, черновик). Эти оговорки для того, чтобы не испытывать лишних иллюзий. Между тем, стандарт нужный и уже используемый. Для меня WSGI - это в первую очередь возможность комбинировать различные back- и frontend’ы.
Теперь, что касается самого стандарта. Он описывает интерфейсы веб-приложения и веб-сервера.
Приложение - принимает в качестве параметров переменные окружения (в виде словаря) и исполняемый объект выполнения запроса. Возвращает итератор.
Сервер - тут чуть сложнее. В переменных окружения, к стандартным переменным веб-сервера, добавляются WSGI-специфичные. Особо останавливаться я на этом не буду, сошлюсь лишь на все тот же PEP 333, где приведен пример. Просто-напросто реализация на стороне сервера меня интересует постольку-поскольку, поэтому на ней не задерживаюсь.
Прослойка, middleware - самое интересное. Middleware "работает" в обе стороны. Т.е. у нее входной и выходной интерфейс идентичны. Я бы провел аналогию с декоратором. Middleware добавляет некую функциональность в исходное веб-приложение, например live debug, или http auth. Причем, можно выстраивать цепочки middleware.
Теперь попробуем все это на практике…
WSGI, введение
,С аббревиатурой WSGI я столкнулся, когда возникла задача развертывания Django-приложения, а mod_python у меня что-то не захотел работать. И в то время для меня WSGI было неким buzz-word, туманным и далеким. Так или иначе, Django я "завел" при помощи flup и lighttpd, но "виски" засел у меня занозой в мозгу.
Вспомнилась эта заноза не так давно, когда я стал читать блог Бена Бангерта Groovie. Бен апологет WSGI и создатель веб-фреймворка Pylons (надеюсь, у меня будет время рассказать о нем). Отправной точкой в моем "погружении" стали статьи на XML.com. А мотивом к написанию этой статьи стал тот факт, что информации по WSGI на русском просто нет. Что ж, постараюсь восполнить этот пробел.
и эффективное решение проблемы взаимодействия
WSGI достаточно простое и эффективное решение проблемы взаимодействия веб-сервера и веб-приложения. Как любой компромисс, он не идеален "везде и всюду", однако для большинства случаев - это разумный выбор. Некоторые используют WSGI не только как стандарт взаимодействия веб-сервера и веб-приложения, но и обособленных библиотек между собой. Возможно, в чем то этот подход оправдан.Еще о магии классов
То, что ограничение воплощения слабее ограничения наследования, существенно для разработки специальных методов, как .__new__(), .__init__(), .__str__() и т.д. Рассмотрим метод .__str__(), анализ для других специальных методов -проводится аналогично.Читатели, вероятно, знают, что печатаемое представление объекта класса можно модифицировать, подменив его метод .__str__(). В том же смысле печатаемое представление класса можно модифицировать, подменив метод .__str__() его метакласса. Например:
Конфликты метаклассов
Стоит вам всерьез поработать с метаклассами, и вы хотя бы раз столкнетесь с конфликтом метаклассов/метатипов. Рассмотрим класс A с метаклассом M_A и класс B с метаклассом M_B; предположим, что мы производим C от A и B. Возникает вопрос: что является метаклассом C? M_A или M_B?Правильный ответ - M_C, где M_C - это метакласс, который наследуется от M_A и M_B, как показано на следующей диаграмме (см. ниже в разделе Ресурсы
ссылку на книгу "Использование метаклассов" (Putting metaclasses to work)):

Рис. 4. Предотвращение конфликта метаклассов
Однако, Python автоматически не создает (пока) M_C. Вместо этого он возбуждает исключение TypeError, предупреждая программиста о конфликте:
Типичная иерархия наследования
>>
>
class A(object): a1 = "A" ... >
>
>
class B(object): a2 = "B" ... >
>
>
class C(A,B): a3 = "C(A,B)" ... >
>
>
class D(C): a4 = "D(C)" ... >
>
>
d = D() >
>
>
d.a5 = "instance d of D"
Мы может протестировать это отношение:
Метаметоды и методы класса
>>
>
class M(Printable): ... def mm(cls): ... return "I am a metamethod of %s" % cls.__name__ ... >
>
>
class C(object): ... __metaclass__=M ... def cm(cls): ... return "I am a classmethod of %s" % cls.__name__ ... cm=classmethod(cm) ... >
>
>
c=C()
Частично эта путаница вызвана тем, что C.mm в терминологии Smalltalk назывался бы "методом класса C". Однако методы класса Python - нечто совсем иное.
Метаметод "mm" может быть вызван либо из метакласса, либо из класса, но не из экземпляра. Метод класса может быть вызван и из класса, и из его экземпляров (но не существует в метаклассе).
Вызов метаметода
>>
>
print M.mm(C) I am a metamethod of C >
>
>
print C.mm() I am a metamethod of C >
>
>
print c.mm() [...] AttributeError: 'C' object has no attribute 'mm' >
>
>
print C.cm() I am a classmethod of C >
>
>
print c.cm() I am a classmethod of C
Кроме того, метаметод извлекается dir(M), а не dir(C), в то время как метод класса извлекается dir(C) и dir(c).
Вы можете вызывать только методы метакласса, которые определены в порядке разрешения метода класса, выполнив диспетчеризацию по метаклассу (встроенные функции, как print, делают это неявно):
Магический метод метакласса
>>
>
print C.__str__() [...] TypeError: descriptor '__str__' of 'object' object needs an argument >
>
>
print M.__str__(C) This is class C
Важно заметить, что этот конфликт диспетчеризации не ограничен магическими методами. Если мы изменим C, добавив атрибут C.mm, возникнет та же проблема (не имеет значения, является ли имя регулярным методом, методом класса, статическим методом или простым атрибутом):
Немагический метод метакласса
>>
>
C.mm=lambda self: "I am a regular method of %s" % self.__class__ >
>
>
print C.mm() [...] TypeError: unbound method
() must be called with C instance as first argument (got nothing instead)
Конфликты метаклассов
>>
>
class M_A(type): pass ... >
>
>
class M_B(type): pass ... >
>
>
class A(object): __metaclass__ = M_A ... >
>
>
class B(object): __metaclass__ = M_B ... >
>
>
class C(A,B): pass # Error message less specific under 2.2 [...] TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
Конфликта метаклассов можно избежать, вручную создав необходимый метакласс для C:
Разрешение конфликта метаклассов вручную
>>
>
M_AM_B = type("M_AM_B", (M_A,M_B), {}) >
>
>
class C(A,B): __metaclass__ = M_AM_B ... >
>
>
type(C)
Разрешение конфликтов метаклассов становится более сложным, если вы желаете "вставить" дополнительные метаклассы в класс вслед за используемыми его предками. Кроме того, в зависимости от метаклассов родительских классов могут появиться избыточные метаклассы - и идентичные метаклассы в различные предках, и отношения базовый класс/производный класс среди метаклассов. Модуль noconflict предоставляет пользователям автоматическое и надежное решение этих проблем (см. Ресурсы).
Тестирование родословной
>>
>
issubclass(D,C) True >
>
>
issubclass(D,A) True >
>
>
issubclass(A,B) False >
>
>
issubclass(d,D) [...] TypeError: issubclass() arg 1 must be a class
А теперь интересный вопрос, необходимый для понимания различия между базовыми классами и метаклассами: как разрешается атрибут наподобие d.attr. Для простоты, рассмотрим только стандартное правило просмотра, а не "сваливание" в .__getattr__(). Первый шаг в таком разрешении - поискать в d.__dict__ имя attr. Если оно найдено - это все; но если нет, должно произойти что-то фантастическое, как, например:
>
>
>
d.__dict__, d.a5, d.a1
({'a5': 'instance d'}, 'instance d', 'A')
Хитрость, позволяющая найти атрибут, который не прикреплен к экземпляру - поискать его в классе экземпляра, а после этого во всех базовых классах. Порядок, в котором просматриваются производные классы, называется порядком разрешения метода (method resolution order) для этого класса. Вы можете увидеть его с помощью (мета)метода .mro() (но только из объектов класса):
>
>
>
[k.__name__ for k in d.__class__.mro()]
['D', 'C', 'A', 'B', 'object']
Другими словами, обращение к d.attr сначала ищет в d.__dict__, а затем в D.__dict__, C.__dict__, A.__dict__, B.__dict__ и в конце в object.__dict__. Если имя не найдено ни в одном из этих мест, возбуждается исключение AttributeError.
Заметьте, что в этой процедуре поиска метаклассы не были упомянуты ни разу.
Наследование атрибутов
>>
>
for s in "Power Wealth Beauty".split(): exec '%s="%s"'%(s,s) ... >
>
>
class Noble(object): # ...in fairy tale world ... attributes = Power, Wealth, Beauty ... >
>
>
class Prince(Noble): ... pass ... >
>
>
Prince.attributes ('Power', 'Wealth', 'Beauty')
Класс Prince наследует атрибут от Noble. Экземпляр класса Prince по-прежнему придерживается последовательности поиска, рассмотренной выше:
Атрибуты в экземплярах
>>
>
charles=Prince() >
>
>
charles.attributes # ...remember, not the real world ('Power', 'Wealth', 'Beauty')
Если оказалось, что у класса Duke есть метакласс, определенный пользователем, он может получить некоторые атрибуты следующим образом:
>
>
>
class Nobility(type): attributes = Power, Wealth, Beauty
...
>
>
>
class Duke(object): __metaclass__ = Nobility
...
Кроме того, что Duke - класс, он является экземпляром метакласса Nobility - поиск атрибутов происходит как и с любым объектом:
>
>
>
Duke.attributes
('Power', 'Wealth', 'Beauty')
Но Nobility не является базовым классом Duke, поэтому нет причин, почему экземпляр класса Duke нашел бы Nobility.attributes:
Атрибуты и метаклассы
>>
>
Duke.mro() [
,
] >
>
>
earl = Duke() >
>
>
earl.attributes [...] AttributeError: 'Duke' object has no attribute 'attributes'
Доступность атрибутов метакласса не является транзитивной, другими словами, атрибуты метакласса доступны его экземплярам, но не экземплярам экземпляров. Именно это и есть главное различие между метаклассами и базовыми классами. Следующая диаграмма подчеркивает ортогональность наследования и воплощения:

Рис. 1. Воплощение и наследование
Поскольку у earl по-прежнему есть класс, вы можете, однако, не напрямую отыскать этот атрибут:
>
>
>
earl.__class__.attributes
На рисунке 1 противопоставляются простые случаи, когда используется либо наследование, либо задействованы метаклассы, но не обе концепции одновременно. Иногда, однако, у класса C есть и класс M, определенный пользователем, и базовый класс B:
Комбинирование базового класса и метакласса
>>
>
class M(type): ... a = 'M.a' ... x = 'M.x' ... >
>
>
class B(object): a = 'B.a' ... >
>
>
class C(B): __metaclass__=M ... >
>
>
c=C()
Графически:

Рис. 2. Комбинированные базовый класс и метакласс
Согласно предшествующему объяснению, мы могли бы представить, что C.a разрешился бы либо в M.a, либо в B.a. Оказывается, поиск по классу следует его порядку разрешения метода до того, как он осуществляется в его метаклассе:
Разрешение метаклассов и базовых классов
>>
>
C.a, C.x ('B.a', 'M.x') >
>
>
c.a 'B.a' >
>
>
c.x [...] AttributeError: 'C' object has no attribute 'x'
Вы все же можете задать величину атрибута, используя метакласс - вам просто нужно указать ее для воплощаемого объекта класса, а не в качестве атрибута метакласса.
Задание атрибута в метаклассе
>>
>
class M(type): ... def __init__(cls, *args): ... cls.a = 'M.a' ... >
>
>
class C(B): __metaclass__=M ... >
>
>
C.a, C().a ('M.a', 'M.a')
Настройка вывода класса на печатающее устройство
>>
>
class Printable(type): ... def __str__(cls): ... return "This is class %s" % cls.__name__ ... >
>
>
class C(object): __metaclass__ = Printable ... >
>
>
print C # equivalent to print Printable.__str__(C) This is class C >
>
>
c = C() >
>
>
print c # equivalent to print C.__str__(c)
Эту ситуацию можно представить с помощью следующей диаграммы:

Рис. 3. Метаклассы и магические методы
Из предыдущего обсуждения ясно, что метод .__str__() в Printable не может заменить метод .__str__() в C, который наследуется из object и, следовательно, обладает приоритетом; печать C по-прежнему дает стандартный результат.
Если бы C наследовал свой метод .__str__() из Printable, а не из object, это породило бы проблему: у экземпляров C нет атрибута .__name__, и печать C сгенерировала бы ошибку. Разумеется, вы по-прежнему могли бы определить метод .__str__() в C, что изменило бы то, как печатается C.
Метаклассы: что мы не рассмотрели в первой статье
В нашей первой статье о программировании метаклассов на Python мы представили концепцию метаклассов, показали некоторые их возможности и продемонстрировали, как их использовать при решении таких проблем, как динамическая настройка классов и библиотек во время исполнения.Эта статья оказалась довольно популярной, но в своем сжатом заключении мы кое-что упустили. А определенные тонкости использования метаклассов заслуживают дополнительного разъяснения. Опираясь на читательский отклик и на обсуждение, развернувшееся на comp.lang.python, в этой статье мы рассмотрим некоторые более каверзные вопросы. В частности, мы считаем, что следующие моменты важны для любого программиста, желающего овладеть метаклассами:
Метаклассы или предки
Ниже приведен простой пример обычного наследования. Мы определяем базовый класс Noble с производными классами, как, например, Prince, Duke, Baron и т.д.Методы класса в сравнении с метаметодами
Другая путаница происходит между методами класса Python и методами, определенными в метаклассе, которые лучше называть метаметодами.Рассмотрим пример:
Ресурсы
Воплощение (instantiation) или наследование (inheritance)
Многие программисты недопонимают различие между метаклассом и базовым классом. На поверхностном уровне "определения класса" они оба кажутся одинаковыми. Но если посмотреть глубже, концепции расходяться.Прежде чем рассмотреть примеры, стоит определиться с терминологией. Экземпляр - это объект Python, который был "произведен" классом; класс действует как разновидность шаблона для экземпляра. Каждый экземпляр - это экземпляр только одного класса (но класс может иметь множество экземпляров). То, что мы часто называем экземпляром класса (instance object) - или, возможно, "простым экземпляром" - является "окончательным" в том смысле, что он не может выступать в качестве шаблона для других объектов (но он все равно может быть фабрикой или делегатом (delegate), которые используются для частично совпадающих целей).
Некоторые экземпляры - сами по себе классы; а все классы являются экземплярами соответствующего метакласса. Даже классы появляются исключительно посредством механизма воплощения. Обычно классы - это экземпляры встроенного, стандартного метакласса type; только когда мы задаем метаклассы, отличные от type, нам нужно думать о программировании метаклассов. Мы также называем класс, используемый для воплощения объекта, типом (type) этого объекта.
Ортогональной к идее воплощения является понятие наследования. В данном случае у класса может быть один или множество родителей, а не только уникальный тип. Родители могут иметь родителей, создавая транзитивное отношение производных классов, которое легко можно извлечь с помощью встроенной функции issubclass(). Например, если мы определим несколько классов и экземпляр:
В этой статье рассматривается ряд
В этой статье рассматривается ряд предостережений и непростых ситуаций. Для работы с метаклассами требуется определенное количество проб и ошибок, прежде чем их поведение станет интуитивным. Однако, эти вопросы никоим образом не безнадежны - эта довольно краткая статья затрагивает большинство подводных камней. Поэкспериментируйте с этими ситуациями сами. И к концу дня вы обнаружите, что метаклассы предоставляют новую степень программного обобщения; выигрыш сторицей окупает небольшой риск.
Программирование: Языки - Технологии - Разработка
- Программирование
- Технологии программирования
- Разработка программ
- Работа с данными
- Методы программирования
- IDE интерфейс
- Графический интерфейс
- Программирование интерфейсов
- Отладка программ
- Тестирование программ
- Программирование на Delphi
- Программирование в ActionScript
- Assembler
- Basic
- Pascal
- Perl
- VBA
- VRML
- XML
- Ada
- Lisp
- Python
- UML
- Форт
- Языки программирования