Aller au menu - Aller au contenu

[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)
Avatar
Auteur : Pingouin chauffé
Note : 18 / 20 (4 votes)
Visualisations : 7 381

Plus d'informations Plus d'informations
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 :

Sommaire du tutoriel :
Icône du chapitre

La bibliothèque standard


La bibliothèque standard de Ruby, complète et polyvalente, est relativement intuitive. Il faut savoir qu'elle se compose de deux API :
  1. La Core API, traduisons API principale.
  2. 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 :

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 :

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...).



Les types en Ruby

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 : Ruby
1
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 : Ruby
1
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

Image utilisateur


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 : Ruby
1
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

Image utilisateur


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 : Ruby
1
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.

Les types étant des objets, ils ont aussi leurs propres mécanismes d'instanciation. Cherchez dans la doc du côté de Class#new pour les détails ;) .

Mixins

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 : Ruby
1
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 :

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 :
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.

Notez que je n'ai cité que les principaux, mais il en existe d'autres ;) . Par ailleurs des démonstrations des méthodes susnommées sortiraient du cadre de ce tutoriel, je vous invite donc à vous documenter là-dessus !


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 : Ruby
1
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

Image utilisateur


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
1
2
self.to_s
# main:Object

Encore un peu d'introspection

Nous avons déjà étudié les méthodes permettant d'obtenir :

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 : Ruby
1
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 : Ruby
1
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)

Notez que pour des raisons évidentes de copyright, je ne peux vous révéler les sources de l'iPhone. Désolé ;) .


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 ;) ).

Q.C.M.

Que désigne le terme introspection ?
Tout objet hérite, en Ruby, de :
On utilise la méthode require pour :
La méthode Object#new est :
Sans tricher, que devrait renvoyer : 1.class.superclass.class
Toujours sans tricher, que va renvoyer : Class.superclass.superclass.superclass.class
Si je fais ceci :

Code : Ruby
1
2
3
def miauler()
  print("miaouu !")
end

Au final, qu'est-ce que miauler ?

Statistiques de réponses au QCM


Ce tutoriel touche à sa fin.

Il est probable que ce cours soit amélioré, surtout si les codes deviennent obsolètes avec Ruby 1.9 ^^ N'hésitez pas à m'envoyer des MPs si vous avez des suggestions !


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 !
Retour en haut Retour en haut


Créé : le 25/11/2007 à 18:37:45
Modifié : le 22/08/2008 à 16:07:22
Avancement : 95%
Licence : Copie non autorisée

Changer de design | En savoir plus | Plan du site | Politique d'accessibilité | Règles | RSS tutoriels | RSS news
Édité par Simple IT SARL : Nous contacter | Notre blog | Revue de presse | Publicité

Y'a plus rien à lire, faut remonter maintenant !

Hébergement web - Correction de tutoriels - Créer un site
Vous souhaitez apparaître ici ? Contactez-nous.

Nombre de connectés 333 Zéros connectés | Requêtes SQL 9 requêtes | Temps de génération de la page : Total (SQL) 0.0831s (0.0696s)