Définir et manipuler des classes
=================================

.. glossary::

    objet

        Un object est une entité possédant un type, un état, et un comportement.
        Un object correspondant généralement à une entité du monde réel, mais
        cette entité peut être abstraite.
        On parle aussi d'**instance**.

**état d'un objet** : ensemble de propriétés auxquelles sont associées des
valeurs.

Les variables de l'objet sont appelées des **attributs**.
le comportement d'un :term:`objet` :

- des actions effectuées sur l'objet
- des appels faits sur l'objet

envois de messages à l'objet = appel de **méthodes**

programmation objet (première approche)
-----------------------------------------


- le type et le protocole d'un objet sont définis par sa classe
- une classe possède un ensemble d'attributs et de méthodes

deux relations possibles
~~~~~~~~~~~~~~~~~~~~~~~~~


.. glossary::

    heritage

        relation "est une espèce de " utilisée entre une classe et une autre classe

    instantiation

        relation "est une instance de " entre un objet et une classe


- est une instance de (objets par rapport à classe)
- est une espèce de (classe par rapport à classe, :term:`heritage`)

**instance**

- définition d'une classe
- instance de classe : on peut créer des objets à partir d'un type "classe"
  (une classe est instanciable)

>>> class A:
...   pass
...
>>> a = A()

:term:`heritage` : notation en python

>>> class A: pass
...
>>> class B(A): pass
...
>>> b = B()
>>> type(b) == B
>>> isinstance(b, A) == True

possibilité en python d'héritage multiple::

    class A(B, C): pass


attribut d'objets et de classes
----------------------------------

>>> o = object()
>>> o
<object object at 0x7f77c9cda0d0>
>>> class C(object): pass
... 
>>> class C: pass
... 
>>> c = C()
>>> c.a = 3
>>> c.a
3
>>> vars(c)
{'a': 3}
>>> c.__dict__
{'a': 3}
>>> C.__dict__
{'__module__': '__main__', '__doc__': None}
>>> C.c = 5
>>> C.__dict__
{'c': 5, '__module__': '__main__', '__doc__': None}
>>> c.c
5
>>> c.z = 3
>>> c.z
3
>>> c.__dict__
{'a': 3, 'z': 3}
>>> C.__dict__
{'c': 5, '__module__': '__main__', '__doc__': None}
>>> class MaKlass:
...   def unefonction(self, x):
...     print x 
... 
>>> MaKlass.__dict__
{'__module__': '__main__', '__doc__': None, 'unefonction': <function unefonction at 0x7f77c5b0c488>}
>>> k = MaKlass()
>>> k.__dict__
{}
>>> def autrefonc(self, x)
  File "<stdin>", line 1
    def autrefonc(self, x)
                         ^
SyntaxError: invalid syntax
>>> def autrefonc(self, x):
...   print x
... 
>>> k.autrefonc = autrefonc
>>> k.__dict__
{'autrefonc': <function autrefonc at 0x7f77c5b0c500>}
>>> MaKlass.__dict__
{'__module__': '__main__', '__doc__': None, 'unefonction': <function unefonction at 0x7f77c5b0c488>}
>>> MaKlass.unefonction(k, "toto")
toto
>>> k.unefonction("toto")
toto
>>> k.__dict__
{'autrefonc': <function autrefonc at 0x7f77c5b0c500>}
>>> MaKlass.toto = "test"
>>> k.__dict__
{'autrefonc': <function autrefonc at 0x7f77c5b0c500>}
>>> k.toto
'test'
>>> 


le __dict__ avec l'héritage de classe 
-------------------------------------------


