If you frequently read Python’s official documentation, you will notice that many documents include example code. For instance, the re module comes with numerous code examples:
>>> import re
>>> m = re.search('(?<=abc)def', 'abcdef')
>>> m.group(0)
'def'
You can copy and execute these examples in the Python interactive environment, and the results will match those shown in the documentation.
This code (along with other explanations) can be written in comments, and tools can then automatically generate documentation from them. Since this code can be pasted and run directly, can we automatically execute the code written in comments?
The answer is yes.
When writing comments, if you format them like this:
def abs(n):
'''
Function to get absolute value of number.
Example:
>>> abs(1)
1
>>> abs(-1)
1
>>> abs(0)
0
'''
return n if n >= 0 else (-n)
it explicitly tells the function’s caller the expected inputs and outputs.
Furthermore, Python’s built-in doctest module can directly extract code from comments and execute tests.
doctest strictly validates test results against the input/output format of the Python interactive command line. Only when testing exceptions can ... be used to represent lengthy, irrelevant traceback output.
Let’s use doctest to test the Dict class we wrote earlier:
# mydict2.py
class Dict(dict):
'''
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
'''
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__=='__main__':
import doctest
doctest.testmod()
Run the script with:
$ python mydict2.py
There is no output—this means all our doctests passed. If there is a bug in the program (e.g., comment out the __getattr__() method), running the script will throw errors:
$ python mydict2.py
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 10, in __main__.Dict
Failed example:
d1.x
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'x'
**********************************************************************
File "/Users/michael/Github/learn-python3/samples/debug/mydict2.py", line 16, in __main__.Dict
Failed example:
d2.c
Exception raised:
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'c'
**********************************************************************
1 items had failures:
2 of 9 in __main__.Dict
***Test Failed*** 2 failures.
Note the last three lines of code: doctest is not executed when the module is imported normally—it only runs when the module is executed directly from the command line. You therefore don’t need to worry about doctests running in non-test environments.
Write doctests for the fact(n) function and execute them:
def fact(n):
'''
Calculate 1*2*...*n
>>> fact(1)
1
>>> fact(10)
?
>>> fact(-1)
?
'''
if n < 1:
raise ValueError()
if n == 1:
return 1
return n * fact(n - 1)
if __name__ == '__main__':
import doctest
doctest.testmod()
doctest is extremely useful—it can be used not only for testing but also as example code directly. With certain documentation generation tools, comments containing doctests can be automatically extracted. When users read the documentation, they also see the doctests (ensuring examples are always up-to-date).
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Dict(dict):
"""
Simple dict but also support access as x.y style.
>>> d1 = Dict()
>>> d1['x'] = 100
>>> d1.x
100
>>> d1.y = 200
>>> d1['y']
200
>>> d2 = Dict(a=1, b=2, c='3')
>>> d2.c
'3'
>>> d2['empty']
Traceback (most recent call last):
...
KeyError: 'empty'
>>> d2.empty
Traceback (most recent call last):
...
AttributeError: 'Dict' object has no attribute 'empty'
"""
def __init__(self, **kw):
super(Dict, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Dict' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
if __name__ == "__main__":
import doctest
doctest.testmod()