1
votes

Modèle de stratégie en python

Je viens de l'arrière-plan C # et pour mettre en œuvre un modèle de stratégie, nous utiliserions toujours une interface, par exemple: ILoggger. Maintenant que je comprends, dans les langages de type canard tels que Python, nous pouvons éviter cette classe / contrat de base.

Ma question est la suivante: est-ce la meilleure façon de mettre en œuvre un modèle de stratégie en tirant parti du typage canard? Et, est-ce que cette manière de taper du canard de le faire indique clairement au prochain utilisateur de mon code qu'il s'agit d'un "point d'extension"? De plus, je pense qu'il est préférable d'utiliser des indices de type pour aider la personne suivante à regarder votre code pour voir quel devrait être le type de stratégie, mais avec le typage canard sans classe / contrat de base, quel type utilisez-vous? Une des classes déjà concrètes?

Voici du code:

class FileSystemLogger():
    def log(self, msg):
        pass

class ElasticSearchLogger():
    def log(self, msg):
        pass

# if i wanted to use type hints, which type should logger be here?
class ComponentThatNeedsLogger():
    def __init__(self, logger):
        self._logger = logger

# should it be this?
class ComponentThatNeedsLogger():
    def __init__(self, logger : FileSystemLogger):
        self._logger = logger

Quelqu'un pourrait-il indiquer quelle est la façon la plus standard / pythonique / lisible de gérer ce?

Je ne recherche pas la réponse "voici la réponse en 2 lignes de code".


6 commentaires

Probablement une dupe de Comment rédiger une stratégie motif ...


De quelle manière auriez-vous besoin de métaclasses ici? ComponentThatNeedsLogger .__ init__ a juste besoin d'un argument qui a une méthode log .


La question est la suivante: définissez-vous des classes * Logger parce que ComponentThatNeedsLogger utilise un objet qui appelle une méthode log , ou fait ComponentThatNeedsLogger utiliser un objet avec une méthode log parce que vous avez défini des classes * Logger ? Vous pouvez probablement vous en tirer simplement en passant une fonction (avec, par exemple, tapez Callable [str, None] ) à ComponentThatNeedsLogger . Tout ne doit pas être implémenté en termes de classes.


Peut-être pourriez-vous utiliser un BaseLogger avec une méthode de journalisation @abstractmethod def log (...) que vos enregistreurs concrets étendent? Voir classes abstraites en python si vous voulez y aller la route très explicite basée sur les classes ...


@PatrickArtner: C'est exactement comme ça que je le ferais si j'utilisais le même formulaire qu'en C #, mais la question était de savoir si le typage canard fournit une réponse plus élégante.


J'ai présenté ma suggestion comme réponse, vous avez également deux autres réponses - quiconque trouvera votre question à l'avenir a un bon choix de réponses sur lesquelles s'appuyer. Je pense que vos questions conviennent bien à SO même si vous n'obtenez pas tout à fait ce que vous recherchiez. Peut-être que d'autres présenteront également d'autres solutions.


3 Réponses :


3
votes

Si vous vouliez vraiment aller jusqu'au bout des classes et appliquer votre utilisation de classe de base, créez un ABC: classe / méthode de base abstraite et quelques implémentations de celle-ci:

Attribut: utilisé Alex Vasses répond ici à des fins de recherche

0
1
2
3
4 
file: 0
file: 1
file: 2
file: 3
file: 4

logger needs to inherit from BaseLogger

Ensuite, utilisez-les:

# import typing # for other type things

class DoIt:
    def __init__(self, logger: BaseLogger):
        # enforce usage of BaseLogger implementation
        if isinstance(logger, BaseLogger):
            self.logger = logger
        else:
            raise ValueError("logger needs to inherit from " + BaseLogger.__name__)

    def log(self, message):
        # use the assigned logger
        self.logger.log(message)

# provide different logger classes
d1 = DoIt(ConsoleLogger())
d2 = DoIt(FileLogger())

for k in range(5):
    d1.log(str(k))
    d2.log(str(k))

with open(d2.logger.fn) as f:
    print(f.read())

