Toly blog

Python and Django notes

Передовые паттерны проектирования в Python

Перевод статьи Advanced Design Patterns in Python

Цель данной статьи - показать передовые паттерны в Python и лучший способ их использовать. В зависимости от того, что вам нужно от структуры кода, будь то быстрый поиск, постоянство, индексация и т.д., вы можете выбрать оптимальную структуру для работы, и большую часть времени вы будете смешивать их вместе что бы получить логичную и простую для понимания модель данных. Паттерны в Python очень интуитивны с точки зрения синтаксиса и они предлагают большой выбор операций. Это руководство пытается собрать наиболее распространенную и полезную информацию о каждом паттерне, а так же советы о том когда лучше всего использовать тот или иной паттерн.

Генераторы

Если вы очень долго используете Python, скорее всего вы слышали о генераторах списков. Они подходят для цикла, блока if и способны уместить все это в одну строку. Другими словами, вы можете отобразить (map) и отфильтровать (filter) список одним выражением.

Генератор списка состоит из следующих частей:

  • входная последовательность
  • переменная, представляющая элементы входной последовательности
  • условие (опционально)
  • выходное выражение, полученное из элементов входного списка, которые удовлетворяют условию

Допустим, нам нужно получить список всех квадратов целых чисел, которые больше нуля:

1
2
3
4
5
6
7
8
9
num = [1, 4, -5, 10, -7, 2, 3, -1]
filtered_and_squared = []

for number in num:
    if number > 0:
        filtered_and_squared.append(number ** 2)
print filtered_and_squared

# [1, 16, 100, 4, 9]

Согласитесь, довольно просто? Но это занимает 4-е строчки, 2-у уровня вложенности, и вдобавок мы делаем довольно тривиальную вещь. Вы можете уменьшить количество кода с помощью функций filter, lambda и map:

1
2
3
4
5
num = [1, 4, -5, 10, -7, 2, 3, -1]
filtered_and_squared = map(lambda x: x ** 2, filter(lambda x: x > 0, num))
print filtered_and_squared

# [1, 16, 100, 4, 9]

Теперь код расширяется горизонтально! Что можно сделать, что бы упростить код? Применить генераторы списков.

1
2
3
4
5
num = [1, 4, -5, 10, -7, 2, 3, -1]
filtered_and_squared = [ x**2 for x in num if x > 0]
print filtered_and_squared

# [1, 16, 100, 4, 9]

Генератор списков заключен в квадратные скобки, таким образом, видно что список производится сразу. В этом генераторе списка только один вызов функции и нет вызовов загадочной lambda - используется обычный итератор, выходное выражение и опциональное условие.

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

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

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

1
2
3
4
5
6
7
8
9
10
num = [1, 4, -5, 10, -7, 2, 3, -1]
filtered_and_squared = ( x**2 for x in num if x > 0 )
print filtered_and_squared

# <generator object <genexpr> at 0x00583E18>

for item in filtered_and_squared:
    print item

# 1, 16, 100 4,9

Это даже немного эффективнее использования генераторов списков. Заменим пример более эффективным кодом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
num = [1, 4, -5, 10, -7, 2, 3, -1]

def square_generator(optional_parameter):
    return (x ** 2 for x in num if x > optional_parameter)

print square_generator(0)
# <generator object <genexpr> at 0x004E6418>

# Option I
for k in square_generator(0):
    print k
# 1, 16, 100, 4, 9

# Option II
g = list(square_generator(0))
print g
# [1, 16, 100, 4, 9]

Использование выражения-генератора вероятно более хорошая практика, но вы не увидите разницы в эффективности, если список не очень велик.

Так же, вы можете использовать функцию zip для работы с двумя и более элементами за раз:

1
2
3
4
5
6
7
8
9
alist = ['a1', 'a2', 'a3']
blist = ['1', '2', '3']

for a, b in zip(alist, blist):
    print a, b

# a1 1
# a2 2
# a3 3

Пример двухуровневого генератора с использованием os.walk():

1
2
3
4
5
6
7
8
import os
def tree(top):
    for path, names, fnames in os.walk(top):
        for fname in fnames:
            yield os.path.join(path, fname)

