Ruby and Python

Alex Martelli aleaxit at yahoo.com
Tue Nov 21 05:28:23 EST 2000


"Suchandra Thapa" <ssthapa at harper.uchicago.edu> wrote in message
news:slrn91htg2.1mn.ssthapa at localhost.localdomain...
    [snip]
>     The only major quibble you can really have about python's handling
> of functions is that runtime creation of objects are limited by
> the restrictions on the lambda function.  However, you can get
> around this by chaining a bunch of lambdas together.

The "new.function" function (in standard module 'new') lets you
"runtime-create" function objects in Python (you supply to it a
code-object, which you can get/create in various ways, as well
as a global-namespace dict, and, optionally, a name-string and
a tuple of default values for arguments).  Also, 'def' is an
executable statement: each time it's executed, it returns a NEW
function-object, too (much like a new.function, but with the
code-object being the body of the 'def', the name being the
identifier right after the 'def' keyword, and the global namespace
being the one currently in use).  lambda, in other words, is just
one of several ways that Python offers to create function objects
at runtime; its limitations (that the code-object be just an
expression, not a statement-suite) only affect that specific
syntax, not other ones.

For example, say we want to build, at runtime, a list of
functions, such that the i-th function in the list will
take one argument x (a string) and return a list composed
of '' (0 repetitions of x), x, x+x, ..., up to x*(i-1).

We can do it with lambda and list comprehensions, of
course:

>>> repeater = []
>>> for i in range(6):
...   repeater.append(lambda x,i=i: [x*j for j in range(i)])
...
>>> for x in repeater: x('bo')
...
[]
['']
['', 'bo']
['', 'bo', 'bobo']
['', 'bo', 'bobo', 'bobobo']
['', 'bo', 'bobo', 'bobobo', 'bobobobo']
>>>

But suppose we didn't have list comprehensions, so that
the bodies of the functions we want to generate and put
in the list would really need to be statement suites
(e.g., to have loops in them).  Because def is an
executable statement and returns a new function object
each time it's executed, it's just as easy, if more
verbose:

>>> rop=[]
>>> for i in range(6):
...     def f(x,i=i):
...         result = []
...         for j in range(i):
...             result.append(x*j)
...         return result
...     rop.append(f)
...
>>> for x in rop: x('bo')
...
[]
['']
['', 'bo']
['', 'bo', 'bobo']
['', 'bo', 'bobo', 'bobobo']
['', 'bo', 'bobo', 'bobobo', 'bobobobo']
>>>

"See, ma, no lambda!":-).  def works just as well as
lambda and without its limitation -- it does have its
own, namely that you have to associate a *name* to
the function you're defining, but that's no biggie.

Just for fun, here's a mixed-approach based on using
new.function (I've kept the functionbody concise by
using a list comprehension again, but it makes no
difference):

>>> def repeater(n):
...     def f(x):
...         return [x*j for j in range(i)]
...     rep=[]
...     for i in range(n):
...         glo={'range':range, 'i':i}
...         rep.append(new.function(f.func_code,glo))
...     return rep
...
>>> rap=repeater(6)
>>> for x in rap: x('bo')
...
[]
['']
['', 'bo']
['', 'bo', 'bobo']
['', 'bo', 'bobo', 'bobobo']
['', 'bo', 'bobo', 'bobobo', 'bobobobo']
>>>

This has one big difference -- here, we pass an explicitly
built *global* namespace, rather than 'priming' the *local*
namespace with the argument-with-default idiom.  Which is
why we need to bind 'range' in that namespace -- to avoid
a NameError on it!

But, isn't this *terrible*, to have to manually build a
namespace this way...?

Pah!  Who ever said we have to do it manually? *Explicitly*,
yes, but if we like this idiom so much that we want to
automate the process, hey, Python *is* a pretty powerful
language, you know, and it *does* have first-class functions
and code-objects and decent reflection abilities, so,
for example, we can code:

def func_env(func, vars):
    code = func.func_code
    glo = {}
    for name in code.co_names:
        if not name in code.co_varnames:
            try: glo[name] = eval(name, vars)
            except NameError: pass
    return new.function(code, glo)

This extracts the code-object wrapped in a function-object;
loops over all the names used in this code-object, but
skips those that have been identified as local variable
names, only treating the 'global' names; tries to prime
the 'glo' dictionary with the current binding of each such
name in a supplied 'vars' namespace (dictionary), ignoring
errors from unbound names; finally, wraps up the codeobject
and the new dictionary (namespace) into a new function
object, which it returns as its results.

In other words, it roughly emulates, explicitly, the implicit
binding-process (environment-construction) that appears to be
the idiom so keenly desired here -- as long as 'vars' is
passed in as the desired 'enclosing namespace', of course.

That is easy to arrange, e.g. by recoding 'repeater' as:

def repeater(n):
    def f(x):
        return [x*j for j in range(i)]
    rep=[]
    for i in range(n):
        rep.append(func_env(f,vars()))
    return rep

since 'vars()' returns exactly the 'current composite
namespace' (at least as far as _reading_ from it is
concerned...).


There are still several open issues with this approach --
what happens if the codeobject being wrapped plays highly
dynamic tricks, or just tries to _re-bind_ globals.  But,
for "well-behaved" codeobjects, wrapping them like this
is quite OK -- if you're really keen about such idioms.
(For much-wider applicability, you might have to get in
at a lower-level, tweaking/hacking the bytecode).

"Explicit is better than implicit" is a Python mantra --
here, you get to code the wrapping-function func_env, which
takes a function object and an environment as its args,
and returns a new function object using that environment,
so you can tweak its semantics to taste...:-).


Alex






More information about the Python-list mailing list