Décorateurs en Ruby (migration de Python)

Je passe aujourd'hui à apprendre Ruby du point de vue de Python. Une chose que j'ai complètement échoué à affronter est un équivalent de décorateurs. Pour réduire les choses, j'essaie de reproduire un décorateur Python trivial:

#! /usr/bin/env python

import math

def document(f):
    def wrap(x):
        print "I am going to square", x
        f(x)
    return wrap

@document
def square(x):
    print math.pow(x, 2)

square(5)

Exécuter cela me donne:

I am going to square 5
25.0

Donc, je veux créer une fonction square (x), mais la décorer pour qu'elle m'avertisse de ce qu'elle va faire square avant de le faire. Débarrassons-nous du sucre pour le rendre plus basique:

...
def square(x):
    print math.pow(x, 2)
square = document(square)
...

Alors, comment puis-je reproduire cela dans Ruby? Voici ma première tentative:

#! /usr/bin/env ruby

def document(f)
    def wrap(x)
        puts "I am going to square", x
        f(x)
        end
    return wrap
    end

def square(x)
    puts x**2
    end

square = document(square)

square(5)

L'exécution de cette opération génère:

./ruby_decorate.rb:8:in `document': wrong number of arguments (0 for 1) (ArgumentError)
    from ./ruby_decorate.rb:15:in `'

Ce que je suppose parce que les parenthèses ne sont pas obligatoires et qu'il prend mon "return wrap" comme une tentative de " return wrap ()". Je ne connais aucun moyen de se référer à une fonction sans l'appeler.

J'ai essayé diverses autres choses, mais rien ne m'amène loin.

27
demandé sur jameshfisher 2009-12-12 01:58:44

11 réponses

Voici une autre approche qui élimine le problème des conflits entre les noms des méthodes aliasées (notez que mon autre solution utilisant des modules pour la décoration est une bonne alternative car elle évite également les conflits):

module Documenter
    def document(func_name)   
        old_method = instance_method(func_name) 

        define_method(func_name) do |*args|   
            puts "about to call #{func_name}(#{args.join(', ')})"  
            old_method.bind(self).call(*args)  
        end
    end
end

Le code ci-dessus fonctionne parce que la variable locale old_method est maintenue en vie dans la nouvelle méthode 'hello' par le fait que le bloc define_method est une fermeture.

13
répondu horseyguy 2009-12-12 17:37:20

Ok, il est temps de tenter une réponse. Je vise ici spécifiquement les Pythoniers essayant de réorganiser leur cerveau. Voici un code fortement documenté qui (approximativement) fait ce que j'essayais de faire à l'origine:

Méthodes d'instance de décoration

#! /usr/bin/env ruby

# First, understand that decoration is not 'built in'.  You have to make
# your class aware of the concept of decoration.  Let's make a module for this.
module Documenter
  def document(func_name)   # This is the function that will DO the decoration: given a function, it'll extend it to have 'documentation' functionality.
    new_name_for_old_function = "#{func_name}_old".to_sym   # We extend the old function by 'replacing' it - but to do that, we need to preserve the old one so we can still call it from the snazzy new function.
    alias_method(new_name_for_old_function, func_name)  # This function, alias_method(), does what it says on the tin - allows us to call either function name to do the same thing.  So now we have TWO references to the OLD crappy function.  Note that alias_method is NOT a built-in function, but is a method of Class - that's one reason we're doing this from a module.
    define_method(func_name) do |*args|   # Here we're writing a new method with the name func_name.  Yes, that means we're REPLACING the old method.
      puts "about to call #{func_name}(#{args.join(', ')})"  # ... do whatever extended functionality you want here ...
      send(new_name_for_old_function, *args)  # This is the same as `self.send`.  `self` here is an instance of your extended class.  As we had TWO references to the original method, we still have one left over, so we can call it here.
      end
    end
  end

