E pur si muove

Discovery of the day

Friday, May 09, 2008
>>> def f(a, b):
...     print 'a', a, 'b', b
>>> f(1, b=2)
a 1 b 2
>>> f(a=1, b=2)
a 1 b 2
>>> f(b=2, a=1)
a 1 b 2

Now I'm debating if I should really use that in production code...


Florian Jung said...

Maybe it isn't that useful in simple functions, but when you have functions with many optional arguments (for example in Rails-like web frameworks) you only have to specify the ones you really need. Moreover it makes the code much more readable because you can see instantly what the arguments really mean. I see no reason why not to use this in production code (afaik Django does), the only thing could be that function calls this way are a tiny bit slower than normal function calls, but imo this is only relevant in really tight loops.

Steve said...

It gets better (or do I mean worse?)—you can provide positional arguments for keyword parameters:

>>> def f(a, b=1):
... print "a:", a, "b:", b
>>> f("A", "B")
a: A b: B

This means that you have to be careful using *args with keywords:

>>> def f(a, b=1, *args, **kw):
... print "a:", "b:", b, "args:", args, "kw:", kw
>>> f(1, 2)
a: b: 2 args: () kw: {}
>>> f(1, 2, b=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() got multiple values for keyword argument 'b'

Don't we have fun in Python!

TK said...

I don't see why you shouldn't use that notation in production code, especially if you're using a function that has more than a couple of arguments.

The downside of course is that you need to know how the variables are named in the function, but you may find it's easier to remember those than the order.

Anonymous said...

I think part of the ugliness of this is that 'a' and 'b' don't have meaningful names and/or have a meaningful ordering. In cases where neither of these is true, it becomes less ugly:

>>> def f(speed, altitude):
...    print "%(speed)d MP/H at %(altitude)d feet" % locals()

Now calling both f(speed=100, altitude=3000) and f(altitude=3000, speed=100) make more sense. In fact, f(100, 3000) is the least understandable of the three.

But I don't think intrinsically ordered arguments will ever look good out of order, even if they have meaningful names: fillout_postcard(line2="Are", line4="Today?", line3="You", line1="How")

Anonymous said...

I guess his worry is about using keywords out of order. If that is the case then yes, it is acceptable to change the order of the keywords.

dosomething(foo, verbose=True, confirm=False)

is conceptually the same than:

dosomething(foo, confirm=False, verbose=True)

sapphirecat said...

@Anonymous, that's what I thought at first, but the actual definition in the post doesn't use keywords. They're both positional args.

And f(**some_dict) also works:

f(**d) # "a: 10 b: 20"

Which is what you'd expect unless your brain has been warped by PHP.

Robin said...

In Python 3, there's a way to specify that an argument can only be passed via keyword argument:

>>> def f(*, a, b):
... print('a', a, 'b', b)
>>> f(1, 2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 0 positional arguments (2 given)
>>> f(a=1, b=2)
a 1 b 2

Floris Bruynooghe said...

What scared me about this is that I've never regarded the name of a positional argument as part of the API. I always assumed you could rename that as you wanted whithout breaking callers, turns out you can't when callers decide to use your positional arguments as keywords... That's why I'm a little hesistant to use it in production code.

Anonymous said...

@sapphirecat: But there is no such thing as a difference between "keyword arguments" and "positional arguments" when defining a function (except for the *list and **dict, obv.), only when calling a function does the distinction exist, as Steve obliquely points out above (though I think he meant "arguments with default values," not "keyword parameters"). As Floris clarifies is his worry, the very use of giving an argument a name in the definition allows for the use of keywords when calling. That defining a default value for an argument looks similar to the calling of a function using a keyword argument, shouldn't confuse you into thinking of them as the same. I found Section 4.7 of the python tutorial very helpful on this whole topic. The example parrot function demonstrates everything being discussed here.

One way to avoid this, if you don't care what other python users will think about you, is to do something like:
>>> def f(*args):
...    if len(args) != 2:
...        raise TypeError("2 args only")
...    (a,b) = args
...    print "A",a,"B",b

However, that messes with the ability to introspect on the function's definition (e.g. help(f) is not useful, calltips in IDLE are just "(...)"), so there's probably a better way. I forget what part of the function object the introspection gets the argument list from, and whether that same list is just for information of others or is used when mapping a call to the function into its locals, but maybe you can fix this up (or maybe that assumption of mine shows a fundamental lack of understanding of the internals of function calling).

But I think attempts to forbid keyword arguments are somewhat unpythonic in spirit. It's just a guess, but I get the impression that keywords are considered a good thing, as they have greater potential for explicitness than positional arguments, in all but the shortest of argument lists (see Robin's mention of a new "*" argument specifier to catch and error on any further positional arguments -- I bet there isn't an equivalent "**" argument specifier to force preceding arguments to be positional-only).

Like I said above (anonymous #1), I think the use of clear, meaningful argument names ameliorates much of the confusion of using them in a different order from the function definition's, excepting cases where there is a clear order inherent in their meaning.

As for API stability and compatibility, I'd argue you have it wrong, Floris. If an argument has a good name, strongly linked to what it means, changing the name makes as much sense as changing the meaning of the argument. If you are willing to make such a change to your API, you very much do want to break callers' code. Imagine you want to deploy a parachute 5 seconds before hitting the ground:
Code A:
deploy_parachute = landing_time(ft_to_m(present_altitude)) - 5
Code B:
deploy_parachute = landing_time(altitude_m=ft_to_m(present_altitude)) - 5
def landing_time(altitude_m):
    """Compute landing time in seconds based on present altitude (in meters)"""

def landing_time(altitude_ft):
    """Compute landing time in seconds based on present altitude (in feet)"""
    #The engineers say they're sick of converting ft to meters all the time, so we're using feet directly now -- make sure you update your calls!

When the API changes from V1 to V2, code A will still continue to run, it will just cause you to crash into the ground. Code B won't run, and will throw an exception after the switch to API V2. I think I prefer the exception to the silent logic error.

Of course in real life I'd use a positional argrument because of not wanting to bother typing out "altitude_m" for every function call, and I'd also just have it called 'altitude' in the function definition because I'd have never considered the possibility of its metric system changing or the possibility of catching that change through good argument naming. And people who really do this sort of thing would be using a system that allows them to actually pass "3000 ft" to the function, not just "3000," and probably wouldn't accept the possibility of this being only a runtime-discoverable error anyway (read: they would demand strongly-typed, static compilation). But in theory :), keyword argument usage, if combined with very good choice of argument names in definitions, can lead to safer API stability and compatibility across versions.

Anyway: f(b=2,*[1])

Floris Bruynooghe said...

I agree that having the name of a parameter (with or without default value) part of the API is A Good Thing. I'm just even more embarrased now that I never read section 4.7.2 of the tutorial properly, somehow I never realised that when calling I could use a keyword argument for a parameter defined without default value. Shame on me. Actually even scary that I managed to live so long whithout this biting me.

New comments are not allowed.

Subscribe to: Post Comments (Atom)