456 lines
14 KiB
Plaintext
456 lines
14 KiB
Plaintext
PEP: 468
|
||
Title: Preserving the order of \*\*kwargs in a function.
|
||
Version: $Revision$
|
||
Last-Modified: $Date$
|
||
Author: Eric Snow <ericsnowcurrently@gmail.com>
|
||
Discussions-To: python-ideas@python.org
|
||
Status: Draft
|
||
Type: Standards Track
|
||
Content-Type: text/x-rst
|
||
Created: 5-Apr-2014
|
||
Python-Version: 3.5
|
||
Post-History: 5-Apr-2014
|
||
Resolution:
|
||
|
||
|
||
Abstract
|
||
========
|
||
|
||
The \*\*kwargs syntax in a function definition indicates that the
|
||
interpreter should collect all keyword arguments that do not correspond
|
||
to other named parameters. However,
|
||
Python does not preserved the order in which those collected keyword
|
||
arguments were passed to the function. In some contexts the order
|
||
matters. This PEP introduces a mechanism by which the passed order of
|
||
collected keyword arguments will now be preserved.
|
||
|
||
|
||
Motivation
|
||
==========
|
||
|
||
Python's \*\*kwargs syntax in function definitions provides a powerful
|
||
means of dynamically handling keyword arguments. In some applications
|
||
of the syntax (see _`Use Cases`), the semantics applied to the
|
||
collected keyword arguments requires that order be preserved.
|
||
Unsurprisingly, this is similar to how OrderedDict is related to dict.
|
||
|
||
Currently to preserved the order you have to do so manually and
|
||
separately from the actual function call. This involves building an
|
||
ordered mapping, whether an OrderedDict or an iterable of 2-tuples,
|
||
which is then passed as a single argument to the function.
|
||
[#arg_unpacking]_
|
||
|
||
With the capability described in this PEP, that boilerplate would no
|
||
longer be required.
|
||
|
||
For comparison, currently::
|
||
|
||
kwargs = OrderedDict()
|
||
kwargs['eggs'] = ...
|
||
...
|
||
def spam(a, kwargs):
|
||
...
|
||
|
||
and with this proposal::
|
||
|
||
def spam(a, **kwargs):
|
||
...
|
||
|
||
Nick Coglan, speaking of some of the uses cases, summed it up well
|
||
[#nick_obvious]_::
|
||
|
||
These *can* all be done today, but *not* by using keyword arguments.
|
||
In my view, the problem to be addressed is that keyword arguments
|
||
*look* like they should work for these cases, because they have a
|
||
definite order in the source code. The only reason they don't work
|
||
is because the interpreter throws that ordering information away.
|
||
|
||
It's a textbook case of a language feature becoming an attractive
|
||
nuisance in some circumstances: the simple and obvious solution for
|
||
the above use cases *doesn't actually work* for reasons that aren't
|
||
obviously clear if you don't have a firm grasp of Python's admittedly
|
||
complicated argument handling.
|
||
|
||
This observation is supported by the appearance of this proposal over
|
||
the years and the numerous times that people have been confused by the
|
||
constructor for OrderedDict. [#past_threads]_ [#loss_of_order]_
|
||
[#compact_dict]_
|
||
|
||
|
||
Use Cases
|
||
=========
|
||
|
||
As Nick noted, the current behavior of \*\*kwargs is unintuitive in
|
||
cases where one would expect order to matter. Aside from more specific
|
||
cases outlined below, in general "anything else where you want to
|
||
control the iteration order *and* set field names and values in a single
|
||
call will potentially benefit." [#nick_general]_ That matters in the
|
||
case of factories (e.g. __init__()) for ordered types.
|
||
|
||
Serialization
|
||
-------------
|
||
|
||
Obviously OrderedDict would benefit (both __init__() and update()) from
|
||
ordered kwargs. However, the benefit also extends to serialization
|
||
APIs [#nick_obvious]_::
|
||
|
||
In the context of serialisation, one key lesson we have learned is
|
||
that arbitrary ordering is a problem when you want to minimise
|
||
spurious diffs, and sorting isn't a simple solution.
|
||
|
||
Tools like doctest don't tolerate spurious diffs at all, but are
|
||
often amenable to a sorting based answer.
|
||
|
||
The cases where it would be highly desirable to be able use keyword
|
||
arguments to control the order of display of a collection of key
|
||
value pairs are ones like:
|
||
|
||
* printing out key:value pairs in CLI output
|
||
* mapping semantic names to column order in a CSV
|
||
* serialising attributes and elements in particular orders in XML
|
||
* serialising map keys in particular orders in human readable formats
|
||
like JSON and YAML (particularly when they're going to be placed
|
||
under source control)
|
||
|
||
Debugging
|
||
---------
|
||
|
||
In the words of Raymond Hettinger [#raymond_debug]_::
|
||
|
||
It makes it easier to debug if the arguments show-up in the order
|
||
they were created. AFAICT, no purpose is served by scrambling them.
|
||
|
||
Other Use Cases
|
||
---------------
|
||
|
||
* Mock objects. [#mock]_
|
||
* Controlling object presentation.
|
||
* Alternate namedtuple() where defaults can be specified.
|
||
* Specifying argument priority by order.
|
||
|
||
|
||
Concerns
|
||
========
|
||
|
||
Performance
|
||
-----------
|
||
|
||
As already noted, the idea of ordered keyword arguments has come up on
|
||
a number of occasions. Each time it has been met with the same
|
||
response, namely that preserving keyword arg order would have a
|
||
sufficiently adverse effect on function call performance that it's not
|
||
worth doing. However, Guido noted the following [#guido_open]_::
|
||
|
||
Making **kwds ordered is still open, but requires careful design and
|
||
implementation to avoid slowing down function calls that don't benefit.
|
||
|
||
As will be noted below, there are ways to work around this at the
|
||
expense of increased complication. Ultimately the simplest approach is
|
||
the one that makes the most sense: pack collected key word arguments
|
||
into an OrderedDict. However, without a C implementation of OrderedDict
|
||
there isn't much to discuss. That should change in Python 3.5.
|
||
[#c_ordereddict]_
|
||
|
||
In some cases the difference of performance between dict and OrderedDict
|
||
*may* be of significance. For instance: when the collected
|
||
kwargs has an extended lifetime outside the originating function or the
|
||
number of collected kwargs is massive. However, the difference in
|
||
performance (both CPU and memory) in those cases should not be
|
||
significant. Furthermore, the performance of the C OrderedDict
|
||
implementation is essentially identical with dict for the non-mutating
|
||
API. A concrete representation of the difference in performance will be
|
||
a part of this proposal before its resolution.
|
||
|
||
Other Python Implementations
|
||
----------------------------
|
||
|
||
Another important issue to consider is that new features must be
|
||
cognizant of the multiple Python implementations. At some point each of
|
||
them would be expected to have implemented ordered kwargs. In this
|
||
regard there doesn't seem to be an issue with the idea. [#ironpython]_
|
||
Each of the major Python implementations will be consulted regarding
|
||
this proposal before its resolution.
|
||
|
||
|
||
Specification
|
||
=============
|
||
|
||
Starting in version 3.5 Python will preserve the order of keyword
|
||
arguments as passed to a function. To accomplish this the collected
|
||
kwargs will now be an OrderedDict rather than a dict.
|
||
|
||
This will apply only to functions for which the definition uses the
|
||
\*\*kwargs syntax for collecting otherwise unspecified keyword
|
||
arguments. Only the order of those keyword arguments will be
|
||
preserved.
|
||
|
||
Relationship to \*\*-unpacking syntax
|
||
-------------------------------------
|
||
|
||
The ** unpacking syntax in function calls has no special connection with
|
||
this proposal. Keyword arguments provided by unpacking will be treated
|
||
in exactly the same way as they are now: ones that match defined
|
||
parameters are gather there and the remainder will be collected into the
|
||
ordered kwargs (just like any other unmatched keyword argument).
|
||
|
||
Note that unpacking a mapping with undefined order, such as dict, will
|
||
preserve its iteration order like normal. It's just that the order will
|
||
remain undefined. The OrderedDict into which the unpacked key-value
|
||
pairs will then be packed will not be able to provide any alternate
|
||
ordering. This should not be surprising.
|
||
|
||
There have been brief discussions of simply passing these mappings
|
||
through to the functions kwargs without unpacking and repacking them,
|
||
but that is both outside the scope of this proposal and probably a bad
|
||
idea regardless. (There is a reason those discussions were brief.)
|
||
|
||
Relationship to inspect.Signature
|
||
---------------------------------
|
||
|
||
Signature objects should need no changes. The `kwargs` parameter of
|
||
inspect.BoundArguments (returned by Signature.bind() and
|
||
Signature.bind_partial()) will change from a dict to an OrderedDict.
|
||
|
||
C-API
|
||
-----
|
||
|
||
TBD
|
||
|
||
Syntax
|
||
------
|
||
|
||
No syntax is added or changed by this proposal.
|
||
|
||
Backward-Compatibility
|
||
----------------------
|
||
|
||
The following will change:
|
||
|
||
* type(kwargs)
|
||
* iteration order of kwargs will now be consistent (except of course in
|
||
the case described above)
|
||
* as already noted, performance will be marginally different
|
||
|
||
None of these should be an issue. However, each will be carefully
|
||
considered while this proposal is under discussion.
|
||
|
||
|
||
Reference Implementation
|
||
========================
|
||
|
||
TBD
|
||
|
||
Implementation Notes
|
||
--------------------
|
||
|
||
TBD
|
||
|
||
|
||
Alternate Approaches
|
||
====================
|
||
|
||
Opt-out Decorator
|
||
-----------------
|
||
|
||
This is identical to the current proposal with the exception that Python
|
||
would also provide a decorator in functools that would cause collected
|
||
keyword arguments to be packed into a normal dict instead of an
|
||
OrderedDict.
|
||
|
||
Prognosis:
|
||
|
||
This would only be necessary if performance is determined to be
|
||
significantly different in some uncommon cases or that there are other
|
||
backward-compatibility concerns that cannot be resolved otherwise.
|
||
|
||
Opt-in Decorator
|
||
----------------
|
||
|
||
The status quo would be unchanged. Instead Python would provide a
|
||
decorator in functools that would register or mark the decorated
|
||
function as one that should get ordered keyword arguments. The
|
||
performance overhead to check the function at call time would be
|
||
marginal.
|
||
|
||
Prognosis:
|
||
|
||
The only real down-side is in the case of function wrappers factories
|
||
(e.g. functools.partial and many decorators) that aim to perfectly
|
||
preserve keyword arguments by using kwargs in the wrapper definition
|
||
and kwargs unpacking in the call to the wrapped function. Each wrapper
|
||
would have to be updated separately, though having functools.wraps() do
|
||
this automaticallywould help.
|
||
|
||
__kworder__
|
||
-----------
|
||
|
||
The order of keyword arguments would be stored separately in a list at
|
||
call time. The list would be bound to __kworder__ in the function
|
||
locals.
|
||
|
||
Prognosis:
|
||
|
||
This likewise complicates the wrapper case.
|
||
|
||
Compact dict with faster iteration
|
||
----------------------------------
|
||
|
||
Raymond Hettinger has introduced the idea of a dict implementation that
|
||
would result in preserving insertion order on dicts (until the first
|
||
deletion). This would be a perfect fit for kwargs. [#compact_dict]_
|
||
|
||
Prognosis:
|
||
|
||
The idea is still uncertain in both viability and timeframe.
|
||
|
||
\*\*\*kwargs
|
||
------------
|
||
|
||
This would add a new form to a function's signature as a mutually
|
||
exclusive parallel to \*\*kwargs. The new syntax, \*\*\*kwargs (note
|
||
that there are three asterisks), would indicate that kwargs should
|
||
preserve the order of keyword arguments.
|
||
|
||
Prognosis:
|
||
|
||
New syntax is only added to Python under the most *dire* circumstances.
|
||
With other available solutions, new syntax is not justifiable.
|
||
Furthermore, like all opt-in solutions, the new syntax would complicate
|
||
the pass-through case.
|
||
|
||
annotations
|
||
-----------
|
||
|
||
This is a variation on the decorator approach. Instead of using a
|
||
decorator to mark the function, you would use a function annotation on
|
||
\*\*kwargs.
|
||
|
||
Prognosis:
|
||
|
||
In addition to the pass-through complication, annotations have been
|
||
actively discouraged in Python core development. Use of annotations to
|
||
opt-in to order preservation runs the risk of interfering with other
|
||
application-level use of annotations.
|
||
|
||
dict.__order__
|
||
--------------
|
||
|
||
dict objects would have a new attribute, `__order__` that would default
|
||
to None and that in the kwargs case the interpreter would use in the
|
||
same way as described above for __kworder__.
|
||
|
||
Prognosis:
|
||
|
||
It would mean zero impact on kwargs performance but the change would be
|
||
pretty intrusive (Python uses dict a lot). Also, for the wrapper case
|
||
the interpreter would have to be careful to preserve `__order__`.
|
||
|
||
KWArgsDict.__order__
|
||
--------------------
|
||
|
||
This is the same as the `dict.__order__` idea, but kwargs would be an
|
||
instance of a new minimal dict subclass that provides the `__order__`
|
||
attribute. dict would instead be unchanged.
|
||
|
||
Prognosis:
|
||
|
||
Simply switching to OrderedDict is a less complicated and more intuitive
|
||
change.
|
||
|
||
|
||
Acknowledgements
|
||
================
|
||
|
||
Thanks to Andrew Barnert for helpful feedback and to the participants of
|
||
all the past email threads.
|
||
|
||
|
||
Footnotes
|
||
=========
|
||
|
||
.. [#arg_unpacking]
|
||
|
||
Alternately, you could also replace ** in your function definition
|
||
with * and then pass in key/value 2-tuples. This has the advantage
|
||
of not requiring the keys to be valid identifier strings. See
|
||
https://mail.python.org/pipermail/python-ideas/2014-April/027491.html.
|
||
|
||
|
||
References
|
||
==========
|
||
|
||
.. [#nick_obvious]
|
||
https://mail.python.org/pipermail/python-ideas/2014-April/027512.html
|
||
|
||
.. [#past_threads]
|
||
https://mail.python.org/pipermail/python-ideas/2009-April/004163.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2010-October/008445.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2011-January/009037.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2013-February/019690.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2013-May/020727.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2014-March/027225.html
|
||
|
||
http://bugs.python.org/issue16276
|
||
|
||
http://bugs.python.org/issue16553
|
||
|
||
http://bugs.python.org/issue19026
|
||
|
||
http://bugs.python.org/issue5397#msg82972
|
||
|
||
.. [#loss_of_order]
|
||
https://mail.python.org/pipermail/python-dev/2007-February/071310.html
|
||
|
||
.. [#compact_dict]
|
||
https://mail.python.org/pipermail/python-dev/2012-December/123028.html
|
||
|
||
https://mail.python.org/pipermail/python-dev/2012-December/123105.html
|
||
|
||
https://mail.python.org/pipermail/python-dev/2013-May/126327.html
|
||
|
||
https://mail.python.org/pipermail/python-dev/2013-May/126328.html
|
||
|
||
.. [#nick_general]
|
||
https://mail.python.org/pipermail/python-dev/2012-December/123105.html
|
||
|
||
.. [#raymond_debug]
|
||
https://mail.python.org/pipermail/python-dev/2013-May/126327.html
|
||
|
||
.. [#mock]
|
||
https://mail.python.org/pipermail/python-ideas/2009-April/004163.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2009-April/004165.html
|
||
|
||
https://mail.python.org/pipermail/python-ideas/2009-April/004175.html
|
||
|
||
.. [#guido_open]
|
||
https://mail.python.org/pipermail/python-dev/2013-May/126404.html
|
||
|
||
.. [#c_ordereddict]
|
||
http://bugs.python.org/issue16991
|
||
|
||
.. [#ironpython]
|
||
https://mail.python.org/pipermail/python-dev/2012-December/123100.html
|
||
|
||
|
||
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
|
||
coding: utf-8
|
||
End:
|
||
|