(Accompanied by Marvin Gaye)
... list[0]+=1
... return list[0]
...
f() 1
2
f() # 'list' is a name bound to a list (mutable) so this makes sense 3
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
.... print( 'k call' )
........ def __init__( self, arg ):
.... self._arg= arg
........ @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
........ def f( ite, itr= GT('k()') ):
.... return itr
....
.... def f( ite, itr= GT('ar[0]+ 1') ):
.... return itr
....
.... def f( ite, itr= GT('ite+ 1') ):
.... return itr
....Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in post
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:
.... @functools.wraps( fun )
.... def post( *ar ):
.... return fun( post, *ar )
.... return post
........ 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.
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,
.... if None is itr:
.... itr= ite+ 1
.... return itr
....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:
.... def f( ite, itr= latearg, j= 0 ):
.... return itr, j
....(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.