web-dev-qa-db-fra.com

Modèle Python Observer: exemples, astuces?

Existe-t-il des exemples de GoF Observer mis en œuvre en Python? J'ai un code de bit qui contient actuellement des morceaux de code de débogage liés à la classe de clés (générant actuellement des messages vers stderr si un env magique est défini). De plus, la classe possède une interface pour renvoyer les résultats de manière incrémentielle, ainsi que pour les stocker (en mémoire) en vue du post-traitement. (La classe elle-même est un gestionnaire de travaux permettant d'exécuter simultanément des commandes sur des machines distantes via ssh).

Actuellement, l'utilisation de la classe ressemble à quelque chose comme:

job = SSHJobMan(hostlist, cmd)
job.start()
while not job.done():
    for each in job.poll():
        incrementally_process(job.results[each])
        time.sleep(0.2) # or other more useful work
post_process(job.results)

Un modèle d'utilisation alternatif est:

job = SSHJobMan(hostlist, cmd)
job.wait()  # implicitly performs a start()
process(job.results)

Tout cela fonctionne bien pour l'utilitaire actuel. Cependant, il manque de flexibilité. Par exemple, je prends actuellement en charge un format de sortie bref ou une barre de progression en tant que résultats incrémentiels. Je prends également en charge les sorties Brief, complete et "message fusionné" pour la fonction post_process().

Cependant, je souhaite prendre en charge plusieurs flux de résultats/résultats (barre de progression vers le terminal, débogage et avertissements dans un fichier journal, sorties des travaux réussis vers un fichier/répertoire, messages d'erreur et autres résultats des travaux infructueux vers un autre. , etc).

Cela ressemble à une situation qui appelle Observer ... si des instances de ma classe acceptent les inscriptions d'autres objets et les rappellent avec des types d'événements spécifiques au fur et à mesure qu'ils se produisent.

Je regarde PyPubSub depuis que j'ai vu plusieurs références à cela dans des questions relatives à SO. Je ne suis pas sûr d'être prêt à ajouter la dépendance externe à mon utilitaire, mais je pourrais voir l'intérêt d'utiliser leur interface comme modèle pour le mien si cela simplifie l'utilisation pour les autres. (Le projet est conçu à la fois comme un utilitaire de ligne de commande autonome et une classe permettant d'écrire d'autres scripts/utilitaires).

En bref, je sais comment faire ce que je veux ... mais il existe de nombreuses façons de le réaliser. Je souhaite des suggestions sur ce qui est le plus susceptible de fonctionner pour les autres utilisateurs du code à long terme.

Le code lui-même est à: classh .

38
Jim Dennis

Cependant, il manque de flexibilité.

Eh bien ... en fait, cela ressemble à une bonne conception pour moi si une API asynchrone est ce que vous voulez. C'est habituellement. Peut-être que tout ce dont vous avez besoin est de passer de stderr au module logging de Python, qui possède une sorte de modèle de publication/abonnement qui lui est propre, avec Logger.addHandler() et ainsi de suite.

Si vous souhaitez soutenir les observateurs, mon conseil est de garder les choses simples. Vous n'avez vraiment besoin que de quelques lignes de code.

class Event(object):
    pass

class Observable(object):
    def __init__(self):
        self.callbacks = []
    def subscribe(self, callback):
        self.callbacks.append(callback)
    def fire(self, **attrs):
        e = Event()
        e.source = self
        for k, v in attrs.iteritems():
            setattr(e, k, v)
        for fn in self.callbacks:
            fn(e)

Votre classe de travail peut sous-classe Observable. Lorsque quelque chose d’intérêt se produit, appelez self.fire(type="progress", percent=50) ou un message similaire.

46
Jason Orendorff

Je pense que les gens dans les autres réponses en font trop. Vous pouvez facilement réaliser des événements en Python avec moins de 15 lignes de code.

Vous avez simple deux classes: Event et Observer. Toute classe qui souhaite écouter un événement, doit hériter de Observer et être configurée pour écouter (observer) un événement spécifique. Lorsqu'une Event est instanciée et déclenchée, tous les observateurs qui écoutent cet événement exécutent les fonctions de rappel spécifiées.

class Observer():
    _observers = []
    def __init__(self):
        self._observers.append(self)
        self._observables = {}
    def observe(self, event_name, callback):
        self._observables[event_name] = callback


class Event():
    def __init__(self, name, data, autofire = True):
        self.name = name
        self.data = data
        if autofire:
            self.fire()
    def fire(self):
        for observer in Observer._observers:
            if self.name in observer._observables:
                observer._observables[self.name](self.data)

Exemple :

class Room(Observer):

    def __init__(self):
        print("Room is ready.")
        Observer.__init__(self) # Observer's init needs to be called
    def someone_arrived(self, who):
        print(who + " has arrived!")

room = Room()
room.observe('someone arrived',  room.someone_arrived)

Event('someone arrived', 'Lenard')

Sortie:

Room is ready.
Lenard has arrived!
18
Pithikos

Quelques autres approches ...

Exemple: le module de journalisation

Peut-être que tout ce dont vous avez besoin est de passer de stderr au module logging de Python, qui possède un modèle de publication/abonnement puissant.

Il est facile de commencer à produire des enregistrements de journal.

# producer
import logging

log = logging.getLogger("myjobs")  # that's all the setup you need

class MyJob(object):
    def run(self):
        log.info("starting job")
        n = 10
        for i in range(n):
            log.info("%.1f%% done" % (100.0 * i / n))
        log.info("work complete")

Du côté des consommateurs, il y a un peu plus de travail. Malheureusement, la configuration de la sortie de l’enregistreur nécessite environ 7 lignes de code. ;)

