Coroutines and argument tupling

Marshall T. Vandegrift llasram at gmail.com
Thu Aug 16 07:37:07 EDT 2007


Bjoern Schliessmann <usenet-mail-0306.20.chr0n0ss at spamgourmet.com> writes:

> The solution I'd use is a decorator that calls next automatically one
> time after instantiation. Then you can use send normally, and don't
> have to care about any initial parameters, which makes the code
> clearer (initial parameters should be used for setup purposes, but not
> for the first iteration, IMHO). It'd look like this (from PEP 342,
> http://www.python.org/dev/peps/pep-0342/):

I'd seen the consumer decorator, and it certainly is cleaner than just
using a generator.  I don't like how it hides the parameter signature in
the middle of the consumer function though, and it also doesn't provide
for argument default values.  It's the difference between:

    ...
    def __init__(self, ...):
        ...
        self.consumer = self._function(value)
        ...

    def function(self, first, second=3, *args, **kwargs):
        self.consumer.send((first, second, args, kwargs))

    @consumer
    def _function(self, setup):
        ...
        first, second, args, kwargs = yield # initial 'next'
        while condition:
            ...
            first, second, args, kwargs = yield retval

Versus just:

    @coroutine
    def function(self, first, second=3, *args, **kwargs):
        ...
        while condition:
            ...
            first, second, args, kwargs = yield retval

Thanks in any case for the replies!  Since I've apparently decided my
ArgPacker is worth it, the complete code for my coroutine decorator
follows.

-Marshall


import inspect
import types
import functools
from itertools import izip

__all__ = [ 'coroutine' ]

class ArgPacker(object):
    def __init__(self, function):
        args, varargs, varkw, defaults = inspect.getargspec(function)
        self.args = args or []
        self.varargs = (varargs is not None) and 1 or 0
        self.varkw = (varkw is not None) and 1 or 0
        self.nargs = len(self.args) + self.varargs + self.varkw
        defaults = defaults or []
        defargs = self.args[len(self.args) - len(defaults):]
        self.defaults = dict([(k, v) for k, v in izip(defargs, defaults)])

    def pack(self, *args, **kwargs):
        args = list(args)
        result = [None] * self.nargs
        for i, arg in izip(xrange(len(self.args)), self.args):
            if args:
                result[i] = args.pop(0)
            elif arg in kwargs:
                result[i] = kwargs[arg]
                del kwargs[arg]
            elif arg in self.defaults:
                result[i] = self.defaults[arg]
            else:
                return None
        if self.varargs:
            result[len(self.args)] = args
        elif args:
            return None
        if self.varkw:
            result[-1] = kwargs
        elif kwargs:
            return None
        return tuple(result)

class coroutine(object):
    """Convert a function to be a simple coroutine.
    
    A simple coroutine is a generator bound to act as much as possible like a
    normal function.  Callers call the function as usual while the coroutine
    produces new return values and receives new arguments with `yield'.
    """
    def __init__(self, function):
        self.function = function
        self.gname = ''.join(['__', function.__name__, '_generator'])
        self.packer = ArgPacker(function)
        coroutine = self
        def method(self, *args, **kwargs):
            return coroutine.generate(self, self, *args, **kwargs)
        self.method = method
        functools.update_wrapper(self, function)
        functools.update_wrapper(method, function)

    def __get__(self, obj, objtype=None):
        return types.MethodType(self.method, obj, objtype)
        
    def __call__(self, *args, **kwargs):
        return self.generate(self, *args, **kwargs)

    def generate(self, obj, *args, **kwargs):    
        try:
            generator = getattr(obj, self.gname)
        except AttributeError:
            generator = self.function(*args, **kwargs)
            setattr(obj, self.gname, generator)
            retval = generator.next()
        else:
            packed = self.packer.pack(*args, **kwargs)
            if packed is None:
                self.function(*args, **kwargs) # Should raise TypeError
                raise RuntimeError("ArgPacker reported spurious error")
            retval = generator.send(packed)
        return retval




More information about the Python-list mailing list