4 must-have паттерна проектирования в Python

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


2019-07-20 22:00

разработка по

Пишете на Python и не знаете, с какого паттерна проектирования начать? В статье разбор популярных шаблонов с примерами кода на Python.

4 must-have паттерна проектирования на Python

Абстрактная фабрика

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

4 must-have паттерна проектирования на Python

Модуль стандартной Python библиотеки json иллюстрирует пример, когда требуется создание экземпляров объектов от имени вызывающей стороны. Рассмотрите строку JSON:

1

text='{"total": 9.61, "items": ["Americano", "Omelet"]}'

По умолчанию модуль json создаёт unicode объекты для строк типа "Americano", float – для 9.61, list – для последовательности элементов и dict – для ключей и значений объекта.

Но некоторым эти настройки по умолчанию не подходят. Например, бухгалтер против представления модулем json точной суммы «9 долларов 61 цент» в виде приближённого числа с плавающей запятой, и предпочёл бы вместо этого использовать экземпляр Decimal.

Это конкретный пример проблемы:

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

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

1

2

3

4

5

6

classFactory(object):

def build_sequence(self):

return[]

def build_number(self,string):

returnDecimal(string)

А вот загрузчик, что использует эту фабрику:

1

2

3

4

5

6

7

8

9

10

11

classLoader(object):

def load(string,factory):

sequence=factory.build_sequence()

forsubstring instring.split(','):

item=factory.build_number(substring)

sequence.append(item)

returnsequence

f=Factory()

result=Loader.load('1.23, 4.56',f)

print(result)

1

[Decimal('1.23'),Decimal('4.56')]

Во-вторых, отделите спецификацию от реализации путём создания абстрактного класса. Этот последний шаг оправдывает слово «абстрактный» в названии паттерна проектирования «Абстрактная фабрика». Ваш абстрактный класс гарантирует, что аргументом factory в load() будет класс, соответствующий требуемому интерфейсу:

1

2

3

4

5

6

7

8

9

10

11

from abc import ABCMeta,abstractmethod

classAbstractFactory(metaclass=ABCMeta):

@abstractmethod

def build_sequence(self):

pass

@abstractmethod

def build_number(self,string):

pass

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

Прототип

Паттерн проектирования «Прототип» предлагает механизм, с помощью которого вызывающая сторона предоставляет структуру с меню классов для создания экземпляра, когда пользователь или другой источник динамических запросов выбирает классы из меню выбора.

4 must-have паттерна проектирования на Python

Проще, если бы ни один класс в меню не нуждался в аргументах в __init__():

1

2

3

4

5

classSharp(object):

"The symbol ?."

classFlat(object):

"The symbol ?."

Вместо этого, в дело вступает паттерн «Прототип», когда требуется создание экземпляров классов с заранее заданными списками аргументов:

1

2

3

4

classNote(object):

"Musical note 1 ? `fraction` measures long."

def __init__(self,fraction):

self.fraction=fraction

Питонические решения

Питоническим подходом будет спроектировать классы исключительно с позиционными аргументами, без именованных. Затем легко хранить аргументы в виде кортежа, который предоставляется отдельно от самого класса. Это знакомый подход класса стандартной библиотеки Thread, который запрашивает вызываемый target= отдельно от передаваемых args=(...). Вот наши пункты меню:

1

2

3

4

5

6

7

menu={

'whole note':(Note,(1,)),

'half note':(Note,(2,)),

'quarter note':(Note,(4,)),

'sharp':(Sharp,()),

'flat':(Flat,()),

}

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

1

2

3

4

5

6

7

menu={

'whole note':(Note,1),

'half note':(Note,2),

'quarter note':(Note,4),

'sharp':(Sharp,),

'flat':(Flat,),

}

Затем структура будет вызывать каждый объект с использованием некоторой вариации tup[0](*tup[1:]).

Однако, возможно, классу потребуются не только позиционные аргументы, но и именованные. В ответ на это предоставьте простые вызываемые объекты, используя лямбда-выражения для классов, которые требуют аргументов:

1

2

3

4

5

6

7

menu={

'whole note':lambda:Note(fraction=1),

'half note':lambda:Note(fraction=2),

'quarter note':lambda:Note(fraction=4),

'sharp':Sharp,

'flat':Flat,

}

Хотя лямбда-выражения не поддерживают быструю интроспекцию для проверки, они хорошо работают, если структура только вызывает их.

Сам паттерн

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

# Чего избегает паттерн «Прототип»:

# создания фабрик для каждого класса.

classNoteFactory(object):

def __init__(self,fraction):

self.fraction=fraction

def build(self):

returnNote(self.fraction)

classSharpFactory(object):

def build(self):

returnSharp()

classFlatFactory(object):

def build(self):

returnFlat()

К счастью, ситуация не такая мрачная. Если перечитаете фабричные классы выше, то заметите, что каждый из них удивительно похож на целевые классы, которые хотим создать. Так же, как и Note, NoteFactory сам хранит атрибут fraction. Стек фабрик выглядит, как минимум, списками атрибутов, как стек создаваемых целевых классов.

Эта симметрия предлагает способ решения нашей проблемы без зеркалирования каждого класса с помощью фабрики. Что, если бы мы использовали сами исходные объекты для хранения аргументов и дали им возможность предоставлять новые экземпляры?

Результатом будет паттерн «Прототип», который напишем на Python с нуля. Все фабричные классы исчезают. Вместо этого у каждого объекта появляется метод clone(), на вызов которого он отвечает созданием нового экземпляра с полученными аргументами:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# Шаблон «Прототип»: научите каждый экземпляр