class Squarer   # Drop any idea of doing things outside of classes.  Your method to decorate has to be in a class/instance rather than floating globally, because the afore-used functions alias_method and define_method are not global.
  extend Documenter   # We have to give our class the ability to document its functions.  Note we EXTEND, not INCLUDE - this gives Squarer, which is an INSTANCE of Class, the class method document() - we would use `include` if we wanted to give INSTANCES of Squarer the method `document`.  <http://blog.jayfields.com/2006/05/ruby-extend-and-include.html>
  def square(x) # Define our crappy undocumented function.
    puts x**2
    end
  document(:square)  # this is the same as `self.document`.  `self` here is the CLASS.  Because we EXTENDED it, we have access to `document` from the class rather than an instance.  `square()` is now jazzed up for every instance of Squarer.

  def cube(x) # Yes, the Squarer class has got a bit to big for its boots
    puts x**3
    end
  document(:cube)
  end

# Now you can play with squarers all day long, blissfully unaware of its ability to `document` itself.
squarer = Squarer.new
squarer.square(5)
squarer.cube(5)

Toujours confus? Je ne serais pas surpris; cela m'a pris presque une journée entière. Quelques autres choses que vous devriez savoir:

  • la première chose, qui est cruciale, est de lire ceci: http://www.softiesonrails.com/2007/8/15/ruby-101-methods-and-messages . Lorsque vous appelez ' foo ' dans Ruby, ce que vous faites en fait est d'envoyer un message à son propriétaire :" veuillez appeler votre méthode 'foo'". Vous ne pouvez tout simplement pas avoir une emprise directe sur les fonctions dans Ruby comme vous le pouvez en Python; ils sont glissants et insaisissables. Vous ne pouvez les voir que comme si les ombres sur un mur de grotte; vous ne pouvez les référencer à travers des chaînes / symboles qui se trouvent être leur nom. Essayez de penser à chaque appel de méthode 'objet.foo (args) 'vous faites dans Ruby comme l'équivalent de cela en Python:' objet.getattribute('foo')(args)'.
  • arrêtez d'écrire des définitions de fonction / méthode en dehors des modules / classes.
  • acceptez dès le départ que cette expérience d'apprentissage va fondre le cerveau, et prenez votre temps. Si Ruby n'a pas de sens, frappez un mur, allez faire une tasse de café ou dormez une nuit.

Méthodes de classe de décoration

Le code ci-dessus décore les méthodes d'instance. Que faire si vous voulez décorer les méthodes directement sur la classe? Si vous lisez http://www.rubyfleebie.com/understanding-class-methods-in-ruby , vous trouvez qu'il existe trois méthodes pour créer des méthodes de classe-mais une seule d'entre elles fonctionne pour nous ici.

C'est la technique anonyme class << self. Faisons ce qui précède mais nous pouvons appeler square() et cube () sans l'instancier:

class Squarer

  class << self # class methods go in here
    extend Documenter

    def square(x)
      puts x**2
      end
    document(:square)

    def cube(x)
      puts x**3
      end
    document(:cube)
    end
  end

Squarer.square(5)
Squarer.cube(5)

Amusez-vous!

11
répondu jameshfisher 2009-12-12 15:03:30

Les décorateurs de type Python peuvent être implémentés dans Ruby. Je n'essaierai pas d'expliquer et de donner des exemples, car Yehuda Katz a déjà publié un bon article de blog sur les décorateurs DSL dans Ruby, donc je recommande fortement de le lire:

Mise à jour: j'ai quelques bas de vote sur celui-ci, alors laissez-moi vous expliquer plus loin.

alias_method (and alias_method_chain) n'est pas exactement le même concept qu'un décorateur. C'est juste une manière de redéfinir l'implémentation de la méthode sans utiliser l'héritage (donc le code client ne remarquera pas de différence, toujours en utilisant le même appel de méthode). Il pourrait être utile. Mais aussi il pourrait être sujet aux erreurs. Quiconque a utilisé la bibliothèque gettext pour Ruby a probablement remarqué que son intégration ActiveRecord a été rompue avec chaque mise à niveau majeure de Rails, car la version aliasée a suivi la sémantique d'une ancienne méthode.

