[Plan du site]
Vous êtes ici ---
> Le Site du Zéro
> Les tutoriels
> Non-Officiels
> Programmation
> Ruby
> Comprendre Ruby avec l'introspection
> Lecture du tutoriel
Comprendre Ruby avec l'introspection
Vous vous apprêtez à lire un tutoriel rédigé par un membre de ce site. Malgré tout le soin que ce membre a pu apporter au tutoriel, nous ne pouvons pas garantir que les informations contenues sur cette page sont exactes à 100%. Merci de garder cela en tête lorsque vous lirez cette page ;o)
Salut !
Dans ce tutoriel, je vais tenter de vous présenter la façon dont Ruby fonctionne globalement (sans aborder les détails bas niveau).
En premier lieu, nous présenterons la bibliothèque standard de Ruby. Ensuite nous étudierons la façon dont Ruby gère les types communs, avant de s'occuper des mixins, mécanisme indispensable pour un programmeur Ruby.
Tout au long du cours, je vais employer des méthodes dites d'
introspection pour illustrer mes propos. Nous les découvrirons au fur et à mesure.
Mais avant d'aller plus loin, assurez-vous de connaître :
- les bases de Ruby (types, modules...)
- les notions primordiales de la programmation orientée objet (instanciation, héritage...)
La bibliothèque standard de Ruby, complète et polyvalente, est relativement intuitive. Il faut savoir qu'elle se compose de deux
API :
- La Core API, traduisons API principale.
- La Standard API (donc API standard, en toute simplicité
).
L'API principale
Elle fournit un ensemble de bibliothèques, incluses automatiquement dans vos programmes (on parle alors de "built-in"). La plupart définissent les mécanismes de base de Ruby :
- Les entrées-sorties
- Les principaux types (que nous étudierons dans la partie suivante)
- Les principaux mixins (on en reparlera aussi)
- Les exceptions
- Les interfaces web
- etc...
En un mot comme en cent, c'est vraiment l'API indispensable. La documentation de référence est excellente :
http://ruby-doc.org/core/, mettez-là en marque-page si ce n'est pas déjà fait

.
L'API standard
De la même façon, elle met à votre disposition une centaine de "packages" (de petites bibliothèques, voire même un unique fichier ruby), que vous devrez inclure manuellement avec la méthode
require si vous souhaitez vous en servir. Vous les trouverez par ailleurs dans le répertoire
lib/ruby/[version]/ de Ruby. Ces paquets implémentent :
- des classes dont a besoin l'API principale (par exemple, cette dernière implémente un module DRb, qui a besoin de la classe DRb de l'API standard pour fonctionner)
- des fonctions utilitaires pour vous rendre la vie plus agréable (ex. : la méthode pretty_print)
- des classes destinées à vous simplifier la vie (ex. : le fichier delegate.rb)
On y a régulièrement recours, il faut donc la connaître un minimum. Il se peut que vous n'ayez pas tous les paquets de la version actuelle, n'hésitez donc pas à les télécharger. Cela peut se faire depuis la documentation de référence, également très bonne :
http://www.ruby-doc.org/stdlib/, un nouveau marque-page