try:
    d3 = DoIt( NotALogger())
except Exception as e:
    print(e)

Résultat: p>

from abc import ABC, abstractmethod

class BaseLogger(ABC):
    """ Base class specifying one abstractmethod log - tbd by subclasses."""
    @abstractmethod
    def log(self, message):
        pass

class ConsoleLogger(BaseLogger):
    """ Console logger implementation."""
    def log(self, message):
        print(message)

class FileLogger(BaseLogger):
    """ Appending FileLogger (date based file names) implementation."""
    def __init__(self):
        import datetime 
        self.fn = datetime.datetime.now().strftime("%Y_%m_%d.log")

    def log(self,message):
        with open(self.fn,"a") as f:
            f.write(f"file: {message}\n")

class NotALogger():
    """ Not a logger implementation."""
    pass

Pour rappel: python a déjà des capacités assez sophistiquées pour se connecter. Regardez dans Journalisation si c'est le seul but de votre demande. P >


2 commentaires

C'est ainsi que je l'écrirais si j'utilisais le formulaire C #. J'essayais de demander s'il y avait une manière plus "pythonique" de faire ceci car il a le typage de canard. Aussi, je connais le module de journalisation python, je viens de l'utiliser par exemple.


Parmi les réponses fournies ici pour le moment, je vais dire que je préfère celle-ci, car si vous allez écrire une application moyenne à grande, vous aurez besoin d'un code stable / lisible / évolutif et il semble que cette approche corresponde ces exigences les plus. Bien que je pense avant de commencer à écrire cette application, je vais lire un livre sur la POO en python, afin de mieux le comprendre.



1
votes

En Python, il n'y a généralement pas besoin d'un modèle de stratégie à grande échelle avec une application de type au moment de la compilation grâce à la sécurité d'exécution et à la terminaison gracieuse.

Si vous souhaitez personnaliser certaines parties du code existant, la pratique courante est la suivante:

  • remplacer la méthode appropriée - que ce soit par sous-classement, incl. avec un mix-in, ou juste une affectation (une méthode est un attribut d'un objet vivant comme les autres et peut être réaffecté de la même manière; le remplacement peut même être non pas une fonction mais un objet avec __call__ );
    • notez qu'en Python, vous pouvez créer des objets contenant du code (qui inclut des fonctions et des classes) à la volée en plaçant sa définition dans une fonction. La définition est ensuite évaluée pendant l'exécution de la fonction englobante et vous pouvez utiliser des variables accessibles (aka fermeture) comme paramètres ad-hoc; ou
  • accepter un rappel à un moment donné (ou un objet dont vous appellerez les méthodes à des moments appropriés, agissant effectivement comme un ensemble de rappels); ou
  • accepter un paramètre de chaîne qui est une constante d'un certain ensemble que le code teste ensuite dans if / else ou recherche dans un registre (qu'il soit global à un module ou local à une classe ou à un objet);
    • il y a enum depuis 3.4 mais pour les cas simples, il est considéré comme trop d'inconvénients pour les avantages (est illisible lors du débogage et nécessite des passe-partout) car Python est plus du côté de la flexibilité que C # sur l'échelle de flexibilité vs évolutivité.

6 commentaires

Pour le n ° 1, je ne sais pas comment cela s'applique à une situation où je peux avoir un ensemble de stratégies. Le remplacement des méthodes à la volée me semble que cela ne demande que des problèmes. Pour le n ° 2, un rappel peut fonctionner, mais que se passe-t-il si plus tard vous avez besoin de plusieurs méthodes, avec un rappel, vous essayez de remplir toutes sortes de paramètres facultatifs pour le rappel afin de tenir compte des multiples méthodes. L'objet que je suis d'accord est un bon choix, mais je ne vois toujours pas comment cela s'applique aux situations où j'ai besoin de quelque chose comme un modèle de stratégie. Pour le n ° 3, que se passe-t-il si j'en ai besoin à 15 endroits?


