To round out the chapter, let’s look at one last example of argument matching at work. The code you’ll see here is intended for use in Python 2.X or earlier (it works in 3.X, too, but is pointless there): it uses both the *args arbitrary positional tuple and the **args arbitrary keyword-arguments dictionary to simulate most of what the Python 3.X print function does. Python might have offered code like this as an option in 3.X rather than removing the 2.X print entirely, but 3.X chose a clean break with the past instead.
As we learned in Chapter 11, this isn’t actually required, because 2.X programmers can always enable the 3.X print function with an import of this form (available in 2.6 and 2.7):
from __future__ import print_function
To demonstrate argument matching in general, though, the following file, print3.py, does the same job in a small amount of reusable code, by building up the print string and routing it per configuration arguments:
#!python
"""
Emulate most of the 3.X print function for use in 2.X (and 3.X).
Call signature: print3(*args, sep=' ', end='\n', file=sys.stdout)
"""
import sys
def print3(*args, **kargs):
sep = kargs.get('sep', ' ') # Keyword arg defaults
end = kargs.get('end', '\n')
file = kargs.get('file', sys.stdout)
output = ''
first = True
for arg in args:
output += ('' if first else sep) + str(arg)
first = False
file.write(output + end)
To test it, import this into another file or the interactive prompt, and use it like the 3.X print function. Here is a test script, testprint3.py (notice that the function must be called “print3”, because “print” is a reserved word in 2.X):
from print3 import print3 print3(1, 2, 3) print3(1, 2, 3, sep='') # Suppress separator print3(1, 2, 3, sep='...') print3(1, [2], (3,), sep='...') # Various object types print3(4, 5, 6, sep='', end='') # Suppress newline print3(7, 8, 9) print3() # Add newline (or blank line) import sys print3(1, 2, 3, sep='??', end='.\n', file=sys.stderr) # Redirect to file
When this is run under 2.X, we get the same results as 3.X’s print function:
C:\code> c:\python27\python testprint3.py
1 2 3
123
1...2...3
1...[2]...(3,)
4567 8 9
1??2??3.
Although pointless in 3.X, the results are identical when run there. As usual, the generality of Python’s design allows us to prototype or develop concepts in the Python language itself. In this case, argument-matching tools are as flexible in Python code as they are in Python’s internal implementation.
It’s interesting to notice that this example could be coded with Python 3.X keyword-only arguments, described earlier in this chapter, to automatically validate configuration arguments. The following variant, in the file print3_alt1.py, illustrates:
#!python3
"Use 3.X only keyword-only args"
import sys
def print3(*args, sep=' ', end='\n', file=sys.stdout):
output = ''
first = True
for arg in args:
output += ('' if first else sep) + str(arg)
first = False
file.write(output + end)
This version works the same as the original, and it’s a prime example of how keyword-only arguments come in handy. The original version assumes that all positional arguments are to be printed, and all keywords are for options only. That’s almost sufficient, but any extra keyword arguments are silently ignored. A call like the following, for instance, will generate an exception correctly with the keyword-only form:
>>> print3(99, name='bob')
TypeError: print3() got an unexpected keyword argument 'name'
but will silently ignore the name argument in the original version. To detect superfluous keywords manually, we could use dict.pop() to delete fetched entries, and check if the dictionary is not empty. The following version, in the file print3_alt2.py, is equivalent to the keyword-only version—it triggers a built-in exception with a raise statement, which works just as though Python had done so (we’ll study this in more detail in Part VII):
#!python
"Use 2.X/3.X keyword args deletion with defaults"
import sys
def print3(*args, **kargs):
sep = kargs.pop('sep', ' ')
end = kargs.pop('end', '\n')
file = kargs.pop('file', sys.stdout)
if kargs: raise TypeError('extra keywords: %s' % kargs)
output = ''
first = True
for arg in args:
output += ('' if first else sep) + str(arg)
first = False
file.write(output + end)
This works as before, but it now catches extraneous keyword arguments, too:
>>> print3(99, name='bob')
TypeError: extra keywords: {'name': 'bob'}
This version of the function runs under Python 2.X, but it requires four more lines of code than the keyword-only version. Unfortunately, the extra code is unavoidable in this case—the keyword-only version works on 3.X only, which negates most of the reason that I wrote this example in the first place: a 3.X emulator that only works on 3.X isn’t incredibly useful! In programs written to run on 3.X only, though, keyword-only arguments can simplify a specific category of functions that accept both arguments and options. For another example of 3.X keyword-only arguments, be sure to see the iteration timing case study in Chapter 21.