Default method arguments

Bengt Richter bokr at oz.net
Wed Nov 16 02:28:45 EST 2005


On 15 Nov 2005 11:02:38 -0800, "Martin Miller" <ggrp1.20.martineau at dfgh.net> wrote:

>Alex Martelli wrote, in part:
>> If it's crucial to you to have some default argument value evaluated at
>> time X, then, by Python's simple rules, you know that you must arrange
>> for the 'def' statement itself to execute at time X.  In this case, for
>> example, if being able to have self.data as the default argument value
>> is the crucial aspect of the program, you must ensure that the 'def'
>> runs AFTER self.data has the value you desire.
>>
>> For example:
>>
>> class A(object):
>>     def __init__(self, n):
>>         self.data = n
>>         def f(self, x = self.data)
>>              print x
>>         self.f = f
>>
>> This way, of course, each instance a of class A will have a SEPARATE
>> callable attribute a.f which is the function you desire; this is
>> inevitable, since functions store their default argument values as part
>> of their per-function data.  Since you want a.f and b.f to have
>> different default values for the argument (respectively a.data and
>> b.data), therefore a.f and b.f just cannot be the SAME function object
>> -- this is another way to look at your issue, in terms of what's stored
>> where rather than of what evaluates when, but of course it leads to
>> exactly the same conclusion.
>
>FWIT and ignoring the small typo on the inner def statement (the
>missing ':'), the example didn't work as I (and possibily others) might
>expect.  Namely it doesn't make function f() a bound method of
>instances of class A, so calls to it don't receive an automatic 'self''
>argument when called on instances of class A.
>
>This is fairly easy to remedy use the standard new module thusly:
>
>import new
>class A(object):
>    def __init__(self, n):
>        self.data = n
>        def f(self, x = self.data):
>            print x
>        self.f = new.instancemethod(f, self, A)
>
>This change underscores the fact that each instance of class A gets a
>different independent f() method.  Despite this nit, I believe I
>understand the points Alex makes about the subject (and would agree).
>
Or as Alex mentioned, a custom descriptor etc is possible, and can also
protect against replacing f by simple instance attribute assignment
like inst.f = something, or do some filtering to exclude non-function
assignments etc., e.g., (not sure what self.data is really
needed for, but we'll keep it):

BTW, note that self.data initially duplicates the default value,
but self.data per se is not used by the function (until the instance
method is replace by one that does, see further on)

 >>> class BindInstMethod(object):
 ...     def __init__(self, inst_fname):
 ...         self.inst_fname= inst_fname
 ...     def __get__(self, inst, cls=None):
 ...         if inst is None: return self
 ...         return inst.__dict__[self.inst_fname].__get__(inst, cls) # return bound instance method
 ...     def __set__(self, inst, val):
 ...         if not callable(val) or not hasattr(val, '__get__'): # screen out some impossible methods
 ...             raise AttributeError, '%s may not be replaced by  %r' % (self.inst_fname, val)
 ...         inst.__dict__[self.inst_fname] = val
 ...

The above class defines a custom descriptor that can be instatiated as a class
variable of a given name. When that name is thereafter accessed as an attribute
of an instance of the latter class (e.g. A below), the decriptor __get__ or __set__
methods will be called (the __set__ makes it a "data" descriptor, which intercepts
instance attribute assignment.

 >>> class A(object):
 ...     def __init__(self, n):
 ...         self.data = n
 ...         def f(self, x = self.data):
 ...              print x
 ...         self.__dict__['f'] = f  # set instance attr w/o triggering descriptor
 ...     f = BindInstMethod('f')
 ...

 >>> a = A(5)
 >>> a.f
 <bound method A.f of <__main__.A object at 0x02EF3B0C>>

Note that a.f is dynamically bound at the time of a.f access, not
retrieved as a prebound instance method.

 >>> a.f()
 5
 >>> a.f('not default 5')
 not default 5
 >>> a.data
 5
 >>> a.data = 'not original data 5'

Since the default is an independent duplicate of a.data
a call with no arg produces the original default:
 >>> a.f()
 5
 >>> a.data
 'not original data 5'
 >>> a.f('this arg overrides the default')
 this arg overrides the default

Try to change a.f
 >>> a.f = 'sabotage f'
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "<stdin>", line 10, in __set__
 AttributeError: f may not be replaced by  'sabotage f'

Now use a function, which should be accepted (note: a function, not an instance method)
 >>> a.f = lambda self: self.data*2
 >>> a.f
 <bound method A.<lambda> of <__main__.A object at 0x02EF3B0C>>
Plainly the method was dynamically bound

 >>> a.f()
 'not original data 5not original data 5'
That was self.data*2 per the lambda we just assigned to a.f
BTW, the assignment is not directly to the instance attribute.
It goes via the descriptor __set__ method.

 >>> a.data = 12
 >>> a.f()
 24
 >>> b = A('bee')
 >>> b.f
 <bound method A.f of <__main__.A object at 0x02EF3BAC>>
 >>> b.f()
 bee
 >>> b.f('not bee')
 not bee
 >>> b.data
 'bee'
 >>> b.data = 'no longer bee'
 >>> b.f()
 bee
 >>> b.data
 'no longer bee'
 >>> b.f = lambda self: ' -- '.join([self.data]*3)
 >>> b.data
 'no longer bee'
 >>> b.data = 'ha'
 >>> b.f()
 'ha -- ha -- ha'
 >>> b.f = lambda self, n='default of n':n
 >>> b.data
 'ha'
 >>> b.f(123)
 123
 >>> b.f()
 'default of n'
 >>> a.f()
 24

Now let's add another name that can be used on instances like f
 >>> A.g = BindInstMethod('g')
 >>> a.g = lambda self:'a.g'
 >>> a.f()
 24
 >>> a.g()
 'a.g'
 >>> a.g(123)
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
 TypeError: <lambda>() takes exactly 1 argument (2 given)
Aha, the bound method got self as a first arg, but we defined g without any args.

 >>> b.g
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "<stdin>", line 7, in __get__
 KeyError: 'g'
No instance method b.g defined yet (A.__init__ only defines f)

Make one with a default
 >>> b.g = lambda self, x='xdefault': x
 >>> b.g()
 'xdefault'
 >>> b.g('and arg')
 'and arg'
 >>> a.g()
 'a.g'
 >>>
 
If we bypass method assignment via the descriptor, we can sapotage it:

 >>> a.g = lambda self: 'this works'
 >>> a.g()
 'this works'
 >>> a.g = 'sabotage'
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "<stdin>", line 10, in __set__
 AttributeError: g may not be replaced by  'sabotage'
That was rejected

but,
 >>> a.__dict__['g'] = 'sabotage' # this will bypass the descriptor
 >>> a.g
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
   File "<stdin>", line 7, in __get__
 AttributeError: 'str' object has no attribute '__get__'
The descriptor couldn't form a bound method since 'sabotage' was not
a function or otherwise suitable.

But we can look at the instance attribute directly:
 >>> a.__dict__['g']
 'sabotage'

We could define __delete__ in the descriptor too, but didn't so

 >>> del a.g
 Traceback (most recent call last):
   File "<stdin>", line 1, in ?
 AttributeError: __delete__

We could have made the descriptor return a.g if unable to form a bound method,
but then you'd probably want to permit arbitrary assignment to the descriptor-controlled
attributes too ;-)

Regards,
Bengt Richter



More information about the Python-list mailing list