# consumer
import myjobs, sys, logging

if user_wants_log_output:
    ch = logging.StreamHandler(sys.stderr)
    ch.setLevel(logging.INFO)
    formatter = logging.Formatter(
        "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
    ch.setFormatter(formatter)
    myjobs.log.addHandler(ch)
    myjobs.log.setLevel(logging.INFO)

myjobs.MyJob().run()

D'autre part, le paquet de journalisation contient une quantité incroyable d'éléments. Si vous avez besoin d'envoyer des données de journal à un ensemble de fichiers en rotation, une adresse électronique et le journal des événements Windows, vous êtes couvert.

Exemple: observateur le plus simple possible

Mais vous n'avez pas besoin d'utiliser une bibliothèque du tout. Un moyen extrêmement simple d'aider les observateurs est d'appeler une méthode qui ne fait rien.

# producer
class MyJob(object):
    def on_progress(self, pct):
        """Called when progress is made. pct is the percent complete.
        By default this does nothing. The user may override this method
        or even just assign to it."""
        pass

    def run(self):
        n = 10
        for i in range(n):
            self.on_progress(100.0 * i / n)
        self.on_progress(100.0)

# consumer
import sys, myjobs
job = myjobs.MyJob()
job.on_progress = lambda pct: sys.stdout.write("%.1f%% done\n" % pct)
job.run()

Parfois, au lieu d'écrire un lambda, vous pouvez simplement dire job.on_progress = progressBar.update, qui est Nice.

C'est à peu près aussi simple que cela devient. Un inconvénient est qu'il ne prend pas naturellement en charge plusieurs auditeurs abonnés aux mêmes événements.

Exemple: événements de type C #

Avec un peu de code de support, vous pouvez obtenir des événements de type C # en Python. Voici le code:

# glue code
class event(object):
    def __init__(self, func):
        self.__doc__ = func.__doc__
        self._key = ' ' + func.__name__
    def __get__(self, obj, cls):
        try:
            return obj.__dict__[self._key]
        except KeyError, exc:
            be = obj.__dict__[self._key] = boundevent()
            return be

class boundevent(object):
    def __init__(self):
        self._fns = []
    def __iadd__(self, fn):
        self._fns.append(fn)
        return self
    def __isub__(self, fn):
        self._fns.remove(fn)
        return self
    def __call__(self, *args, **kwargs):
        for f in self._fns[:]:
            f(*args, **kwargs)

Le producteur déclare l'événement à l'aide d'un décorateur:

# producer
class MyJob(object):
    @event
    def progress(pct):
        """Called when progress is made. pct is the percent complete."""

    def run(self):
        n = 10
        for i in range(n+1):
            self.progress(100.0 * i / n)

#consumer
import sys, myjobs
job = myjobs.MyJob()
job.progress += lambda pct: sys.stdout.write("%.1f%% done\n" % pct)
job.run()

Cela fonctionne exactement comme le code "observateur simple" ci-dessus, mais vous pouvez ajouter autant d'auditeurs que vous le souhaitez en utilisant +=. (Contrairement à C #, il n'y a pas de type de gestionnaire d'événement, vous n'avez pas besoin de new EventHandler(foo.bar) pour vous abonner à un événement et vous n'avez pas besoin de rechercher la valeur null avant de déclencher l'événement. Comme C #, les événements ne suppriment pas les exceptions.)

Comment choisir

Si logging fait tout ce dont vous avez besoin, utilisez-le. Sinon, faites la chose la plus simple qui fonctionne pour vous. L'essentiel à noter est qu'il n'est pas nécessaire d'assumer une dépendance externe importante.

12
Jason Orendorff

Que diriez-vous d'une implémentation où les objets ne sont pas maintenus en vie juste parce qu'ils observent quelque chose? Vous trouverez ci-dessous une implémentation du modèle d'observateur présentant les caractéristiques suivantes:

  1. L'utilisation est Pythonic. Pour ajouter un observateur à une méthode liée .bar d'instance foo, il suffit de faire foo.bar.addObserver(observer).
  2. Les observateurs ne sont pas maintenus en vie parce qu'ils sont des observateurs. En d'autres termes, le code de l'observateur n'utilise aucune référence forte.
  3. Aucune sous-classification nécessaire (descripteurs ftw).
  4. Peut être utilisé avec des types insaisissables.
  5. Peut être utilisé autant de fois que vous le souhaitez dans une seule classe.
  6. (bonus) A ce jour, le code existe dans un package package installable et téléchargeable sur github .

Voici le code (les packages github ou PyPI ont la plus récente mise en oeuvre):

import weakref
import functools

class ObservableMethod(object):
    """
    A proxy for a bound method which can be observed.

    I behave like a bound method, but other bound methods can subscribe to be
    called whenever I am called.
    """

    def __init__(self, obj, func):
        self.func = func
        functools.update_wrapper(self, func)
        self.objectWeakRef = weakref.ref(obj)
        self.callbacks = {}  #observing object ID -> weak ref, methodNames

    def addObserver(self, boundMethod):
        """
        Register a bound method to observe this ObservableMethod.

        The observing method will be called whenever this ObservableMethod is
        called, and with the same arguments and keyword arguments. If a
        boundMethod has already been registered to as a callback, trying to add
        it again does nothing. In other words, there is no way to sign up an
        observer to be called back multiple times.
        """
        obj = boundMethod.__self__
        ID = id(obj)
        if ID in self.callbacks:
            s = self.callbacks[ID][1]
        else:
            wr = weakref.ref(obj, Cleanup(ID, self.callbacks))
            s = set()
            self.callbacks[ID] = (wr, s)
        s.add(boundMethod.__name__)

    def discardObserver(self, boundMethod):
        """
        Un-register a bound method.
        """
        obj = boundMethod.__self__
        if id(obj) in self.callbacks:
            self.callbacks[id(obj)][1].discard(boundMethod.__name__)

    def __call__(self, *arg, **kw):
        """
        Invoke the method which I proxy, and all of it's callbacks.

        The callbacks are called with the same *args and **kw as the main
        method.
        """
        result = self.func(self.objectWeakRef(), *arg, **kw)
        for ID in self.callbacks:
            wr, methodNames = self.callbacks[ID]
            obj = wr()
            for methodName in methodNames:
                getattr(obj, methodName)(*arg, **kw)
        return result

    @property
    def __self__(self):
        """
        Get a strong reference to the object owning this ObservableMethod

        This is needed so that ObservableMethod instances can observe other
        ObservableMethod instances.
        """
        return self.objectWeakRef()


class ObservableMethodDescriptor(object):

    def __init__(self, func):
        """
        To each instance of the class using this descriptor, I associate an
        ObservableMethod.
        """
        self.instances = {}  # Instance id -> (weak ref, Observablemethod)
        self._func = func

    def __get__(self, inst, cls):
        if inst is None:
            return self
        ID = id(inst)
        if ID in self.instances:
            wr, om = self.instances[ID]
            if not wr():
                msg = "Object id %d should have been cleaned up"%(ID,)
                raise RuntimeError(msg)
        else:
            wr = weakref.ref(inst, Cleanup(ID, self.instances))
            om = ObservableMethod(inst, self._func)
            self.instances[ID] = (wr, om)
        return om

    def __set__(self, inst, val):
        raise RuntimeError("Assigning to ObservableMethod not supported")


def event(func):
    return ObservableMethodDescriptor(func)


class Cleanup(object):
    """
    I manage remove elements from a dict whenever I'm called.

    Use me as a weakref.ref callback to remove an object's id from a dict
    when that object is garbage collected.
    """
    def __init__(self, key, d):
        self.key = key
        self.d = d

    def __call__(self, wr):
        del self.d[self.key]

Pour utiliser cela, nous décorons simplement les méthodes que nous voulons rendre observables avec @event. Voici un exemple

class Foo(object):
    def __init__(self, name):
        self.name = name

    @event
    def bar(self):
        print("%s called bar"%(self.name,))

    def baz(self):
        print("%s called baz"%(self.name,))

a = Foo('a')
b = Foo('b')
a.bar.addObserver(b.bar)
a.bar()
6
DanielSank

De wikipedia :

from collections import defaultdict

class Observable (defaultdict):

  def __init__ (self):
      defaultdict.__init__(self, object)

  def emit (self, *args):
      '''Pass parameters to all observers and update states.'''
      for subscriber in self:
          response = subscriber(*args)
          self[subscriber] = response

  def subscribe (self, subscriber):
      '''Add a new subscriber to self.'''
      self[subscriber]

  def stat (self):
      '''Return a Tuple containing the state of each observer.'''
      return Tuple(self.values())

L'Observable s'utilise comme ça.

myObservable = Observable ()

# subscribe some inlined functions.
# myObservable[lambda x, y: x * y] would also work here.
myObservable.subscribe(lambda x, y: x * y)
myObservable.subscribe(lambda x, y: float(x) / y)
myObservable.subscribe(lambda x, y: x + y)
myObservable.subscribe(lambda x, y: x - y)

# emit parameters to each observer
myObservable.emit(6, 2)

# get updated values
myObservable.stat()         # returns: (8, 3.0, 4, 12)
4
Ewan Todd

Sur la base de la réponse de Jason, j'ai implémenté l'exemple d'événements de type C # en tant que module à part entière de python, comprenant de la documentation et des tests. J'aime les trucs Pythonic fantaisie :)

