[Python-ideas] Proposal: A Reduce-Map Comprehension and a "last" builtin

Steven D'Aprano steve at pearwood.info
Thu Apr 5 21:18:54 EDT 2018


On Thu, Apr 05, 2018 at 12:52:17PM -0400, Peter O'Connor wrote:

> I propose a new "Reduce-Map" comprehension that allows us to write:
> 
> signal = [math.sin(i*0.01) + random.normalvariate(0, 0.1) for i in range(1000)]
> smooth_signal = [average = (1-decay)*average + decay*x for x in signal
> from average=0.]

I've already commented on this proposed syntax. A few further comments 
below.


> Instead of:
> 
> def exponential_moving_average(signal: Iterable[float], decay: float,
> initial_value: float=0.):
>     average = initial_value
>     for xt in signal:
>         average = (1-decay)*average + decay*xt
>         yield average

What I like about this is that it is testable in isolation and re- 
usable. It can be documented, the implementation changed if needed 
without having to touch all the callers of that function, and the name 
is descriptive.

(I don't understand why so many people have such an aversion to writing 
functions and seek to eliminate them from their code.)

Here's another solution which I like, one based on what we used to call 
coroutines until that term was taken for async functions. So keeping in 
mind that this version of "coroutine" has nothing to do with async:


import functools

def coroutine(func):
    """Decorator to prime coroutines when they are initialised."""
    @functools.wraps(func)
    def started(*args, **kwargs):
        cr = func(*args,**kwargs)
        cr.send(None)
        return cr
    return started

@coroutine
def exponential_moving_average(decay=0.5):
    """Exponentially weighted moving average (EWMA).

    Coroutine returning a moving average with exponentially
    decreasing weights. By default the decay factor is one half,
    which is equivalent to averaging each value (after the first)
    with the previous moving average:

    >>> aver = exponential_moving_average()
    >>> [aver.send(x) for x in [5, 1, 2, 4.5]]
    [5, 3.0, 2.5, 3.5]

    """
    average = (yield None)
    x = (yield average)
    while True:
        average = decay*x + (1-decay)*average
        x = (yield average)

I wish this sort of coroutine were better known and loved. You 
can run more than one of them at once, you can feed values into 
them lazily, they can be paused and put aside to come back 
to them later, and if you want to use them eagerly, you can just drop 
them into a list comprehension.



-- 
Steve


More information about the Python-ideas mailing list