Оригинал этой статьи опубликован в журнале «Системный администратор» №9 (130) за сентябрь 2013.
Как известно, в языке Python существует красивый механизм декораторов, расширяющих функционал объекта без изменения интерфейса. Это довольно мощное средство, попользоваться им удобно и приятно. Но вот проблема: наш язык программирования — Ruby!
На самом деле никакой проблемы нет, и в Ruby достаточно возможностей, чтобы решать подобные задачи не менее эффективно, чем в конкурирующих технологиях.
... ... ...
Универсальность всегда увеличивает сложность и накладные расходы. Так что мое мнение: жили мы без декораторов в Ruby и еще поживем. Тем не менее сама методика декорирования кода, безусловно, заслуживает внимания и может с успехом применяться в самых разных задачах.
Декораторы в Ruby
Декоратор в общем смысле — шаблон проектирования, предусматривающий динамическое подключение дополнительной функциональности к некоторому уже имеющемуся объекту без изменения его интерфейса. Старая функциональность оказывается как бы обернута в новую.
Декоратор в языке программирования Python — специальная синтаксическая конструкция, «оборачивающая» заданную функцию с использованием ранее определенной функции-обертки. Оборачивающая функция принимает в качестве аргументов заданную и, возможно, какие-то дополнительные параметры, которые в дальнейшем указываются при использовании декоратора, и возвращает замещающую функцию.
Простой пример:
def log(f): def wrapped(*args, **kwargs): print(f) return f(*args, **kwargs) return wrapped @log def test(): print("Test") test()
Идея этой статьи навеяна постом на Хабре о декораторах в языке Python1, а также некоторыми другими материалами в сети на ту же тему. Авторы данных материалов явно гордятся такой возможностью языка, что прямо таки заставляет правоверного рубиста выяснить, а как обстоят дела с аналогами в любимом языке. Конечно, надо понимать, что при всей близости по времени появления, объектно-ориентированности и области применения, Python и Ruby все же разные языки, соответственно, аналоги получаются не один к одному. Но можно говорить о схожих решениях схожих задач. Если мы откроем вышеупомянутый хабрапост, то увидим, в частности:
«Для того, чтобы понять, как работают декораторы, в первую очередь следует осознать, что в Python'е функции — это тоже объекты»
И тут уже приходится остановиться: в Ruby, во-первых, нет функций, есть только методы, а во-вторых, методы объектами не являются. Все плохо? Ничего подобного. Возможностей метапрограммирования в Ruby более чем достаточно для того, чтобы реализовать аналогичную функциональность. Это и будет предметом настоящей статьи.
Пара слов о том, зачем это надо: способы применения могут быть самые разные — от ведения логов и замеров производительности до проверки прав пользователя или каких-то еще сложных условий перед выполнением каждого действия. Причем, обрамлять своими проверками мы можем любые методы, в том числе принадлежащие классам стандартной библиотеки языка, или библиотек, разрабатываемых третьими лицами. Поскольку исходный код имеющихся библиотек не модифицируется, мы не создаем себе никаких препятствий к установке обновлений и исправлений, что важно для проектов с длительным жизненным циклом.
Блоки, методы и объекты Proc
Здесь и далее я описываю сложные, но хорошо документированные, особенности языка применительно к основной теме статьи, кратко, не всегда формально точно и, по возможности, просто. Для изучения языка лучше всего обратиться к более-менее официальным2 и неофициальным3 руководствам.
Итак, что мы имеем на уровне языка? Во-первых, это, конечно, методы. В принципе — те же функции, но определенные для класса и исполняющиеся в контексте объекта. Замечу, что метод всегда определен для какого-то класса, даже если формально он задан непосредственно объекту: так называемые синглтон-методы это методы так называемых синглтон-классов. Методы, определенные в глобальном контексте принадлежат классу Object
. С помощью метода method
можно получить для данного метода объект класса Method
(«Шишков, прости...»), который уже есть полноценный ruby-объект.
Во-вторых, это блоки — их можно рассматривать как анонимные функции, образующие замыкание с тем контекстом, в котором они определены. При этом язык так устроен, что определяем блок мы всегда, передавая его в некий метод, где он доступен, помимо особых средств языка, и как объект класса Proc
.
Одним из таких методов, принимающих блоки, является метод класса Module
define_method
, который волшебным образом превращает блок в метод. С другой стороны, объект класса Proc
, а также любой объект, для которого определен метод to_proc
, в том числе и класса Method
, может быть передан в качестве блока посредством префикса &
.
Демонстрация:
def show x, &block puts block.call(x) end def alpha x x + 1 end a = method(:alpha) puts a.inspect puts a.call(0) b = proc { |x| x + 2 } puts b.inspect Object.send :define_method, :beta, &b puts beta(0) show 10, &a show 10, &b show 10 do |x| x + 3 end
На выходе должны получить что-то вроде этого:
$ ruby deco2.rb #<Method: Object#alpha> 1 #<Proc:0x00000000af1298@deco1.rb:14> 2 11 12 13
Здесь мы из метода alpha
получили объект, а метод beta
наоборот, создали из объекта. «Object.send :define_method
» вместо «Object.define_method
» пришлось использовать потому, что define_method
— приватный.
Теперь, определившись, что у нас есть, можно перейти к рассмотрению того, что можно из этого сделать...
Шаг первый — функция-обертка
Очевидно, что там, где в Python функция, принимающая и возвращающая функцию, у нас будет метод, принимающий объект класса Proc
, а лучше — для универсальности — блок, и возвращающий объект класса Proc
.
Давайте напишем обертку-логгер:
def wrap &block proc do |*args, &blk| begin result = block.call *args, &blk $stderr.puts "OK: #{args.inspect}" + " => #{result.inspect}" result rescue Exception => e $stderr.puts "ERROR! #{args.inspect} => #{e.inspect}" raise end end end
И протестируем ее:
alpha = proc { |x, y| x / y } a = wrap &alpha z1 = a.call 4, 2 z2 = a.call 4, 0
Получаем (в предположении, что определение обертки и тестирующий код находятся в файле deco2.rb
, как у меня4):
$ ruby deco2.rb OK: [4, 2] => 2 ERROR! [4, 0] => #<ZeroDivisionError: divided by 0> deco2.rb:16:in `/': divided by 0 (ZeroDivisionError) from deco2.rb:16:in `block in <main>' from deco2.rb:6:in `call' from deco2.rb:6:in `block in wrap' from deco2.rb:21:in `call' from deco2.rb:21:in `<main>'
Здесь хорошо видно, что мы получили исключение, вывели информацию о нем и отправили его дальше по стеку вызовов. Это сделано затем, чтобы и в плане исключений обертка вела себя так же, как исходная функция.
Чтобы в начале и конце у нас был метод, требуется добавить в общем-то немного:
def wrap_method name Object.send :define_method, name, &(wrap &(method name)) end
Получаем метод, создаем обертку и на ее основе переопределяем метод с тем же именем. Проверяем как-то так:
def alpha x, y x / y end wrap_method :alpha alpha 4, 2 alpha 4, 0
Шаг второй — в правильном контексте
Примеры выше даны для глобального контекста, в котором, однако, на практике методы определяют редко. Нормальное расположение методов — в контексте класса или модуля. Или попросту модуля, поскольку класс в данном случае есть его разновидность (очень специфическая, но все же).
Поместим метод wrap
в явном виде в класс Object
(на самом деле, он и так там, просто сделаем определение более ясным) и заставим его принимать не только блоки, но и непосредственно объекты класса Method
, чтобы иметь доступ к имени метода, а также класса UnboundMethod
, чтобы оперировать методами, не привязанными к конкретному объекту.
class Object private def wrap meth = nil, &block func = meth || block name = (meth && meth.name) || '' proc do |*args, &blk| if UnboundMethod === func func = func.bind self end begin result = func.call *args, &blk $stderr.puts "OK: #{self.inspect}.#{name}" + " #{args.inspect} => #{result.inspect}" result rescue Exception => e $stderr.puts "ERROR! #{self.inspect}.#{name}" + " #{args.inspect} => #{e.inspect}" raise end end end def wrap_singleton_method *names names.each do |name| define_singleton_method name, &wrap(method name) end end end class Module def wrap_method *names names.each do |name| define_method name, &wrap(instance_method name) end end end
И попробуем:
class Fixnum wrap_method :+, :- end z0 = 4 + 2 z1 = 4 - 2 def mul x, y x * y end wrap_singleton_method :mul z2 = mul 4, 2
Получим:
$ ruby deco4.rb OK: 4.+ [2] => 6 OK: 4.- [2] => 2 OK: main.mul [4, 2] => 8
Нетрудно заметить, что использование wrap_method
выглядит весьма похоже на стандартные модификаторы private
, protected
и public
. Давайте еще усилим эту похожесть (а заодно и похожесть на python-декораторы) — при вызове без параметров, метод будет действовать на все последующие определения. Модифицируем wrap_method
:
def wrap_method *names if names.length != 0 @ignore_wrap = true names.each do |name| define_method name, &wrap(instance_method name) end @ignore_wrap = false else ma = method :method_added define_singleton_method :method_added do |name| wrap_method name unless @ignore_wrap ma.call name if ma end end end
Метод класса Module
method_added
вызывается при любом определении метода. Чтобы не уйти в бесконечный цикл, нам приходится дополнительно ввести флаг, говорящий о том, что текущее определение — это наша обертка, которую заново оборачивать не нужно.
Кстати, здесь мы вместо того, чтобы перекрыть метод method_added
создаем (опять же) над ним обертку. Сделано это затем, чтобы нам не помешали его возможные переобъявления. Проверим на следующем коде:
class Alpha class << self def method_added name puts "method_added: #{name}" end end def alpha p :alpha end wrap_method def beta p :beta end def gamma p :gamma end end a = Alpha.new a.alpha a.beta a.gamma
Должно получиться на выходе:
$ ruby deco5.rb method_added: alpha method_added: beta method_added: beta method_added: gamma method_added: gamma :alpha :beta OK: #<Alpha:0x00000001e7c510>.beta [] => :beta :gamma OK: #<Alpha:0x00000001e7c510>.gamma [] => :gamma
Аналогично можно модифицировать и wrap_singleton_method, если очень хочется.
Шаг третий — фабрика генераторов
Ну и наконец, давайте решим задачу в более-менее общем виде. Пусть у нас будет способ генерировать различные «декораторы», задав имя и блок, возвращающий proc-обертку. Блок будет принимать UnboundMethod
и произвольные именованные параметры. Только именованные, поскольку неименованный список мы будем вместе с ними передавать при вызове декоратора — это имена декорируемых методов, как и в примерах выше. В python-декораторах так делать не принято, зато в Ruby подобное сплошь и рядом.
Для упрощения и сокращения кода далее я использую синтаксическую конструкцию для задания именованных параметров, которая появилась только в Ruby версии 2.0 (предыдущие примеры полностью работоспособны и в 1.9). Сделать тоже самое в предыдущих версиях вполне реально, но несколько длиннее.
Вот такое короткое определение:
class Module def decorator name, &wrapper define_singleton_method name do |*names, **opts| if names.length != 0 @ignore_wrap = true names.each do |nm| define_method nm, &wrapper.call(instance_method(nm), **opts) end @ignore_wrap = false else ma = method :method_added define_singleton_method :method_added do |nm| send name, nm, **opts unless @ignore_wrap ma.call nm if ma end end end end end
Прогоним тестовый пример:
$: << '.' require 'deco' class Alpha decorator :echo do |unbound, prefix: 'echo: ', **opts| proc do |*args, &blk| puts prefix + 'name = ' + unbound.name.inspect if opts[:name] puts prefix + 'args = ' + args.inspect if opts[:args] result = unbound.bind(self).call *args puts prefix + 'result = ' + result.inspect if opts[:result] result end end def alpha :alpha end echo :alpha, prefix: '', result: true end class Beta < Alpha def beta x "BETA: " + x.to_s end echo :beta, args: true, result: true echo args: true, name: true, result: true, prefix: '[*] ' def gamma a, b, c a * b * c end end b = Beta.new b.alpha b.beta 1 b.gamma 1, 2, 3
И получим:
$ ruby demo.rb result = :alpha echo: args = [1] echo: result = "BETA: 1" [*] name = :gamma [*] args = [1, 2, 3] [*] result = 6
Идем дальше?
Как видим, все работает. Кроме того, определения декораторов прекрасно наследуются. Для дальнейшего развития можно поработать над тем, чтобы они еще и «включались» при добавлении mixin-модуля, предусмотреть отмену и/или переключение и так далее. Но... так уж ли все это нужно? Тем более, что есть не столь универсальный, зато очень простой и достаточный в большинстве случаев способ сделать обертку:
class Fixnum alias :mul :* def * x result = mul x $stderr.puts "#{self} * #{x} = #{result}" result end end
Универсальность всегда увеличивает сложность и накладные расходы. Так что лично мое мнение: жили мы без декораторов в Ruby, и еще поживем. Тем не менее сама методика декорирования кода, безусловно, заслуживает внимания и может с успехом применяться в самых разных задачах.
1)«Понимаем декораторы в Python'e, шаг за шагом», http://habrahabr.ru/post/141411/ и 141501/, перевод с английского — Владислав Степанов; оригинал: “What are Python decorators?”, Renaud Gaudin, http://yeleman.com/what-are-python-decorators/.
2)Dave Thomas, with Chad Fowler and Andy Hunt, «Programming Ruby: The Pragmatic Programmers' Guide», бесплатная версия первого издания — http://www.ruby-doc.org/docs/ProgrammingRuby/ [en].
3)Викиучебник по Ruby, http://ru.wikibooks.org/wiki/Ruby.
4)Полные тексты всех примеров можно взять на GitHub Gist: https://gist.github.com/shikhalev/6259566.
Комментариев нет:
Отправка комментария