Donc, si vous voulez une solution prête à l’emploi, vous pouvez simplement utiliser le code de github .

3
aepsil0n

Exemple: Observateurs de journaux torsadés

Pour inscrire un observateur yourCallable() (un appelable qui accepte un dictionnaire) pour recevoir tous les événements de journal (en plus des autres observateurs):

twisted.python.log.addObserver(yourCallable)

Exemple: exemple complet producteur/consommateur

De la liste de diffusion Twisted-Python:

#!/usr/bin/env python
"""Serve as a sample implementation of a twisted producer/consumer
system, with a simple TCP server which asks the user how many random
integers they want, and it sends the result set back to the user, one
result per line."""

import random

from zope.interface import implements
from twisted.internet import interfaces, reactor
from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiver

class Producer:
    """Send back the requested number of random integers to the client."""
    implements(interfaces.IPushProducer)
    def __init__(self, proto, cnt):
        self._proto = proto
        self._goal = cnt
        self._produced = 0
        self._paused = False
    def pauseProducing(self):
        """When we've produced data too fast, pauseProducing() will be
called (reentrantly from within resumeProducing's transport.write
method, most likely), so set a flag that causes production to pause
temporarily."""
        self._paused = True
        print('pausing connection from %s' % (self._proto.transport.getPeer()))
    def resumeProducing(self):
        self._paused = False
        while not self._paused and self._produced < self._goal:
            next_int = random.randint(0, 10000)
            self._proto.transport.write('%d\r\n' % (next_int))
            self._produced += 1
        if self._produced == self._goal:
            self._proto.transport.unregisterProducer()
            self._proto.transport.loseConnection()
    def stopProducing(self):
        pass