Le but d'un décorateur en général N'est pas de changer les internes de tout méthode donnée et être toujours capable d'appeler l'original à partir d'une version modifiée, mais pour améliorer le comportement de la fonction. Le cas d'utilisation" entrée/sortie", qui est un peu proche de alias_method_chain, n'est qu'une simple démonstration. Un autre type de décorateur plus utile pourrait être @login_required, qui vérifie l'autorisation, et n'exécute la fonction que si l'autorisation a réussi, ou @trace(arg1, arg2, arg3), qui pourrait effectuer un ensemble de procédures de traçage (et être appelé avec différents arguments pour différentes méthodes de décoration).

7
répondu Oleg Shaldybin 2009-12-12 05:31:50

C'est une question un peu inhabituelle, mais intéressante. Je vous recommande d'abord fortement de ne pas essayer de transférer directement vos connaissances Python à Ruby - il est préférable d'apprendre les idiomes de Ruby et de les appliquer directement, plutôt que d'essayer de transférer directement Python. J'ai beaucoup utilisé les deux langues, et elles sont toutes les deux meilleures en suivant leurs propres règles et conventions.

Après avoir dit tout cela, Voici un code astucieux que vous pouvez utiliser.

def with_document func_name, *args
  puts "about to call #{func_name}(#{args.to_s[1...-1]})"
  method(func_name).call *args
end

def square x
  puts x**2
end

def multiply a, b
  puts a*b
end

with_document :square, 5
with_document :multiply, 5, 3

Ce produit

about to call square(5)
25
about to call multiply(5, 3)
15

Je suis sûr que vous serez d'accord fait le travail.

4
répondu Peter 2009-12-11 23:07:19

IMO mooware a la meilleure réponse jusqu'à présent et c'est la plus propre, la plus simple et la plus idiomatique. Cependant, il utilise 'alias_method_chain' qui fait partie de Rails, et non de Ruby pur. Voici une réécriture en utilisant Ruby pur:

class Foo         
    def square(x)
        puts x**2
    end

    alias_method :orig_square, :square

    def square(x)
        puts "I am going to square #{x}"
        orig_square(x)
    end         
end

Vous pouvez également accomplir la même chose en utilisant des modules à la place:

module Decorator
    def square(x)
        puts "I am going to square #{x}"
        super
    end
end

class Foo
    def square(x)
        puts x**2
    end
end

# let's create an instance
foo = Foo.new

# let's decorate the 'square' method on the instance
foo.extend Decorator

# let's invoke the new decorated method
foo.square(5) #=> "I am going to square 5"
              #=> 25
3
répondu horseyguy 2009-12-12 14:53:43

Michael Fairley l'a démontré à RailsConf 2012. Le Code est disponible ici sur Github. Exemples d'utilisation simples:

class Math
  extend MethodDecorators

  +Memoized
  def fib(n)
    if n <= 1
      1
    else
      fib(n - 1) * fib(n - 2)
    end
  end
end

# or using an instance of a Decorator to pass options
class ExternalService
  extend MethodDecorators

  +Retry.new(3)
  def request
    ...
  end
end
2
répondu coreyward 2012-04-26 18:36:30

Ce que vous pourriez réaliser avec des décorateurs en Python, vous réalisez avec des blocs en Ruby. (Je ne peux pas croire combien de réponses sont sur cette page, sans une seule déclaration de rendement!)

def wrap(x)
  puts "I am going to square #{x}"
  yield x
end

def square(x)
  x**2
end

>> wrap(2) { |x| square(x) }
=> I am going to square 2
=> 4

Le concept est similaire. Avec le décorateur en Python, vous passez essentiellement la fonction " square "à appeler à partir de"wrap". Avec le bloc dans Ruby, Je ne passe pas la fonction elle-même, mais un bloc de code à l'intérieur duquel la fonction est invoquée, et ce bloc de code est exécuté dans le contexte de "wrap", où l'énoncé de rendement est.

