[Python-3000] rough draft signature PEP

Brett Cannon brett at python.org
Sat Apr 22 23:47:58 CEST 2006


[I am posting to python-3000 since this is where the parameter list
ideas are being discussed, but this is probably generic enough to
eventually make it into the 2.x line]

Here is a rough draft of a PEP I wrote last summer after I had Guido
come for lunch at Google when I was interning there (i.e., a little
old  =) .  Someone (I think Philip) had suggested something like this
back then but it didn't go any farther.  I liked the idea and I asked
Guido at lunch if he was okay for it; he was.

So I wrote the following PEP along with a Python implementation
(attached, along with test cases).  It is still a rough draft since I
am on vacation on top of visiting my dad for his birthday and thus
cannot put a ton of time into this at the moment, so don't consider
this a final version in any way.  There is already a list of things to
consider and I am sure there are some things discussed during the new
parameter list ideas that could stand to be worked in.  I am quite
happy to work on finishing this PEP so that Talin can focus on the
parameter list PEP.  So just comment away and I will work on
incorporating them as time permits.

-Brett

------------------------------------------------------------------

PEP: XXX
Title: Introducing the __signature__ Attribute
Version: $Revision: 1.5 $
Last-Modified: $Date: 2005/06/07 13:17:37 $
Author: Brett Cannon <brett at python.org>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: XX-XXX-XXXX
Post-History:

XXX
    * Break abstract into Abstract and Rationale
    * write signature() function
        - maybe not in built-ins; inspect instead?
    * look up object identity (issue|crisis) as reference
    * automatically apply signature when decorator called?
    * make more general by having an attribute
      that points to original object being wrapped that introspection
checks first?
      - __wrapping__
      - __identity__
      - __introspect__
      - __metadata__

Abstract
========

Decorators were introduced to Python in version 2.4 .
Their introduction provided a syntactically easy way to "decorate" functions
and methods.
The ease of use has allowed the use of functions that wrap other functions and
methods to become more prevalent in Python code.

Unfortunately, one side-effect of the increased use of decorators is the loss
of introspection on the function being wrapped.
Most decorators are not meant to be directly noticed by someone using the code;
decorators are meant to be heard, not seen.
But when a decorator is used on a function or method that returns a new
function that wraps the original, introspection will occur on the wrapping
function, not the wrapped one.

::

 def yell(func):
     def wrapper(*args, **kwargs):
         print "Hi!"
	 return func(*args, **kwargs)
     return wrapper

 @yell
 def wrapped_one_arg(x):
     pass

 def unwrapped_one_arg(x):
     pass

 if __name__ == '__main__':
     from inspect import getargspec

     print getargspec(wrapped_one_arg) == getargspec(unwrapped_one_arg)

To help deal with this phenomenon, the __signature__ attribute is being
proposed.
It is to be an optional attribute on objects where introspection of function
parameters may be performed.
It contains an instance of the Signature class that represents all relevant
data that one might want to know about the call signature of a function or
method.
This allows one to assign to a wrapping function's __signature__ attribute an
instance of the Signature class that represents the wrapped function or method,
thus allowing introspection on the wrapping function to reflect the call
signature of the wrapped function.  The Signature also works for classes
(representing the call signature for instantiation) and instances (for the
``__call__`` method of an instance).

A built-in, aptly named ``signature``, is also being proposed.  When passed an
object that meets the proper requirements, it will either return a new instance
of the Signature class or the pre-existing object for the object passed in.


Types of Argument Parameters
============================

Python has a fairly rich set of function parameter possibilities.
To start, there are required arguments; ``def positional(x): pass``.
These types of parameters require an argument be passed in for them.

Next, there are default arguments; ``def defaults(x=42): pass``.
They have a default value assigned to them if no argument is passed in for that
parameter.
They are optional, though, unlike required arguments.

