Питон - статьи

         

Что такое полиморфизм?


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

Например, существует множество функций, которые работают с объектами, "подобными файлам", где это подобие файлам определяется просто посредством поддержания нескольких методов, как .read(), .readlines() и, возможно, .seek(). Функция, как read_app_data(), может принимать аргумент src - когда мы вызовем эту функцию, мы, возможно, решим передать ей локальный файл, объект urllib, объект cStringIO или некий объект, определенный пользователем, который разрешает этой функции вызывать src.read(). Каждый тип объекта равнозначен с точки зрения того, как он функционирует в read_app_data().

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





Процедурный выбор ветвей кода по типу объекта


...bind 'src' in some manner... if <<src is a file object>
>
: read_from_file(src) elif <<src is a urllib object>
>
: read_from_url(src) elif <<src is a stringio object>
>
: read_from_stringio(src) ...etc...

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

Объект src привилегирован по отношению к любым аргументам, передаваемым в его методы. Из-за синтаксиса ООП эта привилегированность кажется неизбежной, но на самом деле это не так. Во многих случаях процедурное переключение просто переносится в тела методов классов. Например, мы могли бы реализовать совместимые по протоколу классы Foo и Bar следующим образом:





Реализация метода .meth() с помощью Foo и Bar


class Foo: def meth(self, arg): if <<arg is a Foo>
>
: ...FooFoo code block... elif <<arg is a Bar>
>
: ...FooBar code block... class Bar: def meth(self, arg): if <<arg is a Foo>
>
: ...BarFoo code block... elif <<arg is a Bar>
>
: ...BarBar code block... # Function to utilize Foo/Bar single-dispatch polymorphism def x_with_y(x, y): if <<x is Foo or Bar>
>
and <<y is Foo or Bar>
>
: 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), <<exception block>
>
)]) x_with_y.add_rule((Foo,Foo), <<FooFoo block>
>
) x_with_y.add_rule((Foo,Bar), <<FooBar block>
>
) x_with_y.add_rule((Bar,Foo), <<BarFoo block>
>
) x_with_y.add_rule((Bar,Bar), <<BarBar block>
>
) #...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 его выделяют, располагая его имя перед точкой - все, что следует за точкой: имя метода и левая скобка - просто аргумент. Но семантически этот объект является особенным при использовании дерева наследования для выбора метода.

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





Ресурсы


Вы можете получить multimethods как , либо как часть пакета Gnosis Utilities.

выходит как Питоновский пакет distutils.

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

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

В Perl есть модуль под названием Class::Multimethods, предназначенный для реализации множественной диспетчеризации (и, по-видимому, предполагается, что в Perl 6 эта концепция будет более глубоко встроена в язык). Дэмиан Конуэей рассматривает этот модуль ().

в зоне Linux developerWorks.



Улучшение наследования


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





Замечания выполнении в многонитевой среде


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