Welcome, guest | Sign In | My Account | Store | Cart

Replacement for lambda that uses language features new to Python 2.4. One implementation has a neat (ab)use of sys._getframe(); the other is portable.

Python, 47 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Implementation 1: not portable

import sys

def fn(gen):
    """Turns a generator expression into a callable."""
    def anonymous(*args):
        return gen.next()
    return anonymous

def args():
    """Works with fn(); yields args passed to anonymous()."""
    while True:
        # Stack frame will look like:
        #   anonymous(args=(...))      "foo(3,4,5)"
        #   gen.next()                 "return gen.next()"
        #   args.next()                internal genexp code
        yield sys._getframe(2).f_locals['args']
args = args()

foo = fn(a + b * c for (a,b,c) in args)
assert foo(3,4,5) == 3+4*5
assert foo(4,5,6) == 4+5*6

# Implementation 2: not thread-safe (but it could be)

class SingleElementIterator:
    """Iterator that must be constantly fed by assigning to .value"""
    def __iter__(self):
        return self
    def next(self):
        value = self.value
        del self.value
        return value
args2 = SingleElementIterator()

def fn2(gen):
    """Turns a generator expression into a callable."""
    def anonymous(*args_):
        args2.value = args_
        return gen.next()

    return anonymous

foo = fn2(a + b * c for (a,b,c) in args2)
assert foo(3,4,5) == 3+4*5
assert foo(4,5,6) == 4+5*6

Guido has expressed the desire to remove "lambda" for Python 3000. There was a discussion on comp.lang.python about what new language feature (if any) should replace lambda. Some of the proposed syntaxes led me to try a pure-python implementation.

The basic idea is to use a generator expression to implement an "expression object". The function "fn" (the name is taken from the Arc language) wraps the generator in a callable and returns it. The wrapper is responsible for feeding its arguments to the genexp; this is done by cooperating with the genexp's input iterator.

In the first (CPython-specific) implementation, the cooperation is simple: the wrapper just needs to name its parameter "args". The iterator reaches up the stack to extract the local variable.

In the second implementation, the wrapper stores its args in what amounts to a global variable, and the input iterator "args2" almost immediately consumes it. There is no way for an anonymous function to execute between the time args.value is set and the time it's consumed; thus the use of global state is safe.

However, the fact that the implementation is stateful at all makes it unsafe in the presence of threads. To make it safe, one would have to make SingleElementIterator put "value" in thread-local storage. This is left as an exercise for the reader :-).

This is never explicitly an issue for these implementations, but it's worth keeping in mind that a genexp's outermost iterator is evaluated immediately.

References: * Pep 3000 http://python.org/peps/pep-3000.html * comp.lang.python thread http://groups-beta.google.com/group/comp.lang.python/msg/41713ae1c0d7385a

3 comments

Steven Bethard 19 years, 2 months ago  # | flag

using namespace for args. This doesn't try to solve the threading problem, but I think I prefer a solution that keeps 'args' in the 'fn' namespace, like:

class fn(object):
    class args(object):
        def __iter__(self):
            return self
        def next(self):
            args = self.args
            del self.args
            return args
    args = args()
    def __new__(self, gen):
        def anonymous(*args):
            fn.args.args = args
            return gen.next()
        return anonymous

which would then be used like:

>>> foo = fn(a + b * c for a, b, c in fn.args)
>>> foo(3, 4, 5)
23
>>> foo(4, 5, 6)
34

That way 'args', which is intimately joined with 'fn', appears as such namespace-wise.

Steven Bethard 19 years, 2 months ago  # | flag

Perhaps a little prettier:

class fn(object):
    class args(object):
        def __iter__(self):
            return self
        def next(self):
            args = self.args
            del self.args
            return args
    args = args()
    def __init__(self, gen):
        self._gen = gen
    def __call__(self, *args):
        fn.args.args = args
        return self._gen.next()

Also has the benefit of naming "functions" generated by fn as "fn":

>>> foo = fn(a + b * c for a, b, c in fn.args)
>>> foo
<__main__.fn object at 0x01150090>
Paul Du Bois (author) 19 years, 2 months ago  # | flag

Good points. I originally used a class for the "portable" version but got stuck because I didn't remember __new__. I'll update the recipe when I get a few tuits; thanks!

Created by Paul Du Bois on Fri, 31 Dec 2004 (PSF)
Python recipes (4591)
Paul Du Bois's recipes (1)

Required Modules

Other Information and Tasks