objet – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Un objet proxy : ce que c’est et à quoi ça sert http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/ http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/#comments Sat, 01 Feb 2014 17:18:13 +0000 http://sametmax.com/?p=8076 autre objet en paramètre et le sauvegarde dans un de ses attributs. Quand on appelle les méthodes du proxy, le proxy appelle la même méthode de l'objet qu'il a en attribut, et retourne le résultat. Quand on set/get/delete un attribut du proxy, il fait la même chose sur l'autre objet.]]> Un objet proxy est un objet qui prend un autre objet en paramètre et le sauvegarde dans un de ses attributs. Quand on appelle les méthodes du proxy, le proxy appelle la même méthode de l’objet qu’il a en attribut, et retourne le résultat. Quand on set/get/delete un attribut du proxy, il fait la même chose sur l’autre objet.

Voilà une implémentation très basique d’un objet proxy en Python :

class Proxy(object):

    def __init__(self, obj):
        # L'objet passé en paramètre est
        # sauvegardé dans un attribut.
        # On le fait en utilisant 
        # object.__setattr__, qui est le
        # __setattr__ du parent, et non directement
        # en faisant self._obj = obj
        # afin d'éviter une boucle infinie car
        # nous écrasons __setattr__ plus bas.
        object.__setattr__(self, "_obj", obj)
        
    # On écrase les méthodes magiques __getattribute__
    # (qui est appelée quand on faire self.nom_attribut), 
    # __delattr__ (qui est appelée quand on fait 
    # del self.nom_attribut) et __setattr__ (qui est 
    # appelée quand on fait self.nom_attribut = truc)
    def __getattribute__(self, name):
        return getattr(object.__getattribute__(self, "_obj"), name)
    def __delattr__(self, name):
        delattr(object.__getattribute__(self, "_obj"), name)
    def __setattr__(self, name, value):
        setattr(object.__getattribute__(self, "_obj"), name, value)

Ca s’utilise comme ceci :

class UnObjetOrdinnaire(object):
    
    attribut = 'VALEUR !'
    
    def methode(self, param):
        return param * 2
    

objet_ordinnaire = UnObjetOrdinnaire()
# on passe l'objet ordinnaire au proxy
objet_proxy = Proxy(objet_ordinnaire)

# Accéder à des méthodes et attribut
# du proxy accède à ceux de l'objet
# derrière le proxy
print(objet_proxy.attribut)
## VALEUR !

print(objet_proxy.methode(3))
## 6

# Modifier l'objet proxy modifie
# l'objet derrière le proxy
objet_proxy.attribut = 'une autre valeur'

print(objet_ordinnaire.attribut)
## une autre valeur

Pour un exemple vraiment à l’épreuve des balles, il faut prendre en compte tout un tas de cas particuliers, ce qui fait qu’il est bien plus rentable d’utiliser une lib solide pour ça.

Attention, un objet proxy peut très bien avoir des méthodes qui n’appellent pas celles de l’objet derrière. On même avoir des méthodes qui appellent des méthodes qui n’ont pas le même nom, ou plusieurs méthodes… Ce que vous voyez en exemple est un proxy très basique.

Pourquoi voudrait-on obtenir ce résultat ?

Plusieurs design patterns font appel à des objets proxy. Par exemple, le design pattern “adapter” consiste à créer un objet proxy qui accepte plusieurs types d’objets en paramètres afin de présenter toujours la même interface.

Imaginez que vous ayez plusieurs objets qui servent à s’authentifier :


class Login(object):

    _is_logged = False 

    def is_logged(self):
        return self._is_logged

    def login(self, email, password):
        self._is_logged = True


class LoginWithUsername(object):

    _is_logged = False 

    def is_logged(self):
        return self._is_logged

    def login(self, username, password):
        self._is_logged = True


class SignIn(object):

    _is_logged = False 

    def is_logged(self):
        return self._is_logged

    def signin(self, email, password):
        self._is_logged = True


class LoginWithKey(object):

    _is_logged = False 
    
    key = "jfjkdlmqfjdmqsjdk"

    def is_logged(self):
        return self._is_logged
    
    def login(self, username):
        self._is_logged = True

Et vous avez un algo de parsing, qui attend un objet d’authentification. Si vous mettez le code qui permet de choisir la bonne API dans l’algo de parsing, vous liez l’algo (qui n’a rien à voir avec l’authentification) à toutes ces implémentations.

Une des manières de faire, est d’utiliser un adaptateur, dont voici une esquisse :

class AuthAdapter(object):


    def __init__(self, obj):
        object.__setattr__(self, "_obj", obj)

    def __getattribute__(self, name):
    
        try:
            # Si l'attribut existe sur le proxy, on l'utilise
            return object.__getattribute__(self, name)
        except AttributeError:
            # Sinon on tente le coup sur l'objet derrière le proxy
            return getattr(object.__getattribute__(self, "_obj"), name)
    
    
    def login(self, id_=None, secret=None):
        # Le login est différent pour chaque classe,
        # donc on s'arrange avec.
        try:
            self._obj.login(id_, secret)
        except AttributeError:
            self._obj.signin(id_, secret)
        except TypeError:
            self._obj.login(id_)
    

En gros, si on essaye d’appeler login(), il va lisser les contours et nous donner toujours la même interface, même si derrière l’objet peut marcher complètement différement. En revanche, si on appelle n’importe quel autre attribut ou méthode (par exemple is_logged, mais il pourrait y en avoir des dizaines d’autres dans la vrai vie vivante), ça tape directement dans l’objet derrière le proxy.

Donc si j’applique l’adaptateur systématiquement, quelle que soit la classe derrière, le comportement est toujours le même : j’appelle login(un id, un secret), et il se logge.

all_auth = (Login, LoginWithUsername, SignIn, LoginWithKey)

for auth in all_auth:
    # 'auth' est une des 4 classes de login. On l'instancie et
    # on met l'instance derrière le proxy
    auth = AuthAdapter(auth())
    print("Testing '%s'" % auth.__class__.__name__)
    print("Is logged : %s" % auth.is_logged())
    # Le login se passe toujours de la même manière, quelle que soit la classe
    auth.login('id', 'secret')
    print("Is logged after logging : %s" % auth.is_logged())

Comme d’habitude, ceci est un exemple naval. Il est bateau quoi. Mais cela vous démontre le principe.

Le design pattern façade ressemble à l’adapter, en fait c’est une spécialisation de l’adapter. Il s’agit juste d’exposer une interface plus simple, de l’objet derrière le proxy.

Un proxy peut aussi servir à hacker une lib. Par exemple, la lib attend un objet d’une ancienne version d’une autre lib dont l’auteur a déprécié un attribut. Avec un proxy, vous pouvez toujours faire semblant que l’attribut est toujours là : enrobez l’objet dans un proxy qui possède cet attribut, tout le reste de l’API sera la même.

Un proxy peut également être utile si vous voulez effectuer des actions quand l’objet est manipulé.

import logging

class ProxyLogger(object):

    def __init__(self, obj):
        object.__setattr__(self, "_obj", obj)

    def __getattribute__(self, name):
        obj = object.__getattribute__(self, "_obj")
        # On lance un warning à chaque accès à un attribut de l'objet
        # derrière le proxy
        logging.warning("%s.%s has been called" % (obj.__class__.__name__, name))
        return getattr(obj, name)

Maintenant, supposons que vous avez un objet d’une lib externe (que vous ne pouvez donc pas modifier) sur lequel vous avez besoin d’infos :

class ObjetExterne(object):
    def ahahah(self):
        pass

o = ProxyLogger(ObjetExterne())
o.ahahah()
## WARNING:root:ObjetExterne.ahahah has been called

Vous pouvez refiler le proxy à n’importe quel objet de sa lib d’origine, si le proxy est bien fait, elle ne vera pas la différence.

On finit sur une note culture, puisque le pattern decorator utilise souvent aussi un proxy. Cette fois d’une fonction sur un autre objet (en générale une autre fonction). Mais le principe est le même. Donc quand vous voyez @un_decorateur, il est peut être en train d’appliquer un proxy à la fonction.

D’ailleurs on dit souvent que le proxy décore l’objet qui est derrière.

]]>
http://sametmax.com/un-objet-proxy-ce-que-cest-et-a-quoi-ca-sert/feed/ 1 8076
La différence entre __new__ et __init__ en Python http://sametmax.com/la-difference-entre-__new__-et-__init__-en-python/ http://sametmax.com/la-difference-entre-__new__-et-__init__-en-python/#comments Fri, 11 Jan 2013 10:06:36 +0000 http://sametmax.com/?p=3534 __new__ et __init__ n'ont rien de spécial. Ce sont des méthodes ordinaires. Mais parce qu'elles sont nommées ainsi, Python les détecte et les appelle automatiquement a un moment précis.]]> Les méthodes __new__ et __init__ n’ont rien de spécial. Ce sont des méthodes ordinaires. Mais parce qu’elles sont nommées ainsi, Python les détecte et les appelle automatiquement a un moment précis.

Ce moment, c’est ce qui différencie __init__ de __new__.

__init__ pour initialiser

__init__ est la méthode qui va être appelée automatiquement après qu’un objet ai été crée. Ce n’est pas un contructeur du tout, c’est un initialiseur.

Si vous faîtes ça:

>>> class Premiere(object):
...         
...         def __init__(self, prix):
...                 print "%s euros" % prix
...         
>>> c = Premiere(10000)
10000 euros

A la ligne c = Premiere(10000), Python va créer une instance de la classe Première(). Il va ensuite immédiatement et automatiquement appeler __init__ en lui passant cette instance en premier argument et les paramètres passés par l’appel Premiere(paramètres). Donc, quand __init__ est appelé, l’objet instancié existe déjà.

On va utiliser __init__ pour initialiser l’objet, c’est à dire pour lui donner son état de départ: changer les attributs, configurer l’objet par rapports aux arguments, etc.

Dans tous les autres langages, on utiliserait le constructeur pour faire ce boulot. Pas en Python.

L’avantage de __init__, c’est qu’il est très facile à manipuler. Il n’y a pas de magie dangereuse dans __init__: on a l’objet tout neuf, et les arguments passés à l’instancitation, on peut donc manipuler l’objet sans se soucier du reste. Ici on attache deux attributs à l’instance self:

 >>> class Premiere(object):
...         discount = False
...         def __init__(self, prix):
...                 self.prix = prix
...                 if self.prix < 5000:
...                     self.discount = True
...         
>>> c = Premiere(10000)
>>> c.discount
False

Comme en Python les attributs sont dynamiques, on peut attacher un argument même si l’instance ne le déclare pas, et il est créé automatiquement.

En résumé: __init__ est appelé automatiquement APRES la création de l’objet, et on met dedans le code d’initialisation de l’objet (généralement une modification des attributs pour leur donner leur état de départ).

__new__ pour créer

__new__ est le vrai constructeur. Pour cette raison, elle doit retourner un objet.

>>> class Premiere(object):
...
...     def __new__(cls, prix):
...        print "%s euros" % prix
...        return super(Premiere, cls).__new__(cls)
...
>>> c = Premiere(10000)
10000 euros

__new__ est appelée AVANT la création de l’objet, car c’est son boulot de créer l’instance et de la retourner. Comme on ne sait pas retourner une instance nous même (enfin si, mais pas dans cet article :-)), on appelle super() pour utiliser la méthode __new__ de object et créer une instance pour cette classe.

L’objet créé sera ensuite passé à __init__ automatiquement par Python.

On utilise rarement __new__. Les deux cas principaux sont:

  • si on hérite d’un type immutable (str, int, tuple, etc), __new__ est le seul endroit où on puisse initialiser l’objet.
  • dans le cas des métaclasses.

