What's Going On?

castironpi at gmail.com castironpi at gmail.com
Thu Mar 13 12:05:39 EDT 2008


On Mar 13, 6:12 am, "Diez B. Roggisch" <de... at nospam.web.de> wrote:
> MartinRineh... at gmail.com wrote:
> > (Accompanied by Marvin Gaye)
>
> >>>> def f(list=[0]):
> > ...    list[0]+=1
> > ...    return list[0]
> > ...
> >>>> f()
> > 1
> >>>> f()
> > 2
> >>>> f() # 'list' is a name bound to a list (mutable) so this makes sense
> > 3
> >>>> f([5])
> > 6
> >>>>f() # What's Going On?
> > 4
>
> That the same default argument is mutated? What did you expect, that it got
> replaced by you passing another list? That would kind of defy the meaning
> of default-arguments, replacing them whenever you call a function with
> actual parameters.

f( ite, itr= ite.__iter__ ).

Case 1:  'ite' is bound in outer scope.
Case 2:  not.

function(code, globals[, name[, argdefs[, closure]]])
The optional argdefs tuple specifies the default argument values.

TypeError: __defaults__ must be set to a tuple object

>>> import functools
>>> def k():
...     print( 'k call' )
...
>>> class GT:
...     def __init__( self, arg ):
...             self._arg= arg
...
>>> def latebound( fun ):
...     @functools.wraps( fun )
...     def post( *ar ):
...             lar= list( fun.__defaults__ )
...             for i, a in enumerate( lar ):
...                     if isinstance( lar[ i ], GT ):
...                             lar[ i ]= eval( a._arg)
...             return fun( *( list( ar )+ lar ) )
...     return post
...
>>> @latebound
... def f( ite, itr= GT('k()') ):
...     return itr
...
>>> f( 2 )
k call
>>> f( 2 )
k call
>>> f( 2 )
k call
>>>

Furthermore,

...
>>> @latebound
... def f( ite, itr= GT('ar[0]+ 1') ):
...     return itr
...
>>> f( 2 )
3
>>> f( 2 )
3
>>> f( 2 )
3
>>>

Unfortunately,

...
>>> @latebound
... def f( ite, itr= GT('ite+ 1') ):
...     return itr
...
>>> f( 2 )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 7, in post
  File "<string>", line 1, in <module>
NameError: name 'ite' is not defined
>>>

The moral of the story is, wouldn't it be nice if wrappers could
access the post-invocation bindings of the wrappee's internal
variables?  Certainly the 'TypeError: __defaults__ must be set to a
tuple object' is unnecessarily restrictive: let it be a mutable, or,
if it's not a tuple, call it.  Just add a __call__ method to that
tuple that does nothing, if a type-test isn't faster-- perhaps even a
null __call__ which does -actually- nothing.  (Why is __defaults__
None when empty, not an empty tuple?)

What are the use case proportions of function-static vs. call-time
evaluation?  Function-static is nice in that it keeps objects floating
around with the function, both in information design, and in
encapsulation.  They both have easy workarounds:

>>> import functools
>>> def auto( fun ):
...     @functools.wraps( fun )
...     def post( *ar ):
...             return fun( post, *ar )
...     return post
...
>>> @auto
... def f( self ):
...     print( self.ite )
...
>>> f.ite= []
>>>
>>> f()
[]
>>> f.ite.append( 2 )
>>> f()
[2]

@auto adds the binding function to its called parameter list, which is
nice if the function will change names ever-- that is, become bound to
other variables-- because its statics still stay with it, and because
it's not a global either.

>>> g= f
>>> del f
>>> g()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in post
  File "<stdin>", line 3, in f
NameError: global name 'f' is not defined

But on the other hand, if the Python community on the whole wants to
keep the status quo,

>>> def f( ite, itr= None ):
...     if None is itr:
...             itr= ite+ 1
...     return itr
...
>>> f( 2 )
3
>>> f( 2 )
3
>>> f( 2 )
3

isn't so bad.  (If you think it is, lobby!)

@latebound is a good compromise.  It keeps the information in the
right place, but takes a little redundancy if the evalled expression
refers to another variable, and but costs the additional GT class, and
adds an O( n ) argument-length boilerplate rearrangement.  @auto frees
us to allow a different behavior to default parameters while keeping
both statics and call-times explicit, keeping the information in the
right place, but it's a change in the language.  Lastly, we have:

>>> @late( itr= 'ite+ 1' )
... def f( ite, itr= latearg, j= 0 ):
...     return itr, j
...
>>> print( f( 2 ) )
(3, 0)

Proof of concept:

import functools

latearg= object()
def late( **kw ):
    def pre( fun ):
        _defs= fun.__defaults__
        if None is _defs: _defs= ()
        _names= fun.__code__.co_varnames
        _deflen= len( _defs )
        _namelen= fun.__code__.co_argcount
        for k in kw:
            if k not in _names:
                raise TypeError( 'Non-parameter'
                    ' keyword \'%s\' in \'late\''
                    ' call.'% k )
        print( _defs )
        print( _names )
        print( _deflen )
        for a, b in zip( _names[ -_deflen: ], _defs ):
            if b is latearg and a not in kw:
                raise TypeError( 'Non-bound'
                    ' latearg \'%s\' in \'late\''
                    ' call.'% k )
        @functools.wraps( fun )
        def post( *ar ):
            _arglen= len( ar )
            _defleft= _namelen- _arglen
            _defused= ()
            if _defleft:
                _defused= _defs[ -_defleft: ]
            _lar= list( ar+ _defused )
            _funargs= {}
            for i, a in enumerate( ar ):
                _funargs[ _names[ i ] ]= a
            for k, v in kw.items():
                if k not in _names:
                    raise TypeError( 'Not all latearg'
                        ' arguments bound in call'
                        ' of \'%s\''% fun.__name__ )
                _place= _names.index( k )
                if _place>= _arglen:
                    _lar[ _place ]= eval(
                        v, globals(), _funargs )
            if latearg in _lar:
                raise TypeError( 'Not all latearg'
                    ' arguments bound in call'
                    ' of \'%s\''% fun.__name__ )
            return fun( *_lar )
        return post
    return pre

@late( itr= 'ite+ 1' )
def f( ite, itr= latearg, j= 0 ):
    return itr, j

assert f( 2 )== ( 3, 0 )
assert f( 2, 0 )== ( 0, 0 )
assert f( 2, 0, 1 )== ( 0, 1 )
assert f( 2, 1 )== ( 1, 0 )

To complete 'post' (**kw not shown) is left as an exercise to the
reader.



More information about the Python-list mailing list