class ServeRandom(LineReceiver):
    """Serve up random data."""
    def connectionMade(self):
        print('connection made from %s' % (self.transport.getPeer()))
        self.transport.write('how many random integers do you want?\r\n')
    def lineReceived(self, line):
        cnt = int(line.strip())
        producer = Producer(self, cnt)
        self.transport.registerProducer(producer, True)
        producer.resumeProducing()
    def connectionLost(self, reason):
        print('connection lost from %s' % (self.transport.getPeer()))
factory = Factory()
factory.protocol = ServeRandom
reactor.listenTCP(1234, factory)
print('listening on 1234...')
reactor.run()
2
jfs

Une approche fonctionnelle de la conception d'observateur:

def add_listener(obj, method_name, listener):

    # Get any existing listeners
    listener_attr = method_name + '_listeners'
    listeners = getattr(obj, listener_attr, None)

    # If this is the first listener, then set up the method wrapper
    if not listeners:

        listeners = [listener]
        setattr(obj, listener_attr, listeners)

        # Get the object's method
        method = getattr(obj, method_name)

        @wraps(method)
        def method_wrapper(*args, **kwags):
            method(*args, **kwags)
            for l in listeners:
                l(obj, *args, **kwags) # Listener also has object argument

        # Replace the original method with the wrapper
        setattr(obj, method_name, method_wrapper)

    else:
        # Event is already set up, so just add another listener
        listeners.append(listener)


