This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
The examples on using command-line arguments so far require the user
of the program to type all arguments in their right sequence, just as
when calling a function with positional arguments in the right
order. It would be very convenient to assign command-line arguments in
the same way as we use keyword arguments. That is, arguments are
associated with a name, their sequence can be arbitrary, and only the
arguments where the default value is not appropriate need to be given.
Such type of command-line arguments may have --option value
pairs,
where option
is some name of the argument.
As usual, we shall use an example to illustrate how to work with
--option value
pairs. Consider the physics formula for the location
\( s(t) \) of an object at time \( t \), given that the object started at \( s=s_0 \) at
\( t=0 \) with a velocity \( v_0 \), and thereafter was subject to a constant
acceleration \( a \):
$$
\begin{equation}
s(t) = s_0 + v_0t + \frac{1}{2}at^2\tp
\tag{1}
\end{equation}
$$
This formula
requires four input variables: \( s_0 \), \( v_0 \), \( a \), and \( t \).
We can make a program location.py
that takes four options,
--s0
,
--v0
,
--a
, and
--t
, on the command line.
The program is typically run like this:
location.py --t 3 --s0 1 --v0 1 --a 0.5
The sequence of --option value
pairs is arbitrary. All options have
a default value such that one does not have to specify all options
on the command line.
All input variables should have sensible default values such that we can leave out the options for which the default value is suitable. For example, if \( s_0=0 \), \( v_0=0 \), \( a=1 \), and \( t=1 \) by default, and we only want to change \( t \), we can run
location.py --t 3
Python has a flexible and powerful module argparse
for reading
(parsing) --option value
pairs on the command line. Using
argparse
consists of three steps. First, a parser object must be
created:
import argparse
parser = argparse.ArgumentParser()
Second, we need to define the various command-line options,
parser.add_argument('--v0', '--initial_velocity', type=float,
default=0.0, help='initial velocity',
metavar='v')
parser.add_argument('--s0', '--initial_position', type=float,
default=0.0, help='initial position',
metavar='s')
parser.add_argument('--a', '--acceleration', type=float,
default=1., help='acceleration', metavar='a')
parser.add_argument('--t', '--time', type=float,
default=1.0, help='time', metavar='t')
The first arguments to parser.add_argument
is the set of options
that we want to associate with an input parameter. Optional arguments
are the type, a default value, a help string, and a name for the value
of the argument (metavar
) in a usage string. The argparse
module
will automatically allow an option -h
or --help
that prints a
usage string for all the registered options. By default, the type is
str
, the default value is None
, the help string is empty, and
metavar
is the option in upper case without initial dashes.
Third, we must read the command line arguments and interpret them:
args = parser.parse_args()
Through the args
object we can extract the values of the
various registered parameters: args.v0
, args.s0
, args.a
,
and args.t
. The name of the parameter is determined by
the first option to parser.add_argument
, so writing
parser.add_argument('--initial_velocity', '--v0', type=float,
default=0.0, help='initial velocity')
will make the initial velocity value appear as args.initial_velocity
.
We can add the dest
keyword to explicitly specify the name where
the value is stored:
parser.add_argument('--initial_velocity', '--v0', dest='V0',
type=float, default=0.0,
help='initial velocity')
Now, args.V0
will retrieve the value of the initial velocity.
In case we do not provide any default value, the value will be None
.
Our example is completed either by evaluating s
as
s = args.s0 + args.v0*t + 0.5*args.a*args.t**2
or by introducing new variables so that the formula aligns better with the mathematical notation:
s0 = args.s0; v0 = args.v0; a = args.a; t = args.t
s = s0 + v0*t + 0.5*a*t**2
A complete program for the example above is
found in the file location.py.
Try to run it with the -h
option to see
an automatically generated explanation of legal command-line options.
Values on the command line involving mathematical symbols and
functions, say --v0 'pi/2'
, pose a problem with the code example
above. The argparse
module will in that case try to do
float('pi/2')
which does not work well since pi
is an undefined
name. Changing type=float
to type=eval
is required to interpret
the expression pi/2
, but even eval('pi/2')
fails since pi
is not
defined inside the argparse
module. There are various remedies for
this problem.
One can write a tailored function for converting a string value given on the command line to the desired object. For example,
def evalcmlarg(text):
return eval(text)
parser.add_argument('--s0', '--initial_position', type=evalcmlarg,
default=0.0, help='initial position')
The file location_v2.py
demonstrates such explicit type conversion through a user-provided
conversion function.
Note that eval
is now taken in the programmer's namespace where
(hopefully) pi
or other symbols are imported.
More sophisticated conversions are possible. Say \( s_0 \) is specified in
terms of a function of some parameter \( p \), like \( s_0=(1-p^2) \). We
could then use a string for --s0
and the StringFunction
tool from
the section Turning string expressions into functions to turn the string into a
function:
def toStringFunction4s0(text):
from scitools.std import StringFunction
return StringFunction(text, independent_variable='p')
parser.add_argument('--s0', '--initial_position',
type=toStringFunction4s0,
default='0.0', help='initial position')
Giving a command-line argument --s0 'exp(-1.5) + 10(1-p**2)
results in args.s0
being a StringFunction
object, which we
must evaluate for a p
value:
s0 = args.s0
p = 0.05
...
s = s0(p) + v0*t + 0.5*a*t**2
The file location_v3.py contains the complete code for this example.
Another alternative is to perform the correct conversion of values in
our own code after the parser
object has read the values. To this
end, we treat argument types as strings in the parser.add_argument
calls, meaning that we replace type=float
by set type=str
(which
is also the default choice of type
). Recall that this approach requires
specification of default values as strings too, say '0'
:
parser.add_argument('--s0', '--initial_position', type=str,
default='0', help='initial position')
...
from math import *
args.v0 = eval(args.v0)
# or
v0 = eval(args.v0)
s0 = StringFunction(args.s0, independent_variable='p')
p = 0.5
...
s = s0(p) + v0*t + 0.5*a*t**2
Such code is found in the file location_v4.py. You can try out that program with
the command-line arguments --s0 'pi/2 + sqrt(p)' --v0 pi/4'
.
The final alternative is to write an Action
class to handle the
conversion from string to the right type. This is the preferred way to
perform conversions and well described in the argparse
documentation. We shall exemplify it here, but the technicalities
involved require understanding of classes (see the document
Introduction to classes in Python
[1]) and inheritance (see the document
Object-oriented programming
[2]). For the conversion from string to any
object via eval
we write
import argparse
from math import *
class ActionEval(argparse.Action):
def __call__(self, parser, namespace, values,
option_string=None):
setattr(namespace, self.dest, eval(values))
The command-line arguments supposed to be run through eval
must
then have an action
parameter:
parser.add_argument('--v0', '--initial_velocity',
default=0.0, help='initial velocity',
action=ActionEval)
From string to function via StringFunction
for the
--s0
argument we write
from scitools.std import StringFunction
class ActionStringFunction4s0(argparse.Action):
def __call__(self, parser, namespace, values,
option_string=None):
setattr(namespace, self.dest,
StringFunction(values, independent_variable='p'))
A complete code appears in the file location_v5.py.