!
Remarque : dans cette dernière documentation, les paquets dont le nom est en gras sont considérés comme bien documentés, ceux dont le nom est en italique comme mal documentés.
require ou load ?
Une question qui revient souvent en Ruby : faut-il utiliser
require ou
load ? Ces deux méthodes sont très semblables, puisqu'elles exécutent toutes deux un programme Ruby donné, qui, dans le cas où il n'est pas donné sous la forme d'un chemin absolu, est cherché dans les répertoires listés dans la globale
$: .
load(nom_fichier, anonymat=false)
Cette méthode prend deux arguments. Le premier est le nom du fichier Ruby à exécuter (mettez bien l'extension). Le second argument est un booléen valant
false par défaut. Si vous le rendez
true, alors le fichier sera executé sous un module anonyme, et votre environnement d'exécution ne sera en aucun cas affecté par les actions du programme lancé.
On utilise le plus souvent
load pour lancer des fichiers tiers dont on est sûr de la nature, et du contenu.
require(nom_fichier)
Cette méthode ne prend en argument qu'un nom de fichier, sans extension. Ce fichier peut être aussi bien un fichier Ruby qu'une bibliothèque.
On l'utilise pour charger des fichiers dont la nature reste floue. Par exemple, on ne sait pas si
Bigdecimal est contenu dans une bibliothèque ou un fichier Ruby. On va donc se servir de
require, afin de ne pas prendre de risque (toute erreur lancerait une exception
LoadError).
Lorsqu'une de ces méthodes exécute un fichier avec succès (elle renvoie
true), elle place le nom de ce fichier dans la globale
$" . Ainsi, si le fichier a déjà été chargé, il ne le sera plus (la méthode renverra
false).
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13
14 | puts $"
# nil
require 'pp'
# true
puts $"
# prettyprint.rb
load 'pp.rb'
# false, le fichier a déjà été chargé !
load 'cr3v3tt3.rb'
# LoadError
|
Notations
Par convention, on se réfère à une méthode avec la notation suivante :
objet#methode. En effet, les implémentations d'une méthode peuvent différer selon le type utilisé, mais de cette manière nous savons exactement de laquelle nous parlons.
Aussi, on se réfèrera à un objet de cette façon :
objet:Type. Vous n'en aurez pas vraiment besoin dans ce tutoriel, mais c'est la syntaxe que Ruby utilise dans les messages d'erreurs, alors retenez-le !
Par ailleurs, j'utiliserai la police
courrier pour me référer à un terme Ruby (nom d'une classe, d'une méthode...).
Hiérarchie des types communs
Comment s'organisent les classes en Ruby ?
Pour le savoir, il nous suffit d'utiliser la méthode d'introspection
class.superclass , afin d'obtenir le nom de la classe dont hérite
class.
L'introspection désigne la capacité d'un programme à s'analyser lui-même.
Elle est utilisée dans les langages à métaobjets, comme Ruby.
Par exemple, pour la classe
String, qui représente les chaînes de caractères :
Code : Ruby1
2
3
4 | String.superclass
# Object
Object.superclass
# nil
|
On sait donc que
String hérite de
Object, classe située au sommet de la hiérarchie des types Ruby. Lorsque je crée une classe Ruby, elle dérive automatiquement d'Object.
Code : Ruby1
2
3
4
5 | class Machin
end
Machin.superclass
# Object
|
Cela signifie que
toutes les classes héritent (directement ou non) d'
Object.
L'organisation des types communs est donc on ne peut plus simple :
Les principaux types de Ruby

De la même façon, on peut trouver le type d'un objet grâce à la méthode d'introspection
obj.class .
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12 | 1.class
# Fixnum
2.0.class
# Float
"Ruby".class
# String
tableau = [1, 2, 3]
tableau.class
# Array
|
Notons au passage que nos deux nombres (1 et 2.0) ne sont pas du même type. En effet, en Ruby et dans la plupart des autres langages, les nombres sont représentés par plusieurs types, selon leur nature :
Code : Ruby1
2
3
4
5
6
7
8
9 | [1, 2.0, 999999999999999999999999999].each { |n| puts n.class }
# Fixnum
# Float
# Bignum
Float.superclass
# Numeric
Bignum.superclass
# Integer
|
Oulà ! Cela fait beaucoup de types ça ! Bon ne nous noyons pas dans des exemples répétitifs et allons à l'essentiel :
Hiérarchie des nombres en Ruby