def remove_listener(obj, method_name, listener):

    # Get any existing listeners
    listener_attr = method_name + '_listeners'
    listeners = getattr(obj, listener_attr, None)

    if listeners:
        # Remove the listener
        next((listeners.pop(i)
              for i, l in enumerate(listeners)
              if l == listener),
             None)

        # If this was the last listener, then remove the method wrapper
        if not listeners:
            method = getattr(obj, method_name)
            delattr(obj, listener_attr)
            setattr(obj, method_name, method.__wrapped__)

Ces méthodes peuvent ensuite être utilisées pour ajouter un écouteur à n'importe quelle méthode de classe. Par exemple:

class MyClass(object):

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

    def some_method(self, num, string):
        print('method:', num, string)

def listener_method(obj, num, string):
    print('listener:', num, string, obj.prop)

my = MyClass('my_prop')

add_listener(my, 'some_method', listener_method)
my.some_method(42, 'with listener')

remove_listener(my, 'some_method', listener_method)
my.some_method(42, 'without listener')

Et le résultat est:

method: 42 with listener
listener: 42 with listener my_prop
method: 42 without listener
1
Dane White

OP demande "Existe-t-il des exemples de GoF Observer mis en œuvre en Python?" .__ Ceci est un exemple dans Python 3.7. Cette classe Observable répond à l'exigence de créer une relation entre un observable et plusieurs observateurs tout en restant indépendant de leur structure.

from functools import partial
from dataclasses import dataclass, field
import sys
from typing import List, Callable


@dataclass
class Observable:
    observers: List[Callable] = field(default_factory=list)

    def register(self, observer: Callable):
        self.observers.append(observer)

    def deregister(self, observer: Callable):
        self.observers.remove(observer)

    def notify(self, *args, **kwargs):
        for observer in self.observers:
            observer(*args, **kwargs)


def usage_demo():
    observable = Observable()

    # Register two anonymous observers using lambda.
    observable.register(
        lambda *args, **kwargs: print(f'Observer 1 called with args={args}, kwargs={kwargs}'))
    observable.register(
        lambda *args, **kwargs: print(f'Observer 2 called with args={args}, kwargs={kwargs}'))

    # Create an observer function, register it, then deregister it.
    def callable_3():
        print('Observer 3 NOT called.')

    observable.register(callable_3)
    observable.deregister(callable_3)

    # Create a general purpose observer function and register four observers.
    def callable_x(*args, **kwargs):
        print(f'{args[0]} observer called with args={args}, kwargs={kwargs}')

    for gui_field in ['Form field 4', 'Form field 5', 'Form field 6', 'Form field 7']:
        observable.register(partial(callable_x, gui_field))

    observable.notify('test')


if __== '__main__':
    sys.exit(usage_demo())
0
lemi57ssss