Variable arguments (*args, **kwargs): seeking elegance
Steven D'Aprano
steve+comp.lang.python at pearwood.info
Sun Oct 6 22:43:18 EDT 2013
On Sat, 05 Oct 2013 21:04:25 -0700, John Ladasky wrote:
> Hi folks,
>
> I'm trying to make some of Python class definitions behave like the ones
> I find in professional packages, such as Matplotlib. A Matplotlib class
> can often have a very large number of arguments -- some of which may be
> optional, some of which will assume default values if the user does not
> override them, etc.
What makes Matplotlib so professional?
Assuming that "professional" packages necessarily do the right thing is
an unsafe assumption. Many packages have *lousy* interfaces. They often
get away with it because of the "sunk cost" fallacy -- if you've spent
$3000 on a licence for CrapLib, you're likely to stick through the pain
of learning its crap interface rather than admit you wasted $3000. Or the
package is twenty years old, and remains compatible with interfaces that
wouldn't be accepted now, but that's what the user community have learned
and they don't want to learn anything new. Or backwards compatibility
requires them to keep the old interface.
I have not used mathplotlib enough to judge its interface, but see below.
> I have working code which does this kind of thing. I define required
> arguments and their default values as a class attribute, in an
> OrderedDict, so that I can match up defaults, in order, with *args. I'm
> using set.issuperset() to see if an argument passed in **kwargs
> conflicts with one which was passed in *args. I use set.isdisjoint()
> to look for arguments in **kwargs which are not expected by the class
> definition, raising an error if such arguments are found.
The cleanest way is:
class Spam:
def __init__(
self, arg, required_arg,
another_required_arg,
arg_with_default=None,
another_optional_arg=42,
and_a_third="this is the default",
):
and so on, for however many arguments your class wants. Then, when you
call it:
s = Spam(23, "something", another_optional_arg="oops, missed one")
Python will automatically:
[quote]
match up defaults, in order, ...
see if an argument conflicts with one ...
look for arguments ... which are not expected...
raising an error if such arguments are found
[end quote]
Why re-invent the wheel? Python already checks all these things for you,
and probably much more efficiently than you do. What benefit are you
getting from manually managing the arguments?
When you have a big, complex set of arguments, you should have a single
point of truth, one class or function or method that knows what args are
expected and uses Python's argument-handling to handle them. Other
classes and functions which are thin (or even not-so-thin) wrappers
around that class shouldn't concern themselves with the details of what's
in *args and **kwargs, they should just pass them on untouched.
There are two main uses for *args:
1) Thin wrappers, where you just collect all the args and pass them on,
without caring what name they eventually get assigned to:
class MySubclass(MyClass):
def spam(self, *args):
print("calling MySubclass")
super(MySubclass, self).spam(*args)
2) Collecting arbitrary, homogeneous arguments for processing, where the
arguments don't get assigned to names, e.g.:
def mysort(*args):
return sorted(args)
mysort(2, 5, 4, 7, 1)
=> [1, 2, 4, 5, 7]
Using *args and then manually matching up each argument with a name just
duplicates what Python already does.
> Even though my code works, I'm finding it to be a bit clunky. And now,
> I'm writing a new class which has subclasses, and so actually keeps the
> "extra" kwargs instead of raising an error... This is causing me to
> re-evaluate my original code.
>
> It also leads me to ask: is there a CLEAN and BROADLY-APPLICABLE way for
> handling the *args/**kwargs/default values shuffle that I can study?
Yes. Don't do it :-)
It is sometimes useful to collect extra keyword arguments, handle them in
the subclass, then throw them away before passing them on:
class MySubclass(MyClass):
def spam(self, *args, **kwargs):
reverse = kwargs.pop('reverse', False)
msg = "calling MySubclass"
if reverse:
msg = msg[::-1]
print(msg)
super(MySubclass, self).spam(*args, **kwargs)
kwargs is also handy for implementing keyword-only arguments in Python 2
(in Python 3 it isn't needed). But in that case, you don't have to worry
about matching up keyword args by position, since position is normally
irrelevant. Python's basic named argument handling should cover nearly
all the code you want to write, in my opinion.
--
Steven
More information about the Python-list
mailing list