[Python-ideas] several different needs [Explicit variable capture list]

Steven D'Aprano steve at pearwood.info
Wed Jan 27 07:41:03 EST 2016


On Tue, Jan 26, 2016 at 12:59:07PM -0800, Andrew Barnert via Python-ideas wrote:
> On Jan 26, 2016, at 11:40, Jim J. Jewett <jimjjewett at gmail.com> wrote:
> > 
> > (1)  Auxiliary variables
> > 
> >    def f(x, _len=len): ...
> > 
> > This is often a micro-optimization;
> 
> When _isn't_ it a micro-optimization? I think if it isn't, it's a very different case, e.g.:
> 
>     def len(iterable, _len=len):
>         if something(iterable): special_case()
>         else: return _len(iterable)

I'm not sure why you call this "a very different case". It looks the 
same to me: both cases use the default argument trick to capture the 
value of a builtin name. The reasons why they do so are incidental.

I sometimes have code like this:

try:
    enumerate("", 1)
except TypeError:
    # Too old a version of Python.
    def enumerate(it, start=0, enumerate=enumerate):
        for a, b in enumerate(it):
            yield (a+start, b)


I don't really want an extra argument, but nor do I want a global:

_enumerate = enumerate
def enumerate(it, start=0):
    for a, b in _enumerate(it):
        yield (a+start, b)


This isn't a matter of micro-optimization, it's a matter of 
encapsulation. That my enumerate calls the built-in enumerate is an 
implementation detail, and what I'd like is to capture the value without 
either a global or an extra argument:

# capture the current builtin
def enumerate(it, start=0)(enumerate):
    for a, b in enumerate(it):
        yield (a+start, b)


Obviously I'm not going to be able to use hypothetical Python 3.6 syntax 
in code that needs to run in 2.5. But I might be able to use that syntax 
in Python 3.8 for code that needs to run in 3.6.


> > (2)  immutable bindings
> > 
> > once X
> > final Y
> > const Z
> 
> But a default value neither guarantees immutability, nor signals such 
> an intent. Parameters can be rebound or mutated just like any other 
> variables.

I don't think this proposal has anything to say about about either 
immutability or bind-once-only "constants".


> > (3)  Persistent storage
> > 
> >    def f(x, _cached_results={}): ...
> 
> > I still think it might be nice to just have a way of easily opening a
> > new scope ...
> 
> You mean to open a new scope _outside_ the function definition, so it 
> can capture the cache in a closure, without leaving it accessible from 
> outside the scope? But then f won't be accessible either, unless you 
> have some way to "return" the value to the parent scope. And a scope 
> that returns something--that's just a function, isn't it?

I'm not sure what point you think you are making here, or what Jim 
meant by his comment about the new scope, but in this case I don't 
think we would want an extra scope. We would want the cache to be in the 
function's local scope, but assigned once at function definition time.

When my caching needs are simple, I might write something like this:

def func(x, cache={}): ...

which is certainly better than having a global variable cache. For many 
applications (quick and dirty scripts) this is perfectly adequate.

For other applications were my caching needs are more sophisticated, I 
might invest the effort in writing a decorator (or use functools.lru_cache),
or a factory to hide the cache in a closure:

def factory():
    cache = {}
    def func(x):
        ...
    return func

func = factory()
del factory

but there's a middle ground where I want something less quick'n'dirty 
than the first, but not going to all the effort of the second. For that, 
I think that being able to capture a value fits the use-case perfectly:

def func(x)(cache={}): ...



> Meanwhile, a C-style function-static variable isn't really the same 
> thing. Statics are just globals with names nobody else can see. So, 
> for a nested function (or a method) that had a "static cache", any 
> copies of the function would all share the same cache, 

Copying functions is, I think, a pretty rare and advanced thing to do. 
At least up to 3.4, copy.copy(func) simply returns func, so if you want 
to make an actual distinct copy, you probably need to build a new 
function by hand. In which case, you could copy the cache as part of the 
process.


-- 
Steve


More information about the Python-ideas mailing list