>>> class A(object): pass
... 
>>> A.__dict__
dict_proxy({'__dict__': <attribute '__dict__' of 'A' objects>, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
>>> class B(A): 
...   b = 3
... 
>>> class C(B):
...   c = 2
... 
>>> c = C()
>>> o = C()
>>> o.__dict__
{}
>>> o.c
2
>>> o.b
3
>>> o.__class__
<class '__main__.C'>
>>> o.__class__.__dict__
dict_proxy({'__module__': '__main__', 'c': 2, '__doc__': None})
>>> 


method resolution object 
-----------------------------

>>> class A(object): pass
... 
>>> A.__mro__
(<class '__main__.A'>, <type 'object'>)
>>> class B(A): pass
... 
>>> B.__mro__
(<class '__main__.B'>, <class '__main__.A'>, <type 'object'>)
>>> class C(A, B): pass
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Error when calling the metaclass bases
    Cannot create a consistent method resolution
order (MRO) for bases A, B
>>> 


introspection contre encapsulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

voir un objet comme un espace de nommage. C'est plus **agile**.

attributs et méthodes vus comme des ajouts dans l'espace de nommage

>>> a.a = 2
>>> def function(x):
...   print x
...
>>> a.f = function
>>> a.f("hello")
hello
>>>


la nécessité d'un design objet
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

affichage d'une calculette, il faut créer un type `Touche`
qui contient deux désignations : `Chiffre` et `operation`::

  type operation = Plus | Moins | Divise
  type touche = Chiffre of int | Memoire | Op of operation

soit, on définit un type touche, soit on ne définit pas ce type::

  type operation = Plus | Moins | Divise
  type memoire = Memoire
  type chiffre = Chiffre

- les structures de données (int, str, list, dict...) : types de base
- les structures de données utilisateur : les classes !

.. function:: type (objname)

    :param objname: l'objet dont on veut connaître le type

Manipulations sur les classes et les objets
---------------------------------------------

En python un type et une classe c'est la même chose. Une classe
est un type standard. En python tout est objet, et tout provient d'un seul objet
(qui s'appelle ``object``).

- encapsulation (cacher les attributs (variables d'état)) d'un objet.

- interfaces : chaque aspect d'une classe peut être vu comme une interface.

Une interface décrit un ensemble de comportements.
on peut considérer une interface comme un protocole d'utilisation d'un objet
dans un contexte donné.

on peut alors créer des outils qui sauront traiter n'importe quel objet
pourvu qu'il respecte une ensemble d'interfaces.

.. todo:: travailler l'héritage, l'aggrégation, la délégation

Voici un exemple de classe `Voiture` :

.. literalinclude:: snippets/heritage.py
    :pyobject: Voiture

j'instancie ma classe `Voiture` :

>>> ma_voiture = Voiture("ma_marque", "150km/h")
>>> ma_voiture.roule()
'vroummm'
>>> ma_voiture.signaletique()
'constructeur : ma_marque, vitesse_max 150km/h'

.. todo:: faire des traitements dans l'init

- héritage (est une sorte de)
- polymorphisme : un objet apparait comme une instance d'une classe parente

.. literalinclude:: snippets/heritage.py
    :pyobject: Turbo

>>> v = DoDoche("marque", 160)
>>> v.get_prix()
'7000'
>>> isinstance(v, Prix)
True
>>>

mais l'objet conserve son identité :

>>> type(v)
<type 'Voiture'>

la fonction ``achete_voiture()`` sera appelée indépendamment du type de l'objet,
du moment que l'objet a une méthode `get_prix()`, c'est le duck typing, qu'il
est préférable de ramener au polymorphisme d'objet, ou bien utiliser les :mod:`abc`
(abstract base classes).

.. literalinclude:: snippets/heritage.py
    :pyobject: achete_voiture

tout le code :

.. literalinclude:: snippets/heritage.py

:download:`télécharger le code <snippets/heritage.py>`

- **l'aggrégation**

un attribut est lui-même un objet (ce qui est fréquent en python)...

.. literalinclude:: snippets/aggregation.py

- **la délégation**

la fonction "property" est un élément du design de python lui-même

.. function:: property()

les patrons de conception
---------------------------

- le patron **factory**

.. literalinclude:: snippets/patrons.py

:download:`télécharger usine (factory) <snippets/patrons.py>`

- le patron **wrapper**

:download:`télécharger wrapper <snippets/wrap.py>`

.. literalinclude:: snippets/wrap.py

exemple d'utilisation de `Wrap()`

>>> class O:
...   pass
...
>>> o = O()
>>> o.a = "blah"
>>>
>>> from wrap import Wrap
>>> w = Wrap("monwrap", o)
>>> w._name
'monwrap'
>>> w._w
<__main__.O instance at 0x7fbf177aaa28>
>>> w._w.a
'blah'
>>> w.a
'blah'
>>> w.u
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "wrap.py", line 11, in __getattr__
    return getattr(self._w, name)
AttributeError: O instance has no attribute 'u'
>>>

- le patron de conception **itérable**

:download:`télécharger iterable <snippets/iterable.py>`

.. literalinclude:: snippets/iterable.py

- le patron **decorateur**

:download:`télécharger decorateur <snippets/decorateur.py>`

.. literalinclude:: snippets/decorateur.py

>>> def deco(func):
...    func.attr = 'decorated'
...    return func
...
>>> @deco
... def f(): pass
...
>>> f.attr
'decorated'
>>>

autre exemple : les méthodes statiques

>>> class A(object):
...   @staticmethod
...   def my_class_method(cls):
...      # do stuff here
         
         
métaclasses
-----------------

>>> class A:
...   pass
... 
>>> type(A)
<type 'classobj'>
>>> class B(object): pass
... 
>>> type(B)
<type 'type'>
>>> help(type)

>>> C = type('C', (), {})
>>> C
<class '__main__.C'>
>>> 

>>> type(object)
<type 'type'>
>>> type(type)
<type 'type'>
>>> isinstance(type, object)
True
>>> isinstance(object, type)
True
>>> 

::

  class MaMetaClasse(type):
    """Exemple d'une métaclasse."""
    def __new__(metacls, nom, bases, dict):
        """Création de notre classe."""
        print("On crée la classe {}".format(nom))
        return type.__new__(metacls, nom, bases, dict)

  class MaClasse(object):
    __metaclass__ = MaMetaClasse


exemple : forcer l'implémentation d'un singleton avec les métaclasses 

::

  class Singleton(type):
    instance = None
    def __call__(cls, *args, **kw):
        if not cls.instance:
             cls.instance = super(Singleton, cls).__call__(*args, **kw)
        return cls.instance

  class ASingleton(object):
    __metaclass__ = Singleton

    a = ASingleton()
    b = ASingleton()
    assert a is b
    print(a.__class__.__name__, b.__class__.__name__)
    
    class BSingleton(object):
        __metaclass__ = Singleton
    
    c = BSingleton()
    d = BSingleton()
    assert c is d
    print(c.__class__.__name__, d.__class__.__name__)
    assert c is not a