среда, 23 февраля 2011 г.

Python и цепная реакция и ... дескрипторы

А теперь мы научимся делать ядерную бомбу на Python ... Нет, не о том...

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

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

Простая задачка
Рассмотрим реализацию простого класса, назовем его "тупой охранник". Представим себе, что мы делаем компьютерную игру. У нас есть замок, а у ворот патрулирует охранник: ходит туда-сюда, больше ничего не делает. Код на Python 3.1:

class StupidGuard:
    """
    Stupid guard that moves around given route (way points).
    """
    direction = 0
    position = {'x': 0, 'y': 0}
  
    def go(self, steps):
        self.position['x'] += steps * cos(radians(self.direction))
        self.position['y'] += steps * sin(radians(self.direction))
    def rotate(self, angle):
        self.direction += angle
  
    def __repr__(self):
        return 'Guard: %s' % self.position

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

guard = StupidGuard()
print (guard)
guard.go(5)
guard.rotate(180)
guard.go(5)
guard.rotate(180)
print(guard)
Вот, что нам говорит Python о состоянии нашего человека в начале и конце пути:

Guard: {'y': 0, 'x': 0}
Guard: {'y': 6.123233995736766e-16, 'x': 0.0}
Итак, задачка сделана - охранник ходит. Только долго как-то ходит - 4 строчки кода. А хочется, чтобы он ходил не 4 строчки кода, а, например, одну :)

Цепные вызовы
Это в принципе и есть тот простой способ - цепные вызовы методов. Суть в том, что вышеприведенный код переписать так:
guard.go(5).rotate(180).go(5).rotate(180)
Нетрудно догадаться, что для этого нужно всего навсего сделать так, чтобы методы go и rotate возвращали self, а не None, как есть сейчас. То есть эти методы превращаются в такие:
def go(self, steps):
        self.position['x'] += steps * cos(radians(self.direction))
        self.position['y'] += steps * sin(radians(self.direction))
        return self
    def rotate(self, angle):
        self.direction += angle
        return self
Работает? Отлично работает. Но такой способ мне показался недостаточно гламурным и я решил сделать декоратор, назовем его Chained, чтобы каждый раз не возвращать self. Естественно, это код не сокращает, но выразительность такого кода гораздо выше, нежели return True. Декоратор-декоратором, а просто декоратором обойтись нельзя. Так как декоратор метода должен иметь доступ к экземпляру класса, - приходится писать дескриптор.

Пишем дескриптор и наводим марафет
Итак, дескриптор для этой задачи довольно простой и небольшой:
class Chained:
    """
        Descriptor makes method to always return its owner class instance
    """
  
    def __init__(self, method):
        self.method = method
      
    def __get__(self, instance, owner):
      
        def wrapper(*args, **kwargs):
            self.method(instance, *args, **kwargs)
            return instance
      
        return wrapper
Все, что он делает - оборачивает метод экземпляра в функцию, которая передает начальный код на исполнение, но возвращает экземляр класса, метод которого мы декорируем. Соответственно поменяется код нашего охранника:
class StupidGuard:
    """
    Stupid guard that moves around given route (way points).
    """
    direction = 0
    position = {'x': 0, 'y': 0}
  
    @Chained
    def go(self, steps):
        self.position['x'] += steps * cos(radians(self.direction))
        self.position['y'] += steps * sin(radians(self.direction))
    @Chained      
    def rotate(self, angle):
        self.direction += angle
  
    def __repr__(self):
        return 'Guard: %s' % self.position
Теперь мы с легкостью можем заставить его ходить уже двумя способами:
guard = StupidGuard()
print (guard)
guard.go(5)
guard.rotate(180)
guard.go(5)
guard.rotate(180)
print(guard)
guard.go(5).rotate(180).go(5).rotate(180)
print(guard)
Вот что интерпретатор сообщает о состоянии объекта:
Guard: {'y': 0, 'x': 0}
Guard: {'y': 6.123233995736766e-16, 'x': 0.0}
Guard: {'y': 1.224646799147353e-15, 'x': 0.0}
Ну вот и все
Ничего сложного и неочевидного, как видите, нет. Всего, чего, по-моему, можно добиться так это большей выразительности и читабельности кода. На более сложных примерах с разными рабочими процессами моделей это, пожалуй, будет выглядить более красиво.

4 комментария:

  1. > Декоратор-декоратором, а просто декоратором обойтись нельзя. Так как декоратор метода должен иметь доступ к экземпляру класса

    https://gist.github.com/eaca7df99f27cb643a6f

    ОтветитьУдалить
  2. @bsdemon
    Спасибо. С дескриптором переколдовал :)

    ОтветитьУдалить
  3. Rostislav, zdravstvuyte, ne mogu nayti vash kontakt - tel/e-mail . Proshu vas nabrat' +380632379995.
    Ivan

    ОтветитьУдалить
  4. Отличная статья, спасибо!

    ОтветитьУдалить

В этом гаджете обнаружена ошибка