for name in tree('C:\Users\XXX\Downloads\Test'):
    print name

Декораторы

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

Декоратор - это функция, которая обертывает другую функцию: вызывается основная функция и ее результат передается в декоратор. Затем декоратор возвращает функцию, которая заменяет оборачиваемую функцию так как нужно.

1
2
3
4
5
6
7
8
9
10
def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    pass

@timethis
def countdown(n):
    while n > 0:
        n -= 1

Символ @ указывает на применение декоратора.

Теперь давайте реализуем код декоратора. Здесь мы фактически используем код декорируемой функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time
from functools import wraps

def timethis(func):
    '''
    Decorator that reports the execution time.
    '''
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(func.__name__, end-start)
        return result
    return wrapper

@timethis
def countdown(n):
    while n > 0:
        n -= 1

countdown(100000)

# ('countdown', 0.006999969482421875)

Когда вы пишете код вроде этого:

1
2
@timethis
def countdown(n):

это то же самое, как если бы выполнялось следующее

1
2
3
4
def countdown(n):
	...

countdown = timethis(countdown)

Код внитри декоратора обычно включает в себя создание новой функции, которая принимает любые аргументы (с использованием *args и **kwargs) как показано в случае с функцией wrapper в этом примере. Внутри этой функции вы принимает входные аргументы оригинальной функции и возвращаете результат. Однако, вы так же можете разместить там дополнительный код (например, замер времени и т.д.). Таким образов созданная функция-обертка возвращает результат, как если бы это была оригинальная функция.

Давайте рассмотрим другой пример:

1
2
3
@decorator
def function():
    print("inside function")

Когда компилятор проходит этот код, function() компилируется и получившийся объект-функция передается в код декоратора decorator, который создает из нее другой объект-функцию, что бы заменить первоначальную функцию function().

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

Единственное ограничение на результат возвращаемый декоратором, это то, что он может быть вызван как функция - т.е. что его можно вызвать. Таким образом, любые классы, которые мы используем в качестве декораторов должны быть с методом __call__.

Что должен делать декоратор после вызова? Вообще-то, все что угодно, но обычно ожидается, что будет использован код оригинальной функции. Однако, это не обязательно:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class decorator(object):

    def __init__(self, f):
        print("inside decorator.__init__()")
        f() # Prove that function definition has completed

    def __call__(self):
        print("inside decorator.__call__()")

@decorator
def function():
    print("inside function()")

print("Finished decorating function()")

function()

# inside decorator.__init__()
# inside function()
# Finished decorating function()
# inside decorator.__call__()

Практический пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def decorator(func):
    def modify(*args, **kwargs):
        variable = kwargs.pop('variable', None)
        print variable
        x,y=func(*args, **kwargs)
        return x,y
    return modify

@decorator
def func(a,b):
    print a**2,b**2
    return a**2,b**2

func(a=4, b=5, variable="hi")
func(a=4, b=5)

# hi
# 16 25
# None
# 16 25

ContextLib (менеджеры контекста)

Модуль contextlib содержит средства для работы с менеджерами контекста и оператором with. Обычно, что бы написать менеджер контекста, вы определяете класс с методами __enter__() и __exit__(). Например:

1
2
3
4
5
6
7
8
9
10
11
import time
class demo:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_ty, exc_val, exc_tb):
        end = time.time()
        print('{}: {}'.format(self.label, end - self.start))

Полный пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time

class demo:
    def __init__(self, label):
        self.label = label

    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_ty, exc_val, exc_tb):
        end = time.time()
        print('{}: {}'.format(self.label, end - self.start))

with demo('counting'):
    n = 10000000
    while n > 0:
        n -= 1

# counting: 1.36000013351

Менеджер контекста “включается” оператором with. Возвращаемый объект будет использоваться в контексте. Метод __enter__() выполняется, когда поток управления входит в блок кода внутри оператора with. Когда поток управления покидает блок кода внутри with, вызывается метод __exit__(), что бы очистить используемые ресурсы.

Заново напишем исходный пример, используя декоратор @contextmanager из модуля contextlib:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from contextlib import contextmanager
import time

