Python观察者模式:示例,提示?

时间:2009-12-14 23:51:32

标签: python design-patterns listener observer-pattern

是否有在Python中实现的GoF Observer的示例性示例?我有一些代码,当前有一些调试代码通过密钥类(如果设置了魔法env,当前生成消息给stderr)。此外,该类还有一个接口,用于递增返回结果以及存储它们(在内存中)以进行后期处理。 (该类本身是一个作业管理器,用于通过ssh在远程机器上同时执行命令。)

目前该类的用法类似于:

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)

alernative使用模型是:

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

这一切都适用于当前的实用程序。但它确实缺乏灵活性。例如,我目前支持简短的输出格式或进度条作为增量结果,我也支持 post_process()函数的简短,完整和“合并消息”输出。

但是,我想支持多个结果/输出流(到终端的进度条,对日志文件的调试和警告,从成功作业到一个文件/目录的输出,错误消息和其他非成功的结果)工作到另一个,等等。)

这听起来像是一个需要Observer的情况......让我的类的实例接受来自其他对象的注册,并在发生时使用特定类型的事件回调它们。

我正在查看PyPubSub因为我在SO相关问题中看到了几个引用。我不确定我是否已经准备好将外部依赖项添加到我的实用程序中,但是我可以看到使用它们的接口作为我的模型的价值,如果这将使其他人更容易使用它。 (该项目既可以作为独立的命令行实用程序,也可以作为编写其他脚本/实用程序的类。)

简而言之,我知道如何做我想要的......但是有很多方法可以实现它。从长远来看,我想要了解最有可能为代码的其他用户工作的建议。

代码本身位于:classh

9 个答案:

答案 0 :(得分:47)

  

然而,它缺乏灵活性。

嗯......实际上,如果你想要的是异步API,这对我来说就是一个很好的设计。通常是。也许你只需要从stderr切换到Python的logging模块,它有一个自己的发布/订阅模型,Logger.addHandler()等等。

如果您确实想支持观察员,我的建议是保持简单。你真的只需要几行代码。

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)

您的Job类可以继承Observable。当感兴趣的事情发生时,请致电self.fire(type="progress", percent=50)等。

答案 1 :(得分:21)

我认为其他人的回答是过分的。您可以使用少于15行代码轻松地在Python中实现事件。

您可以简单地使用两个类:EventObserver。任何想要监听事件的类都需要继承Observer并设置为侦听(观察)特定事件。当实例化并触发Event时,监听该事件的所有观察者都将运行指定的回调函数。

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)

示例

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')

输出:

Room is ready.
Lenard has arrived!

答案 2 :(得分:12)

更多方法......

示例:日志记录模块

也许您只需要从stderr切换到Python的logging模块,该模块具有强大的发布/订阅模型。

开始生成日志记录很容易。

# 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")

在消费者方面,还有一些工作要做。不幸的是,配置记录器输出需要7个完整的代码行。 ;)

# 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()

另一方面,日志包中有大量内容。如果您需要将日志数据发送到一组旋转的文件,一个电子邮件地址和Windows事件日志,那么您将受到保护。

示例:最简单的观察者

但您根本不需要使用任何库。支持观察者的一种非常简单的方法是调用一个什么都不做的方法。

# 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()

有时候不是写一个lambda,你可以说job.on_progress = progressBar.update,这很好。

这很简单。一个缺点是它自然不支持多个订阅相同事件的监听器。

示例:类似C#的事件

通过一些支持代码,您可以在Python中获得类似C#的事件。这是代码:

# 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)

生产者使用装饰器声明事件:

# 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()

这与上面的“简单观察者”代码完全相同,但您可以使用+=添加任意数量的侦听器。 (与C#不同,没有事件处理程序类型,订阅事件时不需要new EventHandler(foo.bar),并且在触发事件之前不必检查null。就像C#一样,事件不会被压制异常。)

如何选择

如果logging完成了您需要的一切,请使用它。否则做最适合你的事情。需要注意的关键是你不需要承担很大的外部依赖。

答案 3 :(得分:6)

如果一个实现的对象不能仅因为它们正在观察某些东西而保持活着状态呢?下面是一个具有以下特征的观察者模式的实现:

  1. 用法是pythonic。要将观察者添加到实例.bar的绑定方法foo,只需执行foo.bar.addObserver(observer)
  2. 观察员不会因观察员而活着。换句话说,观察者代码不使用强引用。
  3. 不需要进行子分类(描述符ftw)。
  4. 可与不可用类型一起使用。
  5. 可以在单个班级中多次使用。
  6. (奖金)截至今天,代码存在于适当的可下载,可安装package on github
  7. 以下是代码(github packagePyPI package具有最新的实现):

    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]
    

    要使用此功能,我们只需使用@event装饰我们想要观察的方法。这是一个例子

    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()
    

答案 4 :(得分:4)

来自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())

Observable就像这样使用。

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)

答案 5 :(得分:3)

根据Jason的回答,我实现了类似C#的事件示例,作为一个包含文档和测试的完整的python模块。我喜欢花哨的pythonic东西:)

因此,如果您需要一些现成的解决方案,可以使用code on github

答案 6 :(得分:2)

示例:twisted log observers

注册观察者yourCallable()(接受字典的可调用对象)以接收所有日志事件(除了任何其他观察者):

twisted.python.log.addObserver(yourCallable)

示例:complete producer/consumer example

来自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()

答案 7 :(得分:1)

观察者设计的功能性方法:

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__)

然后可以使用这些方法向任何类方法添加侦听器。例如:

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')

输出是:

method: 42 with listener
listener: 42 with listener my_prop
method: 42 without listener

答案 8 :(得分:0)

OP询问“是否存在用Python实现的GoF Observer的示例性示例?” 这是Python 3.7中的一个示例。此Observable类满足在一个可观察许多观察者之间建立关系,同时保持其结构独立的要求。

<DataGrid x:Name="myColorGrid">
    <DataGrid.ItemContainerStyle>
        <Style TargetType="DataGridRow">
            <Setter Property="Background" Value="{Binding Brush}" />
        </Style>
    </DataGrid.ItemContainerStyle>
</DataGrid>