@InfinityHorizon Vous n'avez pas spécifié votre cas d'utilisation, donc je ne sais pas si vous devez sélectionner "stratégie" par classe, par objet ou par appel - j'ai donc répertorié toutes les options. Le n ° 1 est le meilleur pour la sélection par classe; le remplacement à la volée est utile par ex. monkey-patching où vous n'avez pas le contrôle sur le code ou la création d'objets sur mesure (créez effectivement une nouvelle classe mais avec moins de frais généraux). # 2 est le meilleur pour sélectionner par objet et agit au niveau de la sous-méthode (en fait, vous remplacez une partie de l'implémentation d'une méthode) mais vous pouvez également accepter un rappel à chaque appel. # 3 est pour sélectionner par appel.


Si vous passez une méthode liée ou non liée comme rappel, vous pouvez la paramétrer via "son" objet, indépendamment de l'objet "hôte".


Je pense que notre définition du modèle de stratégie est différente. Je parle de quelque chose de plus proche du modèle de stratégie du GOF, tandis que vous parlez de quelque chose de plus générique. Je ne suis toujours pas d'accord pour dire que le monkey-patching est une bonne approche. Si quelqu'un regarde votre classe A, il voudra savoir ce qu'il fait (il vérifiera également ses sous / super classes). Avec le patching de singe, il doit regarder tout le dépôt pour savoir exactement comment il est utilisé. Il serait extrêmement dangereux de changer cette classe sans vérifier l'ensemble du dépôt ...


Ce que blackwasp.co.uk/Strategy.aspx décrit est l'ensemble n ° 2 "de option "callbacks", par objet. Mais le principe général est le même - "Au lieu d'implémenter directement un seul algorithme, le code reçoit des instructions d'exécution quant à lesquels dans une famille d'algorithmes utiliser. " - alors pourquoi limiter l'explication et les options à cela? Il n'est pas pertinent de savoir à quel moment avant l'appel ces "instructions" sont reçues, l'effet net est le même.


Oui, le monkey-patching n'est pas évolutif, alors ne l'utilisez pas si vous avez besoin d'évolutivité plus que de flexibilité supplémentaire. Sa principale force est la capacité à gérer du code hors de votre contrôle (qui change rarement), et c'est la personne qui corrige plutôt que l'auteur de la classe d'origine qui est responsable de réagir aux changements. C'est toujours une option alors j'ai dû le mentionner.



1
votes

Pour autant que je sache, la manière la plus courante d'implémenter le modèle de stratégie en Python est de passer une fonction (ou appelable). Les fonctions sont des objets de première classe en Python, donc si tout ce dont le consommateur a besoin est une fonction, vous n'avez pas besoin de lui donner plus que cela. Bien sûr, vous pouvez l'annoter si vous le souhaitez. En supposant que vous ne souhaitiez enregistrer que des chaînes:

class ComplexLogger:
    def __init__(self, lots_of_dependencies):
        # initialize the logger here

    def log(self, msg: str): None
        # call private methods and the dependencies as you need.

    def _private_method(self, whatever):
        # as many as you need.

ComponentThatNeedsLogger(
    log_func= ComplexLogger(lots_of_dependencies).log
)

Ceci vous permet de créer un simple enregistreur à la volée:

ComponentThatNeedsLogger(
    log_func=print
)

Mais vous pouvez aussi tirer parti de toute la puissance des classes pour créer un enregistreur complexe et transmettre uniquement la méthode appropriée.

class ComponentThatNeedsLogger:
    def __init__(self, log_func: Callable[[str], None]):
        self._log_func = log_func


2 commentaires

Je comprends cela, mais que faire si j'ai besoin plus tard d'avoir logDebug / logError / logInfo ... c'est l'inconvénient de simplement passer une fonction. Vous pourriez également utiliser un deuxième paramètre de LOGTYPE, mais vous pourrez alors entrer dans un endroit où vous aurez un ensemble de paramètres optionnels que l'utilisateur devra savoir quelle combinaison d'entre eux utiliser dans quelle situation.


Si vous voulez plusieurs stratégies de journalisation pour différents types de messages, vous pouvez passer plusieurs fonctions.