[Python-Dev] Re: method decorators (PEP 318)
Robert Mollitor
mollitor at earthlink.net
Sun Mar 28 15:00:22 EST 2004
On Sunday, March 28, 2004, at 09:45 AM, Phillip J. Eby wrote:
> At 12:54 PM 3/28/04 +0100, Paul Moore wrote:
>> Robert Mollitor <mollitor at earthlink.net> writes:
>>
>> > It would be nice if transformer decorations were never allowed
>> > "arguments". It would keep that list as short
>> > and as tidy as possible.
>>
>> That's the sort of restriction I imagined that Guido was tending
>> towards. While it's justifiable in this context, I would prefer to
>> leave the option of using arguments available, in case someone comes
>> up with a use where function attributes are inappropriate.
>
> It's inappropriate to use attributes of a function for attributes that
> logically belong to the decorator. For example
> 'synchronized(lockattr="baz")'. The 'lockattr' logically belongs to
> the synchronizing decoration. Declaring it in a separate location
> makes the whole thing harder to read/understand.
The following is an example in PEP 318:
def accepts(*types):
def check_accepts(f):
assert len(types) == f.func_code.co_argcount
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
assert isinstance (a, t), \
"arg %r does not match %s" % (a, t)
return f(*args, **kwds)
return new_f
return f(*args, **kwds)
def returns(rtype):
def check_returns(f):
def new_f(*args, **kwds):
result = f(*args, **kwds)
assert isinstance(result, rtype), \
"return value %r does not match %s" % (result, rtype)
return result
return new_f
return check_returns
that is two functions that return a function that returns a function.
Why? Because
def func(arg1, arg2) [accepts(int, (int, float)), returns((int,
float))]:
pass
expands roughly to "returns((int, float)) (accepts(int, (int, float))
(func))". Whether or not this
is the best implementation, it is reasonable if you view the parameters
as logically belonging
to the decorator instead of logically belonging to the function. With
transformer plus annotations,
this could be recast as
def func [check_types] (arg1, arg2):
:accepts (int, (int, float))
:returns (int, float)
pass
def check_types(f):
if hasattr(f, 'accepts'):
assert len(types) == f.func_code.co_argcount
def new_f(*args, **kwds):
if hasattr(f, 'accepts'):
for (a, t) in zip(args, f.accepts):
assert isinstance (a, t), \
"arg %r does not match %s" % (a, t)
result = f(*args, **kwds)
if hasattr(f,'returns'):
assert isinstance(result, f.returns), \
"return value %r does not match %s" % (result, f.returns)
return result
return new_f
As an added bonus, the function attributes are available for other
inspecting operations such as
generating documentation, say.
My point here is we may want to aim for making the information kept on
the function object itself as rich as possible
and make the "decorations" do the work of pulling information from
whatever the function "publishes".
Even if you have a case like you mention of
'synchronized(lockattr="baz")', where perhaps you might want to say
that nobody outside of this particular transformer implementation would
ever want to know which attribute the function
is synchronized on, there is a trivial workaround if we restrict the
transformer list to identifiers:
sync = synchronized(lockattr="baz")
def func [sync] (arg1, arg2):
pass
However, I think that in general people will decide to be "generous"
and choose to publish those parameters
as true function attributes on 'func', so this work-around should not
be necessary.
It is true that there is a potential namespace issue with function
attribute names, but this is a general multiple
inheritance issue. In fact, it might not be a bad idea to view
transformer decorations as "mix-ins" (though with
non-trivial differences). Despite the fact that most of the examples
in PEP 318 are functions, the only existing
transformers, classmethod and staticmethod are NOT functions. They are
types. In fact, it may turn out that in
order to ensure commutability we may require that all transformers be
types/classes that follow a specific pattern.
Now, if the transformers are classes, then they are not functions that
return a function or (in the parameterized
case) functions that return a function that returns a function.
A non-parameterized transformer would be something like
class check_types:
def __init__ (self, f):
self.f = f
def __call__(self, < some syntax involving asterisks>):
do something with self.f
A parameterized transformer could be something like
class synchronized:
def __init__(self, lockattr):
self.lockattr = lockattr
def __call__(self, <again with the asterisks>):
def new_f(f):
do something with f
return new_f
But there is perhaps one crucial difference, the non-parameterized one
returns a non-function object instance that
may have other methods besides __call__ (to make the transformer play
nice commutability-wise, say), whereas
the parameterized one is returning a true function object (which would
be treated by an outer transformer as a
unwrapped function, probably). To make them analogous, you would need
something like
class synchronized(self, lockattr):
def __init__(self, lockattr):
self.lockattr = lockattr
def __call__(self, <...>):
class synchronzied_wrapper:
def __init__(self, f):
self.f = f
def __call__(self, <...>):
do something with self.f
So "type(f)" (given "def f [synchonized(...)] (...): ...") would not be
'synchronized' but 'synchronized_wrapper'.
robt
More information about the Python-Dev
mailing list