# объекта создавать копии самого себя.

classNote(object):

"Musical note 1 ? `fraction` measures long."

def __init__(self,fraction):

self.fraction=fraction

def clone(self):

returnNote(self.fraction)

classSharp(object):

"The symbol ?."

def clone(self):

returnSharp()

classFlat(object):

"The symbol ?."

def clone(self):

returnFlat()

Хотя пример и так иллюстрирует паттерн проектирования, при желании усложните его. Например, добавьте в каждом методе clone() вызов type(self) вместо жёсткого кодирования имени собственного класса для случая вызова метода в подклассе.

Компоновщик

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

4 must-have паттерна проектирования на Python

Реализация: наследовать или нет?

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

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

В других статических языках ограничение мягче. Нет строгой необходимости в том, чтобы контейнер и его содержимое делились реализацией. Пока оба соответствуют «интерфейсу», который объявляет конкретные общие методы, объекты вызываются симметрично.

Так как это программирование на Python, оба ограничения испаряются! Пишите код в предпочтительном для себя диапазоне безопасности и краткости. Хотите, пойдите классическим путём и добавьте общий суперкласс:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

classWidget(object):

def children(self):

return[]

classFrame(Widget):

def __init__(self,child_widgets):

self.child_widgets=child_widgets

def children(self):

returnself.child_widgets

classLabel(Widget):

def __init__(self,text):

self.text=text

Или задайте объектам один и тот же интерфейс. И положитесь на тесты, которые помогут поддерживать симметрию между контейнерами и содержимым. (Где для простейших скриптов ваш «тест» может быть фактом выполнения кода.)

1

2

3

4

5

6

7

8

9

10

11

12

13

classFrame(object):

def __init__(self,child_widgets):

self.child_widgets=child_widgets

def children(self):

returnself.child_widgets

classLabel(object):

def __init__(self,text):

self.text=text

def children(self):

return[]

Или выберите другой подход из спектра дизайна между этими двумя крайностями. Вот что поддерживает Python:

  • Следуйте классической архитектуре с общим суперклассом, показанной в первом примере выше.
  • Сделайте суперкласс абстрактным базовым классом с помощью инструментов модуля стандартной библиотеки abc.
  • Объявите для двух классов совместно используемый интерфейс, наподобие поддерживаемых старым пакетом zope.interface.
  • Применяйте аннотации для получения жёстких гарантий того, что и контейнер, и содержимое реализуют требуемое поведение. Для этого понадобится установка Python библиотеки проверки типов, к примеру, MyPy.
  • Вы в праве использовать утиную типизацию и не просить ни разрешения, ни прощения!

Поскольку Python предлагает такой спектр подходов, не стоит определять паттерн «Компоновщик» классически, то есть как один конкретный механизм (суперкласс) для создания или гарантирования симметрии. Вместо этого определите его как создание симметрии любыми средствами в иерархии объектов.

Итератор

Как реализовать паттерн проектирования «Итератор» и подключиться к встроенным итерационным механизмам языка Python for, iter() и next()?

4 must-have паттерна проектирования на Python
  • Добавьте в контейнер метод __iter__(), который возвращает объект итератора. Поддержка этого метода делает контейнер итерируемым.
  • Каждому итератору установите метод __next__() (в старом коде Python 2 next() записывали без двойного подчёркивания), который возвращает следующий элемент из контейнера при каждом вызове. Бросайте исключение StopIterator, когда больше нет элементов.
  • Помните, что некоторые пользователи передают в цикл for итераторы вместо основного контейнера? Чтобы обезопаситься в этом случае, каждому итератору также нужен метод __iter__(), который возвращает сам себя.

Посмотрите, как эти требования работают вместе, на примере нашего собственного итератора!

Обратите внимание, что не требуется, чтобы элементы, полученные в результате __next__(), сохранялись как постоянные значения внутри контейнера или даже присутствовали до вызова __next__(). Значит написать пример паттерна проектирования «Итератор» можно даже без реализации хранилища в контейнере:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

classOddNumbers(object):

"An iterable object."

def __init__(self,maximum):

self.maximum=maximum

def __iter__(self):

returnOddIterator(self)

classOddIterator(object):

"An iterator."

def __init__(self,container):

self.container=container

self.n=-1

def __next__(self):

self.n+=2

ifself.n>self.container.maximum:

raise StopIteration

returnself.n

def __iter__(self):

returnself

Благодаря этим трём методам – ??одному для объекта-контейнера и двум для его итератора – контейнер OddNumbers теперь полноправно участвует в богатой итерационной экосистеме языка программирования Python. Он будет работать без проблем с циклом for:

1

2

3

4

numbers=OddNumbers(7)

forninnumbers:

print(n)

1

2

3

4

1

3

5

7

И также работает со встроенными методами iter() и next().

1

2

3

it=iter(OddNumbers(5))

print(next(it))

print(next(it))

1

2

1

3

Он дружит даже с генераторами списков и множеств!

1

2

print(list(numbers))

print(set(nforninnumbers ifn>4))

1

2

[1,3,5,7]

{5,7}

Три простеньких метода – и вы разблокировали доступ к поддержке итераций на уровне синтаксиса Python.

Резюме

Теперь вы подружите Python с парой порождающих паттернов проектирования – абстрактной фабрикой и прототипом. Без труда реализуете структурный шаблон – прототип. По плечу вам имплементация поведенческого паттерна проектирования – итератора. Наверняка вас заинтересуют примеры других шаблонов проектирования на Python.

Как думаете, какого паттерна проектирования не хватает в статье?


Источник: proglib.io

Комментарии: