proxy – 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
Objets proxy et pattern adapter en Python http://sametmax.com/objets-proxy-et-pattern-adapter-en-python/ http://sametmax.com/objets-proxy-et-pattern-adapter-en-python/#comments Fri, 16 Aug 2013 19:40:02 +0000 http://sametmax.com/?p=7112 En informatique, le vocabulaire c’est une bonne partie du travail. On a des tas de termes comme polymorphisme, récursivité, idempotent ou closure. Certains sont des termes mathématiques, d’autres sont des anglicismes, mais la majorité sont juste des mots compliqués pour décrire des choses simples.

Vous connaissez mon manque d’attrait pour ça, on va donc clarifier.

Pour comprendre cet article, il vous faut les pré-requis suivants :

  • Être à l’aise avec avec la programmation orientée objet : héritage, property…
  • Bien comprendre la notion de référence.
  • Avoir un peu de temps devant soi.

Et pour une fois, on va mettre un morceau qui colle bien au blog :

S’il vous plait, dessine moi un objet proxy

Un proxy, en anglais, c’est un mandataire, c’est à dire grosso merdo un intermédiaire. Un objet proxy est donc tout simplement un objet qui fait l’intermédiaire, généralement entre un objet et un autre.

Exemple, on a des enseignants qui vont avoir besoin d’une autorisation de sortie pour des enfants dans leur classe. Un enfant ne peut pas décider de cela, ses parents décident de ce genre de chose :

class Enseignant(object):

    # la demande d'autorisation attend un objet enfant en paramètre
    def demande_autorisation_de_sortie(self, enfant):
        return enfant.peut_sortir()


class Enfant(object):

    def __init__(self, age):
        self.age = age

    # mais un enfant ne peut pas donner cette information
    def peut_sortir(self):
        raise NotImplementedError('Un enfant ne peut decider de cela')


class Parent(object):

    # le parent va faire proxy sur l'objet enfant
    def __init__(self, enfant):
        self.enfant = enfant

    # et propose la même méthode que l'objet enfant afin de pouvoir
    # être passé en paramètre à sa place à la méthode
    # demande_autorisation_de_sortie()
    def peut_sortir(self):
        return self.enfant.age > 10

Ici, si la classe Enseignant appelle directement la méthode peut_sortir() de la classe Enfant, on obtient une erreur. :

>>> prof = Enseignant()
>>> enfant = Enfant(11)
>>> prof.demande_autorisation_de_sortie(enfant)
Traceback (most recent call last):
  File "", line 1, in 
    prof.demande_autorisation_de_sortie(enfant)
  File "", line 4, in demande_autorisation_de_sortie
    return enfant.peut_sortir()
  File "", line 13, in peut_sortir
    raise NotImplementedError('Un enfant ne peut decider de cela')
NotImplementedError: Un enfant ne peut decider de cela

Nous avons créé un système où on est obligé de créer une instance de Parent, qui va se placer entre la classe Enseignant et la classe Enfant, et qui va retourner la valeur pour cette méthode :

>>> parent = Parent(enfant) # on passe l'enfant en paramètre au parent
>>> prof.demande_autorisation_de_sortie(parent) # et le parent remplace l'enfant
True

Ceci est un objet proxy, c’est à dire un objet qui se place entre deux objets, et fait l’intermédiaire. Pour que cela marche, il faut que l’objet proxy, ici l’instance de Parent, ait la même interface (les mêmes méthodes, avec les mêmes paramètres et noms) que l’objet derrière le proxy, ici l’instance de Enfant.

Dans notre cas, c’est académique, ça ne sert pas à grand chose. Mais il y a des tas de cas utiles pour les objets proxy.

Ainsi, on veut parfois créer des objets dit “lazy”, littéralement “paresseux”, c’est à dire pas évalués tout de suite. C’est ce que fait Django dans le cadre de certaines chaînes de caractères traduites. En effet, en Django, il existe une fonction lazy_gettex() que l’on peut appliquer sur les chaînes à traduire. Mais elle ne retourne pas une string, car on ne connait pas encore la langue dans laquelle il faut traduire la chaîne : la langue dépend de l’utilisateur qui viendra demander la page Web en question. lazy_gettex() retourne un objet proxy, et quand, à l’appel de la page Web, on tente de manipuler cet objet comme une string pour l’afficher, le proxy va chercher la traduction, et retourne la bonne chaîne. Pour que ça marche, il faut que l’interface de l’objet lazy ressemble trait pour trait à celui d’une chaîne.

Dans d’autres cas on veut mettre un proxy pour gérer des permissions, et selon le niveau de permission, l’objet proxy donne accès ou non à l’objet derrière lui.

Le design pattern décorateur, le fameux @truc en Python, n’est jamais qu’un objet fonction qui fait proxy vers une autre fonction qu’on a enrobé.

Mais la raison majeur pour laquelle on va utiliser un objet proxy est le design pattern “adapter”.

Ce n’est pas un chapeau, c’est un boa qui a avalé un éléphant

En informatique, on aime bien les traitements généralistes.

Par exemple, imaginez cet objet qui dit si on a le droit de se bourrer la gueule :

class Videur(object):

    def tu_peux_rentrer(self, personne):
        """
            Cette fonction n'est pas réaliste, puisque bien entendu une mineure
            à gros nichons rentrerait, et un arabe en short lacoste non,
            mais heureusement l'informatique est plus simple que notre société.
        """
        return personne.age >= 18

class Personne(object):
    def __init__(self, age):
        self.age = age

Et tant que vous avez la main sur le système, tout va bien dans le meilleur des mondes :

>>> import random
>>> clients = [Personne(random.randint(12, 65)) for x in xrange(random.randint(0, 100))]
>>> videur = Videur()
>>> for client in clients:
...     if videur.tu_peux_rentrer(client):
...         print("Glouglou")
...
Glouglou
Glouglou
Glouglou
Glouglou
Glouglou
...

Mais malheureusement la vie n’est pas toujours pleine de licornes et de bisounours, et votre collègue, lui, il a codé une lib en C qui a extrait les clients d’un XML, et quand vous vous mappez directement dessus avec ctype, vous obtenez des clients comme ça:

class Client(object):
    def __init__(self, majeur):
        self.majeur = majeur

Bon, vous vous dites que c’est pas grave. Après tout, c’est juste une condition à rajouter :

class Videur(object):

    def tu_peux_rentrer(self, personne):

        if hasattr(personne, 'age'):
            return personne.age >= 18

        return personne.majeur

Ouai mais votre commercial arrive avec un super contrat avec pillierdebar.com, et il faut s’interfacer avec leur API SOAP de 1997, dont la deserialisation vous retourne :

class Prospect(object):
    def __init__(self, datetime):
        self.date_de_naissance = datetime

Les connards. Ah les connards.

Mais bon, c’est pas grave, c’est juste une petite condition à rajouter.

import datetime

class Videur(object):

    def tu_peux_rentrer(self, personne):

        if hasattr(personne, 'age'):
            return personne.age >= 18

        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18

        return personne.majeur

Bon, vous allez enfin pouvoir soufll…. Ahhhh ! Votre boss appelle, il a oublié de vous dire, le stagiaire travaille aussi sur une feature, et a fait son propre objet Prospect, c’est comme l’objet Client mais pas pareil. Non on ne peut pas changer. Oui il a fait de la merde mais si on change tout son code pête. Il se barre demain. Il n’a pas fait de test. Démerde toi.

class LeClient(object):
    def __init__(self, majeur_de_combien):
        self.__majeur_de_combien = majeur_de_combien

    def get_majeur_de_combien(self):
        return self.__majeur_de_combien

A ce stade là, le code est tellement moche de toute façon, vous vous en branlez complètement. Vous allez rentrer jouer Left4Dead avec un mod qui transforme les zombies pour qu’ils aient la tête de votre patron. Le reste n’est que détail d’implémentation.

class Videur(object):

    def tu_peux_rentrer(self, personne):

        if hasattr(personne, 'age'):
            return personne.age >= 18

        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18

        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1

        return personne.majeur

Quelque moi(s) plus tard, les specs changent. Il faut maintenant pouvoir vérifier la carte d’identité des clients. Ou la carte de membre. Et garder l’ancien système compatible bien entendu. Ah, et il y a aussi des controleurs, c’est comme des videurs, mais qui doivent vérifier si les personnes dans le bar sont majeurs, sauf qu’il n’acceptent pas la carte de membre ou la date de naissance donnée à l’oral.

class CarteDeMembre(object):
    pass


class CarteIdentité(object):
    def __init__(self, date_de_naissance):
        self.date_de_naissance = date_de_naissance


class Client(object):
    def __init__(self, majeur=None, carte_identite=None, carte_membre=None):
        self.majeur = majeur
        self.carte_identite = carte_identite
        self.carte_membre = None


class Videur(object):

    def tu_peux_rentrer(self, personne):

        if hasattr(personne, 'age') and personne.age is not None:
            return personne.age >= 18

        if hasattr(personne, 'carte_membre'):
            return True

        if hasattr(personne, 'carte_identite') and personne.carte_identite is not None:
            now = datetime.datetime.now()
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18

        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18

        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1

        return personne.majeur


class Controleur(object):

    def est_autorise(self, personne):

        if hasattr(personne, 'age') and personne.age is not None:
            return personne.age >= 18

        if hasattr(personne, 'carte_identite') and personne.carte_identite is not None:
            now = datetime.datetime.now()
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18

        if hasattr(personne, 'date_de_naissance'):
            # TODO: prendre en compte les années bisextiles
            return (datetime.datetime.now() - personne.date_de_naissance) / 365 > 18

        if hasattr(personne, 'get_majeur_de_combien'):
            # retourne le nombre d'années après 18 ans ou -1. Don't ask...
            return personne.get_majeur_de_combien() > -1

        return personne.majeur

Bon, là c’est la merde. Vous le sentez, demain il va y avoir un portail électronique qui check uniquement les cartes de membres. Quand il va falloir faire une modif quelque part, ça va être galère. La maintenance va être un enfer. Le debuggage, bien relou. Il faut trouver un moyen de faire plus propre.

C’est ici qu’entre en jeu le pattern “adapter”. Un adapter, c’est comme les adaptateurs pour prises, mais pour les objets. C’est un proxy qu’on met devant pour que la prise ressemble à une autre.

Dans le domaine du code, ça veut dire qu’on va enrober chaque objet dans un autre pour qu’ils aient tous la même interface. Le traitement du coup sera de nouveau très simple, et la complexité sera répartie dans les adapters.


# on met le code commun dans un parent abstrait, notamment le check pour
# savoir si un adapter convient a un objet donné
class Adapteur(object):

    cls = None

    def __init__(self, objet):
        self.objet = objet

    def peut_adapter(self, objet):
        return self.cls == type(objet)


# un adapter, c'est juste un objet proxy qui arrondit les angles
class PersonneAdapteur(Adapteur):

    cls = Personne

    # on va réduire la complexité en faisant en sorte que tous les checks
    # soient cachés derrière une simple property
    @property
    def majeur(self):
        return self.objet >= 18


class ProspectAdapteur(Adapteur):

    cls = Prospect

    @property
    def majeur(self):
        # TODO: prendre en compte les années bisextiles
        return (datetime.datetime.now() - self.objet.date_de_naissance) / 365 > 18


class LeClientAdapteur(Adapteur):

    cls = LeClient

    @property
    def majeur(self):
        # retourne le nombre d'années après 18 ans ou -1. Don't ask...
        return self.objet.get_majeur_de_combien() > -1


class ClientAdapteur(Adapteur):

    cls = Client

    def __init__(self, objet):
        self.objet = objet

    @property
    def majeur(self):

        if self.accepte_carte_membre and self.objet.carte_membre is not None:
            return True

        if self.objet.majeur:
            return True

        if self.object.carte_identite is not None:
            now = datetime.datetime.now()
            # TODO: prendre en compte les années bisextiles
            return (now - personne.carte_identite.date_de_naissance) / 365 > 18

        return False

Le contrôleur et le videur deviennent du coup beaucoup plus simples, maintenant que la logique est normalisée :

class VerificateurDeMajorite(object):
    def check_majorite(self, personne):
        for adapteur in self.adapteurs:
            if adapteur.peut_adapter(personne):
                return adapteur(personne).majeur

# du coup un vérificateur, c'est très déclaratif
class Videur(VerificateurDeMajorite):

    adapteurs = (
        PersonneAdapteur,
        ClientAdapteur,
        ProspectAdapteur,
        LeClientAdapteur,
    )

    def tu_peux_rentrer(self, personne):
        return self.check_majorite(personne)


class Controleur(VerificateurDeMajorite):

    adapteurs = (
        PersonneAdapteur,
        # ProspectAdapteur()
        LeClientAdapteur(),
    )

    def est_autorise(self, personne):
        return self.check_majorite(personne)

Si il faut ajouter un autre format de client, il suffit de coder un adapteur. Si il faut ajouter une autre classe de vérification, il suffit de lui donner les bons adapteurs.

Et le traitement est aussi simple, car si vous avez une liste qui contient plein de clients différents :

>>> clients = [Client(majeur=True), Client(carte_de_membre=CarteDeMembre()), Client(carte_identite=CarteIdentité(datetime.datetime.now()), LeClient(-1), Personne(age=16)...]

Vous pouvez quand même vérifier tout ça sans y réfléchir:

>>> videur = Videur()
>>> for client in clients:
...     if videur.tu_peux_rentrer(client):
...         print("Glouglou")
...
Glouglou
Glouglou
...

>>> controlleur = Controleur()
>>> for client in clients:
...     if not controlleur.est_autorise(client):
...         print("Ahhhh !")
...
Ahhhh !
Ahhhh !
...

Dans un cas comme celui de notre exemple, l’adapter est un peu overkill, on s’en tirerait aussi bien avec une liste de fonctions, et le code serait plus léger et lisible.

Mais quand les objets et la logique deviennent très complexes, tout ramener à une interface commune permet de simplifier énormément le code.

]]>
http://sametmax.com/objets-proxy-et-pattern-adapter-en-python/feed/ 14 7112
Récupérer l’IP client avec Nginx en proxy sous CherryPy/Django/Bottle – proxy_set_header http://sametmax.com/recuperer-lip-client-avec-nginx-en-proxy-sous-cherrypy-proxy_set_header/ Thu, 06 Sep 2012 02:11:26 +0000 http://sametmax.com/?p=2041 Si vous utilisez Nginx en proxy, vous allez vous heurter à son IP lorsque votre script tentera de récupérer l’IP du client soit 127.0.0.1.

En rajoutant ces quelques lignes à votre config Nginx:

location / { 
 # ici un proxy pass quelconque
 proxy_pass http://cherrypy;

 # et ici la magie opère
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

Dans votre script python (ici Bottle), vous pouvez du coup la récupérer:

#get user IP
user_ip = request.remote_addr
]]>
2041