А теперь мы научимся делать ядерную бомбу на 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()Вот, что нам говорит Python о состоянии нашего человека в начале и конце пути:
print (guard)
guard.go(5)
guard.rotate(180)
guard.go(5)
guard.rotate(180)
print(guard)
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}
Ну вот и все
Ничего сложного и неочевидного, как видите, нет. Всего, чего, по-моему, можно добиться так это большей выразительности и читабельности кода. На более сложных примерах с разными рабочими процессами моделей это, пожалуй, будет выглядить более красиво.
> Декоратор-декоратором, а просто декоратором обойтись нельзя. Так как декоратор метода должен иметь доступ к экземпляру класса
ОтветитьУдалитьhttps://gist.github.com/eaca7df99f27cb643a6f
@bsdemon
ОтветитьУдалитьСпасибо. С дескриптором переколдовал :)
Rostislav, zdravstvuyte, ne mogu nayti vash kontakt - tel/e-mail . Proshu vas nabrat' +380632379995.
ОтветитьУдалитьIvan
Отличная статья, спасибо!
ОтветитьУдалить