Le type
Numeric représente les nombres. (il hérite bien entendu de
Object)
Le type
Float représente les nombres à virgule flottante.
Le type
Integer les entiers.
Les types
Fixnum et
Bignum représentent respectivement les entiers inférieurs et supérieurs à un mot-machine.
Tout est objet
Vous avez probablement déjà entendu que dans Ruby, tout était objet, mais que cela signifie-t-il ? Peut-on traîter les types comme des objets ? La réponse est oui. Les classes, les méthodes, les modules... tout a un type en Ruby.
On peut donc traîter les classes comme des objets avec la méthode
obj.class.
Testons cela avec notre classe
Machin, encapsulée dans le module
Truc. Nous allons y implémenter une méthode
bidulifier, dont on va récupérer une référence grâce à la méthode d'introspection
obj.method.
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | module Truc
class Machin; end
end
class Truc::Machin
def bidulifier
puts "L'instance est maintenant un bidule."
end
end
Truc::Machin.superclass
# Object
Truc.class
# Module
Module.superclass
# Object (eh oui...)
chose = Truc::Machin.new
bidule = chose.method :bidulifier
# bidule est desormais une reference de la methode bidulifier de l'objet chose
bidule.class
# Method
bidule.call
# "L'instance est maintenant un bidule."
|
Avec ce bout de code, on a découvert une chose : les méthodes sont des instances d'une classe
Method, implémentant d'ailleurs une méthode
call, laquelle est invoquée lorsque l'on appelle la méthode d'un objet. On remarquera aussi que les modules semblent être des instances d'une classe
Module.
Plus intriguant, nous allons maintenant étudier le type d'un... type !
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | class Machin
end
Machin.class
# Class
Class.superclass
# Module
Class.superclass.superclass
# Object
Class.class
# Class
Class.superclass.class
# Class
|
On déduit de ce code une règle inaliénable :
le type d'un type est toujours Class.
Cela peut d'ailleurs se vérifier très simplement : sachant que le type d'
Object est
Class, et que tout type dérive d'
Object, tout type a pour type
Class.
Si un type est considéré en Ruby comme un objet, la réciproque est totalement fausse ! Un objet n'est donc pas nécessairement un Class.
On notera aussi que
Class hérite des types
Module et Object, lesquels sont de type
Class, ce qui mène à une hiérarchie complexe...
Instanciation d'objet
Nous allons ici regarder en détail le fonctionnement d'une instanciation en Ruby. La syntaxe générale de la définition d'un type doit vous être bien connue :
Code : Ruby1
2
3
4
5
6
7
8 | class IPhone
def initialize(nom)
@proprietaire = nom
end
end
#instanciation
telephone = IPhone.new("<Zer0>")
|
Lors de l'instanciation, connaissant la syntaxe des appels de méthode Ruby, nous pouvons déduire que la méthode singleton
self.new de
IPhone est appelée (on l'appelle méthode
constructeur). Plus précisément, on appelle la méthode
new héritée de
Object.
La méthode initialize n'est pas un constructeur, mais un initialisateur ! Elle se charge de la création des champs, etc... avant de rendre définitivement l'instance.
Bien entendu, on peut en fournir notre propre implémentation, mais c'est assez risqué. En effet, c'est
new qui doit rendre l'instance, c'est donc elle qui va fixer le type de l'objet. Illustrons cela avec une classe dont on réimplémente
new :
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13 | class Machin
def self.new(*args)
Object.new(*args)
end
def initialize(*args)
puts "Machin::initialize"
end
end
chose = Machin.new
chose.class
# Object
|
L'appel d'
Object#new a pour effet d'invoquer
Object::initialize en lieu et place de
Machin::initialize. On peut confirmer cette hypothèse en redéfinissant cette dernière, mais on recevera un avertissement comme quoi on risque de causer une boucle infinie

. Redéfinir
new est donc généralement une mauvaise idée.
Le terme de
POO "Mixin" vient de l'anglais,
to mix in, mélanger dans.
Qu'est-ce qu'un mixin ?
En programmation Ruby, un mixin est un module inclus directement dans une classe. En gros :
Code : Ruby1
2
3
4
5
6
7
8
9 | module Pouvoirs
def invulnerable?
return true
end
end
class Heros
include Pouvoirs # Pouvoirs est mélangé dans Heros. C'est un mixin
end
|
Lorsque l'on fait un mixin, la classe a accès aux méthodes implémentées dans le module. En fait, on s'en sert souvent pour simuler l'héritage multiple, ce mécanisme n'existant pas dans Ruby :
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13
14 | class Personne
def parler?
return true
end
end
class Heros < Personne
include Pouvoirs
end
Heros.new.parler?
# true
Heros.new.invulnerable?
# true
|
On ne pouvait pas faire hériter
Heros d'une classe
Personne et d'une classe
Pouvoirs. On l'a donc fait hériter de
Personne, et on a fait un mixin du module
Pouvoirs. De cette façon, une instance de
Heros peut être considérée comme une personne avec des pouvoirs.
Les mixins de l'API principale
Kernel
Ruby comporte quelques mixins qu'on utilise très souvent, et qu'il est donc impératif de connaître. Le plus connu d'entre eux est le mixin
Kernel, qui est inclus dans
Object, donc qui est omniprésent dans tous vos programmes. Vous avez probablement déjà utilisé des méthodes du module Kernel sans le savoir. Il implémente entre autres, pour les plus connues :
- exec : lance une commande externe donnée en argument
- exit : termine le script Ruby en lançant l'exception SystemExit
- gets : lit la dernière ligne d'une liste de fichiers passée en argument au programme, ou bien la dernière ligne de l'entrée standard
- load : exécute un script Ruby
- loop : crée une boucle infinie qui appelle à chaque tour un bloc donné
- open : crée un objet IO relié à un fichier ou flot
- puts : écrit sur un flot de sortie, stdout par défaut
- rand : génération de nombres aléatoires
- require : lance une librairie ou un script Ruby
- system : exécute la commande passée en argument
Pour en savoir plus, n'hésitez surtout pas à aller explorer la documentation !
Enumerable
Ce mixin est lui aussi très utilisé, puisqu'il définit les 22 méthodes d'énumeration de Ruby. Chaque objet pouvant être itéré voit son type inclure ce mixin. C'est le cas des types
Array,
Hash, ou encore
Range. Le module
Enumerable implémente entre autres :
- collect
- inject
- sort
- sort_by
Néanmoins, ces méthodes ont un prix, puisque vous devrez fournir une implémentation de la méthode
each. Cela n'est toutefois pas bien compliqué

