Here’s a more substantial module example that demonstrates another way that the prior section’s __name__ trick is commonly employed. The following module, formats.py, defines string formatting utilities for importers, but also checks its name to see if it is being run as a top-level script; if so, it tests and uses arguments listed on the system command line to run a canned or passed-in test. In Python, the sys.argv list contains command-line arguments—it is a list of strings reflecting words typed on the command line, where the first item is always the name of the script being run. We used this in Chapter 21’s benchmark tool as switches, but leverage it as a general input mechanism here:
#!python
"""
File: formats.py (2.X and 3.X)
Various specialized string display formatting utilities.
Test me with canned self-test or command-line arguments.
To do: add parens for negative money, add more features.
"""
def commas(N):
"""
Format positive integer-like N for display with
commas between digit groupings: "xxx,yyy,zzz".
"""
digits = str(N)
assert(digits.isdigit())
result = ''
while digits:
digits, last3 = digits[:-3], digits[-3:]
result = (last3 + ',' + result) if result else last3
return result
def money(N, numwidth=0, currency='$'):
"""
Format number N for display with commas, 2 decimal digits,
leading $ and sign, and optional padding: "$ -xxx,yyy.zz".
numwidth=0 for no space padding, currency='' to omit symbol,
and non-ASCII for others (e.g., pound=u'\xA3' or u'\u00A3').
"""
sign = '-' if N < 0 else ''
N = abs(N)
whole = commas(int(N))
fract = ('%.2f' % N)[-2:]
number = '%s%s.%s' % (sign, whole, fract)
return '%s%*s' % (currency, numwidth, number)
if __name__ == '__main__':
def selftest():
tests = 0, 1 # fails: −1, 1.23
tests += 12, 123, 1234, 12345, 123456, 1234567
tests += 2 ** 32, 2 ** 100
for test in tests:
print(commas(test))
print('')
tests = 0, 1, −1, 1.23, 1., 1.2, 3.14159
tests += 12.34, 12.344, 12.345, 12.346
tests += 2 ** 32, (2 ** 32 + .2345)
tests += 1.2345, 1.2, 0.2345
tests += −1.2345, −1.2, −0.2345
tests += −(2 ** 32), −(2**32 + .2345)
tests += (2 ** 100), −(2 ** 100)
for test in tests:
print('%s [%s]' % (money(test, 17), test))
import sys
if len(sys.argv) == 1:
selftest()
else:
print(money(float(sys.argv[1]), int(sys.argv[2])))
This file works identically in Python 2.X and 3.X. When run directly, it tests itself as before, but it uses options on the command line to control the test behavior. Run this file directly with no command-line arguments on your own to see what its self-test code prints—it’s too extensive to list in full here:
c:\code>python formats.py0 1 12 123 1,234 12,345 123,456 1,234,567...etc...
To test specific strings, pass them in on the command line along with a minimum field width; the script’s __main__ code passes them on to its money function, which in turn runs commas:
C:\code>python formats.py 999999999 0$999,999,999.00 C:\code>python formats.py −999999999 0$-999,999,999.00 C:\code>python formats.py 123456789012345 0$123,456,789,012,345.00 C:\code>python formats.py −123456789012345 25$ −123,456,789,012,345.00 C:\code>python formats.py 123.456 0$123.46 C:\code>python formats.py −123.454 0$-123.45
As before, because this code is instrumented for dual-mode usage, we can also import its tools normally to reuse them as library components in scripts, modules, and the interactive prompt:
>>>from formats import money, commas>>>money(123.456)'$123.46' >>>money(-9999999.99, 15)'$ −9,999,999.99' >>>X = 99999999999999999999>>>'%s (%s)' % (commas(X), X)'99,999,999,999,999,999,999 (99999999999999999999)'
You can use command-line arguments in ways similar to this example to provide general inputs to scripts that may also package their code as functions and classes for reuse by importers. For more advanced command-line processing, see Python Command-Line Arguments in Appendix A, and the getopt, optparse, and argparse modules’ documentation in Python’s standard library manual. In some scenarios, you might also use the built-in input function, used in Chapter 3 and Chapter 10, to prompt the shell user for test inputs instead of pulling them from the command line.
Also see Chapter 7’s discussion of the new {,d} string format method syntax added in Python 2.7 and 3.1; this formatting extension separates thousands groups with commas much like the code here. The module listed here, though, adds money formatting, can be changed, and serves as a manual alternative for comma insertions in earlier Pythons.
This module’s money function defaults to dollars, but supports other currency symbols by allowing you to pass in non-ASCII Unicode characters. The Unicode ordinal with hexadecimal value 00A3, for example, is the pound symbol, and 00A5 is the yen. You can code these in a variety of forms, as:
The character’s decoded Unicode code point ordinal (integer) in a text string, with either Unicode or hex escapes (for 2.X compatibility, use a leading u in such string literals in Python 3.3)
The character’s raw encoded form in a byte string that is decoded before passed, with hex escapes (for 3.X compatibility, use a leading b in such string literals in Python 2.X)
The actual character itself in your program’s text, along with a source code encoding declaration
We previewed Unicode in Chapter 4 and will get into more details in Chapter 37, but its basic requirements here are fairly simple, and serve as a decent use case. To test alternative currencies, I typed the following in a file, formats_currency.py, because it was too much to reenter interactively on changes:
from __future__ import print_function # 2.X
from formats import money
X = 54321.987
print(money(X), money(X, 0, ''))
print(money(X, currency=u'\xA3'), money(X, currency=u'\u00A5'))
print(money(X, currency=b'\xA3'.decode('latin-1')))
print(money(X, currency=u'\u20AC'), money(X, 0, b'\xA4'.decode('iso-8859-15')))
print(money(X, currency=b'\xA4'.decode('latin-1')))
The following gives this test file’s output in Python 3.3 in IDLE, and in other contexts configured properly. It works the same in 2.X because it prints and codes strings portably. Per Chapter 11, a __future__ import enables 3.X print calls in 2.X. And as introduced in Chapter 4, 3.X b'...' bytes literals are taken as simple strings in 2.X, and 2.X u'...' Unicode literals as treated as normal strings in 3.X as of 3.3.
$54,321.99 54,321.99 £54,321.99 ¥54,321.99 £54,321.99 €54,321.99 €54,321.99 ¤54,321.99
If this works on your computer, you can probably skip the next few paragraphs. Depending on your interface and system settings, though, getting this to run and display properly may require additional steps. On my machine, it behaves correctly when Python and the display medium are in sync, but the euro and generic currency symbols in the last two lines fail with errors in a basic Command Prompt on Windows.
Specifically, this test script always runs and produces the output shown in the IDLE GUI in both 3.X and 2.X, because Unicode-to-glyph mappings are handled well. It also works as advertised in 3.X on Windows if you redirect the output to a file and open it with Notepad, because 3.X encodes content on this platform in a default Windows format that Notepad understands:
c:\code>formats_currency.py > tempc:\code>notepad temp
However, this doesn’t work in 2.X, because Python tries to encode printed text as ASCII by default. To show all the non-ASCII characters in a Windows Command Prompt window directly, on some computers you may need to change the Windows code page (used to render characters) as well as Python’s PYTHONIOENCODING environment variable (used as the encoding of text in standard streams, including the translation of characters to bytes when they are printed) to a common Unicode format such as UTF-8:
c:\code>chcp 65001# Console matches Python c:\code>set PYTHONIOENCODING=utf-8# Python matches console c:\code>formats_currency.py > temp# Both 3.X and 2.X write UTF-8 text c:\code>type temp# Console displays it properly c:\code>notepad temp# Notepad recognizes UTF-8 too
You may not need to take these steps on some platforms and even on some Windows distributions. I did because my laptop’s code page is set to 437 (U.S. characters), but your code pages may vary.
Subtly, the only reason this test works on Python 2.X at all is because 2.X allows normal and Unicode strings to be mixed, as long as the normal string is all 7-bit ASCII characters. On 3.3, the 2.X u'...' Unicode literal is supported for compatibility, but taken the same as normal '...' strings, which are always Unicode (removing the leading u makes the test work in 3.0 through 3.2 too, but breaks 2.X compatibility):
c:\code>py −2>>>print u'\xA5' + '1', '%s2' % u'\u00A3'# 2.X: unicode/str mix for ASCII str ¥1 £2 c:\code>py −3>>>print(u'\xA5' + '1', '%s2' % u'\u00A3')# 3.X: str is Unicode, u'' optional ¥1 £2 >>>print('\xA5' + '1', '%s2' % '\u00A3')¥1 £2
Again, there’s much more on Unicode in Chapter 37—a topic many see as peripheral, but which can crop up even in relatively simple contexts like this! The takeaway point here is that, operational issues aside, a carefully coded script can often manage to support Unicode in both 3.X and 2.X.
Finally, because this example’s main file uses the docstring feature introduced in Chapter 15, we can use the help function or PyDoc’s GUI/browser modes to explore its tools as well—modules are almost automatically general-purpose tools. Here’s help at work; Figure 25-2 gives the PyDoc view on our file.
>>>import formats>>>help(formats)Help on module formats: NAME formats DESCRIPTION File: formats.py (2.X and 3.X) Various specialized string display formatting utilities. Test me with canned self-test or command-line arguments. To do: add parens for negative money, add more features. FUNCTIONS commas(N) Format positive integer-like N for display with commas between digit groupings: "xxx,yyy,zzz". money(N, numwidth=0, currency='$') Format number N for display with commas, 2 decimal digits, leading $ and sign, and optional padding: "$ -xxx,yyy.zz". numwidth=0 for no space padding, currency='' to omit symbol, and non-ASCII for others (e.g., pound=u'£' or u'£'). FILE c:\code\formats.py
Figure 25-2. PyDoc’s view of formats.py, obtained by running a “py −3 -m pydoc –b” command line in 3.2 and later and clicking on the file’s index entry (see Chapter 15)