Contrairement aux décorateurs, le bloc Ruby passé n'a pas besoin d'une fonction pour en faire partie. Ce qui précède aurait pu être simplement:

def wrap(x)
  puts "I am going to square #{x}"
  yield x
end

>> wrap(4) { |x| x**2 }
=> I am going to square 4
=> 16
2
répondu Legion 2013-06-17 20:06:04

Votre supposition est juste.

Il est préférable d'utiliser alias pour lier la méthode d'origine à un autre nom, puis définir le nouveau pour imprimer quelque chose et appeler l'ancien. Si vous devez le faire à plusieurs reprises, vous pouvez créer une méthode qui le fait pour n'importe quelle méthode (j'ai eu un exemple une fois, mais je ne peux pas le trouver maintenant).

PS: votre code ne définit pas une fonction dans une fonction mais une autre fonction sur le même objet (Oui, c'est une fonctionnalité non documentée de Ruby)

class A
  def m
    def n
    end
  end
end

Définit à la fois m et n a

NB: la manière de se référer à une fonction serait

A.method(:m)
1
répondu akuhn 2009-12-11 23:35:28

Ok, j'ai encore trouvé mon code qui fait des décorateurs dans Ruby. Il utilise alias pour lier la méthode d'origine à un autre nom, puis définir le nouveau pour imprimer quelque chose et appeler l'ancien. Tout cela est fait en utilisant eval, de sorte qu'il peut être réutilisé comme des décorateurs en Python.

module Document
  def document(symbol)
    self.send :class_eval, """
      alias :#{symbol}_old :#{symbol}
      def #{symbol} *args
        puts 'going to #{symbol} '+args.join(', ')
        #{symbol}_old *args
      end"""  
  end
end

class A 
  extend Document
  def square(n)
    puts n * n
  end
  def multiply(a,b)
    puts a * b
  end
  document :square
  document :multiply
end

a = A.new
a.square 5
a.multiply 3,4

Edit: ici la même chose avec un bloc (pas de douleur de manipulation de chaîne)

module Document
  def document(symbol)
    self.class_eval do
       symbol_old = "#{symbol}_old".to_sym
       alias_method symbol_old, symbol
       define_method symbol do |*args|
         puts "going to #{symbol} "+args.join(', ')
         self.send symbol_old, *args
       end
    end  
  end
end
1
répondu akuhn 2009-12-11 23:48:07

Je crois que l'idiome Ruby correspondant serait la chaîne de méthode alias, qui est fortement utilisée par Rails. Cet article le considère également comme le décorateur de style Rubis.

Pour votre exemple, il devrait ressembler à ceci:

class Foo
  def square(x)
    puts x**2
  end

  def square_with_wrap(x)
    puts "I am going to square", x
    square_without_wrap(x)
  end

  alias_method_chain :square, :wrap
end

Le alias_method_chain appeler renomme square à square_without_wrap et rend square un alias pour square_with_wrap.

Je crois que Ruby 1.8 n'a pas cette méthode intégrée, donc vous devriez la copier à partir de Rails, mais 1.9 devrait l'inclure.

Mes Ruby-compétences ont obtenu un peu rouillé, donc je suis désolé si le code ne fonctionne pas réellement, mais je suis sûr qu'il démontre le concept.

0
répondu mooware 2009-12-11 23:43:25

Dans Ruby, vous pouvez imiter la syntaxe de Python pour les décorateurs comme ceci:

def document        
    decorate_next_def {|name, to_decorate|
        print "I am going to square", x
        to_decorate
    }
end

document
def square(x)
    print math.pow(x, 2)
end

Bien que vous ayez besoin de lib pour cela. J'ai écrit ici Comment implémenter une telle fonctionnalité (quand j'essayais de trouver quelque chose dans Rython qui manque dans Ruby).

0
répondu Alexey 2010-12-25 22:39:30