.
Comparable
Ce mixin propose une implémentation des méthodes
<,
<=,
==,
>=,
>, et
between?. Elle est utilisée par les classes ayant besoin de classer leurs objets. Toutefois, pour fonctionner, la classe en question doit proposer une implémentation de la méthode
<=>.
Precision
Ce dernier mixin est abondamment utilisé par les classes régissant les nombres, puisque c'est lui qui permet de représenter les nombre réels avec précision. En pratique, on ne l'utilisera pas souvent, mais il faut savoir qu'il existe.
On peut savoir quels mixins utilise telle classe, avec la méthode d'introspection
class.ancestors, qui nous renvoie une liste de tout l'arbre généalogique d'un type, y compris les mixins.
Code : Ruby1
2
3
4
5
6
7 | Integer.ancestors
# Integer
# Precision
# Numeric
# Comparable
# Object
# Kernel
|
Voilà donc notre graphique plus complet :
Hiérarchie des nombres en Ruby

L'environnement d'exécution
Le fait que dans Ruby tout soit objet doit vous sembler plus familier depuis la précédente partie. Mais si je vous disais que l'environnement d'exécution lui-même était un objet ? Surpris ?
Cela se vérifie pourtant simplement, grâce à l'introspection :
Code : Ruby 1
2
3
4
5
6
7
8
9
10 | foo = "Foo"
puts self.foo
# "Foo"
self.bar = "Bar"
puts bar
# "Bar"
self.class
# Object
|
Etonant, non ? Même l'environnement dérive de
Object !
Cela est toutefois assez logique. En effet, comment cela se fait-il que l'on ai accès aux méthodes comme
require ou
puts ? C'est parce qu'elles font partie du mixin
Kernel, inclus dans
Object, type de l'environnement d'exécution !
Notons enfin que le programme se réfère à l'environnement par
main:Object.
Code : Ruby
Nous avons déjà étudié les méthodes permettant d'obtenir :
- Le nom du type d'un objet
- La classe mère d'un type
- Une référence à une méthode
Il en existe de nombreuses autres. Je vais ici vous lister celles qui vous seront le plus utile.
obj.methods
Vous fournit la liste des méthodes publiques d'un objet.
Cette liste comprend les méthodes héritées.
De la même façon,
obj.private_methods vous renverra les méthodes d'instance privées.
obj.singleton_methods
Vous fournit la liste des méthodes
singleton d'un objet.
Code : Ruby1
2
3
4
5
6
7 | Dir.singleton_methods
# glob
# []
# pwd
# unlink
# rmdir
# ...
|
mod.instance_methods
Vous fournit la liste des méthodes d'instance d'une classe ou d'un module.
class.method_defined?(:methode)
Renvoie
true si
class a une méthode
methode.
Code : Ruby1
2
3
4
5
6 | Class.method_definied? :method_defined?
# true
# on peut aussi utiliser respond_to sur l'instance d'une classe
0.respond_to? :upto
# true
|
meth.arity
Retourne le nombre d'arguments que prend
meth. Si elle en prend un nombre variable,
arity retourne
-n-1, où
n est le nombre d'arguments requis.
Code : Ruby 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 | class IPhone
def allumer
puts "Bienvenue sur l'iPhone !"
end
def appeller(numero)
# ...
end
def preferences(resolution, luminosite, *autres)
# ...
end
end
Iphone.method(:allumer).arity
# 0
Iphone.method(:appeller).arity
# 1
Iphone.method(:preferences).arity
# -3 (-2 - 1)
|
method_added, method_removed
Implémentées dans une classe, ces méthodes vous permettent de réagir respectivement à l'ajout d'une méthode et à la supression d'une méthode. Elles sont interfacées dans
Module. N'oubliez pas de préfixer leur nom de
self dans la définition !
obj.method_missing
Définie dans
Kernel, cette méthode d'introspection vous permet de réagir aux appels de méthodes inexistantes (Ruby le fait automatiquement, mais pas d'une façon très élégante

).
Ce tutoriel touche à sa fin.
Le sujet est assez vaste et peut certainement être approfondi, mais si je ne vous ai rien appris sur Ruby j'espère au moins vous avoir introduit aux techniques d'introspection

. Celles-ci sont très nombreuses, fouillez la doc pour les connaître !
Avec les connaissances que vous devriez avoir acquis désormais, vous pouvez passer au niveau supérieur en regardant du côté de la métaprogrammation et des métaclasses

.
Bonne chance !