En résumé: __new__ est le vrai constructeur, il est appelé pour créer l’objet, et l’objet ainsi instancié est passé à __init__. Vous n’avez presque aucune raison de vous en servir, c’est vraiment pour les cas particuliers.

Voici l’ordre d’éxécution:

>>> class Premiere(object):
...         def __new__(cls, prix):
...                 print "__new__"
...                 return super(Premiere, cls).__new__(cls)
...         def __init__(self, *args):
...                 print "__init__"

>>> c = Premiere(10000)
__new__
__init__

Exemple d’utilisation de __new__

Généralement on sait très bien utiliser __init__, mais __new__ est moins évident.

L’usage le plus fréquent de __new__ quand on hérite d’objets immutables. Par exemple, si vous voulez faire un objet Temperature qui hérite de float et qui accepte une unité en plus, ceci ne va pas marcher:

class Temperature(float):

    def __init__(self, value, unit):

        super(Temperature, self).__init__(value)
        self.unit = unit

    def __str__(self):
        return "%s%s" % (self.value, self.unit)

print Temperature(10, '°C')

Traceback (most recent call last):
  File "", line 11, in 
    Temperature(10, '°C')
TypeError: float() takes at most 1 argument (2 given)

La raison est que du fait de la nature immutable de float, il est initialisé dans __new__, et il n’attend aucune valeur de plus dans __new__, mais on lui passe malgré tout (via Temperature(10, '°C')).

En revanche, ceci va marcher:

class Temperature(float):

    def __new__(cls, value, unit):

        instance = super(Temperature, cls).__new__(cls, value)
        instance.unit = unit
        return instance

    def __str__(self):
        return "%s%s" % (super(Temperature, self).__str__(), self.unit)

print Temperature(10, '°C')
10.0°C

Comme on override __new__, on lui donne la possibilité d’accepter une argument de plus.

Un autre exemple serait de vouloir créer une chaîne de caractères qui est toujours en majuscule (ce qui est bien moins utile que l’exemple précédent):

class CapsLockString(str):

    def __init__(self, value):

        print value # et maintenant je fais quoi ?

print CapsLockString('test')
test
test

Ça ne plantera pas, mais il n’y a rien que nous puissions faire car str est immutable. On ne peut tout simplement pas faire quoique ce soit avec value. Avec __new__, on peut faire quelque chose sur la chaîne intermédiaire:

class CapsLockString(str):

    def __new__(cls, value):

        return super(CapsLockString, cls).__new__(cls, value.upper())

print CapsLockString('test')
TEST

Deux chaînes sont en fait créées, une normale, puis une en majuscule retournée par upper() qui va servir de valeur à notre objet (en fait il y en a même 3 dans l’implémentation CPython, c’est pour ça que les notations littérales sont plus rapides que l’usage des classes pour créer des built-in).

__new__ permet donc essentiellement de créer de jolis API. On l’utilise par ailleurs dans les metaclasses, mais ce sera pour un autre article.

Un troisième usage de __new__, assez rare (mais en même temps utiliser __new__ est déjà rare), c’est le pattern factory. Les javaistes le connaissent bien, c’est un motif de conception qui permet de gérer la création d’objets qui peuvent eux même créer des objets, qui créer des objets qui… Bref.

Car en fait __new__ peut retourner n’importe quoi. Il peut retourner toujours la même instance pour faire un singleton par exemple. On peut même carrément renvoyer un truc qui n’a rien n’a voir, par exemple une fonction :

class FonctionFactory(object):

    def __new__(self, value, repeat):

        def repeater(string=value):

            return string * repeat

        return repeater


>>> function = FonctionFactory('hello', 2) # création de la fonction
>>> print function()
hellohello
>>> print function('bonjour')
bonjourbonjour

Ici on retourne carrément une fonction, et pas du tout une instance de FonctionFactory() comme prévu. On pourrait faire ceci de manière plus simple avec de la programmation fonctionnelle, mais __new__ permet de bénéficier de tout l’outillage de la POO.

]]>
http://sametmax.com/la-difference-entre-__new__-et-__init__-en-python/feed/ 21 3534