[Python-ideas] positional only arguments decorator

Arnaud Delobelle arno at marooned.org.uk
Wed May 30 17:16:43 CEST 2007

On 27 May 2007, at 16:37, Steven Bethard wrote:

> On 5/27/07, Arnaud Delobelle <arno at marooned.org.uk> wrote:
>> On 21 May 2007, at 19:30, Steven Bethard wrote:
>>> Ok, looks like there's not much chance of agreeing on a syntax, so
>>> here's a decorator that covers the two use cases I know of::
>>> * matching the signature of dict() and dict.update()
>>> * allowing arguments to have their names changed without worrying
>>> about backwards compatibility (e.g. ``def func(sequence)``  
>>> changing to
>>> ``def func(iterable)``)
>> I propose a slightly different solution.  It may be a bit of a hack,
>> but I don't see why it should not be safe. This solution allows you
>> to use * and ** in order to write a definition like:
>>>>> @posonly
>> ... def update(self, container=None, **kwargs):
>> ...     return self, container, kwargs
>> ...
>>>>> update('self')
>> ('self', None, {})
>>>>> update('self', 'container')
>> ('self', 'container', {})
>>>>> update('self', self='abc', container='xyz', foo='bar')
>> ('self', None, {'self': 'abc', 'foo': 'bar', 'container': 'xyz'})
> Cool!  Yes, it's hackish, but at least the results are pretty. ;-)

Wait!  I think I've got a devastatingly simple solution.  I've  
thought about this while looking at the PyEval_EvalCodeEx() function  
in Python/ceval.c.  It turns out that  the 'co_varnames' attribute of  
a code object is only used in order to 'slot in' arguments passed as  
keywords.  So by adding a '@' to the names we make these arguments  
positional only (one could choose any decoration including a  
character which is illegal in identifiers).  This method has two  

* there is **no** overhead at all in calling a positional only function.

* the function keeps its original signature (positional only  
arguments are flagged with the trailing '@').

There is one 'drawback' (which could be considered a useful feature):

* if x is positional, f(**{'x@':1}) will work.

Here is the code.
from types import CodeType

code_args = (
     'argcount', 'nlocals', 'stacksize', 'flags', 'code',
     'consts', 'names', 'varnames', 'filename', 'name',
     'firstlineno', 'lnotab', 'freevars', 'cellvars'

def copy_code(code_obj, **kwargs):
     "Make a copy of a code object, maybe changing some attributes"
     for arg in code_args:
         if not kwargs.has_key(arg):
             kwargs[arg] = getattr(code_obj, 'co_%s' % arg)
     return CodeType(*map(kwargs.__getitem__, code_args))

def posonly(f):
     code = f.func_code
     varnames, nargs = code.co_varnames, code.co_argcount
     varnames = ( tuple(v+'@' for v in varnames[:nargs])
                  + varnames[nargs:] )
     f.func_code = copy_code(code, varnames = varnames)
     return f

That's it!  Example:

 >>> @posonly
... def update(self, container=None, **kwargs):
...     return self, container, kwargs
 >>> update(1,2, self=3, container=4, x=5)
(1, 2, {'x': 5, 'self': 3, 'container': 4})
 >>> update(1)
(1, None, {})
 >>> help(update) # Notice the unobfuscated signature!
Help on function update in module __main__:

update(self@, container@=None, **kwargs)
 >>> # 'container' is still accessible by name:
... update(1, **{'container@':2})
(1, 2, {})


More information about the Python-ideas mailing list