@contextmanager
def demo(label):
    start = time.time()
    try:
        yield
    finally:
        end = time.time()
        print('{}: {}'.format(label, end - start))

with demo('counting'):
    n = 10000000
    while n > 0:
        n -= 1

# counting: 1.32399988174

В функции demo(label) весь код до оператора yield исполняется как метод менеджера контекста __enter__(). Весь код после оператора yield выполняется как метод __exit__(). Если в блоке внутри with возникнет исключение, оно “объявится” на месте оператора yield.

Дескрипторы

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

Что бы создать дескриптор, нужно определить хотя бы один из следующих трех методов. Обратите внимание, что instance - это объект, к аттрибуту которого меняется доступ, а owner - класс, для которого дескриптор является аттрибутом.

__get__(self, instance, owner) - вызывается, когда запрашивается аттрибут (value = obj.attr); то что возвращается будет передано коду, запрашивающему аттрибут.

__set__(self, instance, value) - вызывается, когда аттрибуту устанавливается значение (obj.attr = value); ничего не возвращает.

__delete__(self, instance) - вызывается, когда аттрибут объекта удалаяется (del obj.attr)

Ленивые аттрибуты:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import weakref

class lazyattribute(object):
    def __init__(self, f):
        self.data = weakref.WeakKeyDictionary()
        self.f = f
    def __get__(self, obj, cls):
        if obj not in self.data:
            self.data[obj] = self.f(obj)
        return self.data[obj]

class Foo(object):
    @lazyattribute
    def bar(self):
        print "Being lazy"
        return 42

f = Foo()

print f.bar
# Being lazy
# 42

print f.bar

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

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

Метаклассы

Метаклассы предлагают мощный способ изменить поведение классов в Python.

Метакласс определяется как “класса класса”. Любой класс, экземпляры которого являются сами классы, является метаклассом.

1
2
3
4
5
6
7
8
9
10
class demo(object):
    pass

obj = demo()

print "Class of obj is {0}".format(obj.__class__)
print "Class of obj is {0}".format(demo.__class__)

# Class of obj is <class '__main__.demo'>
# Class of obj is <type 'type'>

Мы создали класс и объект этого класса. Запросив у экземпляра аттрибут __class__, мы увидели, что это demo. Дальше интереснее. Что такое класс demo? У него мы тоже можем посмотреть аттрибут __class__ - это type.

Итак, type - это класс классов в Python. Другими словами, в приведенном выше примере obj - это объект класса demo, сам класс demo является объектом type.

Таким образом это делает type метаклассом - в действительности наиболее часто используемый метакласс в Python, т.к. это дефолтный метакласс для всех классов.

Т.к. метакласс - это класс классов, он используется для создания классов (тех, что создают объекты). Но подождите, не мы ли создаем классы, когда определяем их стандартным способом? Все верно, но то что делает Python “под капотом” выглядит так:

  • когда встречается определение класса, Python собирает аттрибуты (включая методы) в словарь
  • когда определение класса закончилось, Python определяет для него метакласс; давайте назовем его Meta
  • после Python выполняет Meta(name, bases, dct), где:
    • Meta - это метакласс, поэтому этот вызов создает его экземпляр
    • name - это имя только что созданного класса
    • bases - это кортеж базовых классов
    • dct - словарь, связывающий названия аттрибутов с объектами; в нем перечислены все аттрибуты класса

Как определить какой метакласс у класса? Если у класса (или один из его базовых классов) имеет аттрибут __metaclass__, то он считается метаклассом. В противном случае, метаклассом является type.

Паттерны

“Проще просить прощения, чем разрешения”

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

Синглтон (одиночка)

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

Null object

Null object может быть использован вместо None, что бы избежать проверки на None.

Обозреватель

Паттерн обозреватель позволяет нескольким объектам иметь доступ к общим данным.

Конструктор

Параметры конструктора часто назначаются переменным экземпляра. Этот паттерн может заменить много строк ручного присваивания одной строчкой.

Заключение

Спасибо за чтение. Оставляйте свои ​​комментарии для дальнейшего обсуждения.

Comments