Lastly, there are excess arguments; ``def excess(*args, **kwargs): pass``.
There are two types of excess arguments.
One type is an excess positional argument (``*args``).
When this type of parameter is present all positional arguments provided during
the call are collected into a tuple and assigned to the excess positional
argument.
The other type of parameter is an excess keyword argument.
All keyword arguments that are not directly assigned to an existing keyword
parameter are collected into a dict keyed on the keyword name and containing a
value of the argument passed in.
This dict then gets assigned to the excess keyword argument parameter.
For both types, direct assignment to the parameter is not permitted.


Signature Object
================

All types of function parameters mentioned in the section `Types of
Argument Parameters`_
must be handled by the Signature object in order for it to be useful.
One should be able to view the Signature object as providing enough information
to be able to know what is possible in terms of calling a function or method.
That said, there must be a way to handle required arguments, default arguments,
and both types of excess argument parameters.

For required arguments, the attribute ``required_args`` provides a tuple of
strings that correspond to the names of the required arguments in the order
they are defined in the function.
By providing the name and order one can easily find out the calling convention
required for the minimal call of the function or method.

Default arguments are represented by the ``default_args`` attribute.
A tuple is contained within the attribute which itself contains two item tuples
of ``(name, value)`` pairs representing the name of the default argument
parameter and the value that is given.  For instance, for the function ``def
defaults(x=42, y=True): pass``, the value held by ``default_args`` would be
``(('x', 42), ('y', True))``.

Excess argument parameters each have their own attribute to represent their
existence.
For the existence of an excess positional argument, ``excess_pos_args`` exists.
``excess_kw_args`` represents an excess keyword argument parameter.
Both attributes contain a boolean value representing whether they parameter
exists.
While changing the attributes to contain the name of the respective excess
parameter is technically feasible, it has been deemed unnecessary since that
knowledge is not useful when making an actual call to the function or method
that is represented by Signature object.

One key decision that was made during the development of attribute names is
taking into consideration the possibility of optional type checking for
function parameters.
This meant the names should be general enough to represent what they do without
type information.

Finally, the Signature object implements a ``__str__`` method.
Calling this will return a string representing the parameter as might be seen
in the actual code.
This is provided for human consumption to easily deduce what is required for
calling the object.
No parentheses are put around the returned string.

The final interface, following the syntax outlined in [#interface-syntax]_,
is::

 interface Signature:
     """Specifies the call signature of a class, instance, method, or class"""

     def __str__(self) -> str:
         """String representation of the parameters"""

     required_args: tuple(str)
     default_args: tuple(tuple(str, object))
     excess_pos_args: bool
     excess_kw_args: bool


Implementation
==============

XXX Signature object (using 'inspect'), changing pydoc (and thus help()),
    builtin


Example Usage
=============

For all examples, assume the variable ``sig`` contains an instance of the
Signature object.


Making a decorator have its __signature__ set to the function it is wrapping
----------------------------------------------------------------------------
::

 def wrapper(func):
    def inner(*args, **kwargs):
        # Magic happens here ...
 	return func(*args, **kwargs)
    inner.__signature__ = signature(func)
    return inner


All positional arguments
------------------------
::

 sig.required_args + (name for name, value in sig.default_args)


Dictionary of default arguments
-------------------------------
::

 dict(sig.default_args)


References
==========

.. [#interface-syntax] Guido van Rossum's blog ("Interfaces or
Abstract Base Classes?")
   http://www.artima.com/weblogs/viewpost.jsp?thread=92662


Copyright
=========

This document has been placed in the public domain.



..
   Local Variables:
   mode: indented-text
   indent-tabs-mode: nil
   sentence-end-double-space: t
   fill-column: 70
   End:
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.py
Type: application/octet-stream
Size: 2262 bytes
Desc: not available
Url : http://mail.python.org/pipermail/python-3000/attachments/20060422/726eb5a2/attachment-0002.obj 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: test_signature.py
Type: application/octet-stream
Size: 3969 bytes
Desc: not available
Url : http://mail.python.org/pipermail/python-3000/attachments/20060422/726eb5a2/attachment-0003.obj 


More information about the Python-3000 mailing list