optparse是标准库里用于分析命令行参数的模块(路径:/usr/lib/python2.x/optparse.py, x86-64版本的系统在lib64下)。使用它,可以让自己的程序方便地获得命令行参数,无需再自己处理了。至少也是让程序更标准化。本文首先简单介绍optparse的使用,详细用法请参照官方文档介绍。接着,详细地分析optparse如何实现这些功能的,optparse的代码结构是什么样子的。
一、 optparse模块使用简介
创建client.py,实现的功能是向server发送一个字符串信息。命令行的参数形式如下:
Usage: client.py [options] message
Options:
-h, --help show this help message and exit
-p PORT, --port=PORT port number which server listens on
-s SERVER_ADDR, --server=SERVER_ADDR
server address
-q, --quiet don't print status message to stdout
client.py的代码内容如下:
1 # -*- coding:utf-8 -*-
2 # 包含中文,必须有第一行的声明。
3 from optparse import OptionParser
4
5 usage = "%prog [options] message"
6 parser = OptionParser(usage) #创建一个OptionParser对象(包含多个Option对象的容器)
7 parser.add_option('-p', '--port', action='store', #在容器对象里,添加一个Option对象
8 type='int', default=7890, dest='port', #选项值放在‘port’里
9 help='port number which server listens on')
10 parser.add_option('-s', '--server', action='store',
11 type='string', dest='server_addr', #选项值放在‘server_addr’里
12 help='server address')
13 parser.add_option('-q', '--quiet', action='store_false',
14 dest='verbose', default=True, #选项值放在‘verbose’里
15 help="don't print status message to stdout")
16 options, args = parser.parse_args() #解析命令行参数,将选项参数值放在options里,参数放在args里
17 print "Server address:", options.server_addr #访问server_addr选项
18 print "Server port :", options.port #访问port选项
19 print "Verbose :", options.verbose #访问verbose选项
20 print "message :", args #访问参数选项
以上短短几行代码就把想要的功能实现了。接下来,让我们看看它是如何做到的。
二、 optparse模块的代码分析。
分析模块的代码,首先,应该要熟悉模块的使用方法,如此才能知道代码为什么要这样做,这个很重要。再者,对于不理解的地方,可以使用import pdb做调试观察。
optparse的代码只有1703行,包含的内容不多,主要的只有Option类和OptionContainer类(容器),这也是最常被使用到的。所以,下面只介绍这两个部分。
1. Option类:
上文,我们调用parser.add_option('-p', '--port', action='store', type='int', default=7890, dest='port', help='port number which server listens on')时,实际上就是创建一个Option类,也就是给程序创建一个选项。那么,一个选项会包含哪些东西呢?如我们调用程序时常见到的,通常有以下几种形式:
“<program> -v -d message" #两个选项-v 和-d分开
”<program> -vd message" #两个选项写在一起
"<program> -f outfile" #使用短选项-f 指定输出文件,选项名和参数用空格隔开
"<program> -foutfile" #选项名和参数连在一起
"<program> --file outfile" #使用长选项--file指定输出文件,选项名和参数用空格隔开
"<program> --file=outfile" “选项名和参数用”=“连接
从上可以看到,一个选项通常包含一个短选项名(如”-f“),一个长选项名(如”--file“),以及跟着一个参数(有些选项不需要),至少需要包含一个短选项名或者长选项名。每个选项其实就是对某一个attribue设置一个value。那么dest='port'就是指明attribute的名称是dest, default=7890指value的默认值为7890,这个value也可以从调用命令的参数行中获取。type=‘int’指value的类型是整数,optparse支持多种类型,主要是string(字符串),int(整数),float(浮点),True, False。help=“..."指输出帮助信息说明该选项的用途。所以一个Option对象,会有以下几个属性值:
- 短选项名称列表
- 长选项名称列表
- dest(attribute名称)
- default(指定默认值)
- type(value的类型)
- help (帮助信息)
- action (通常是store,还有store_const, store_true, store_false。)
下面是Option类的初始化方法:
560 def __init__(self, *opts, **attrs):
#opts接收选项名称参数列表,如['-f', '--file']
#attrs接收dest,default,type这些属性设置参数
563 self._short_opts = [] #短选项名称列表
564 self._long_opts = [] #长选项名称列表
565 opts = self._check_opt_strings(opts) #检查选项名称列表opts,至少应该有一个短选项名,或一个长选项名
566 self._set_opt_strings(opts) #解析opts列表的每个成员是否合法,以'-'或'--'开头,
#否则抛出OptionError异常。最后,将合法的成员分别
#放入对应列表self._shor_opts或者self._long_opts。
567
568 # Set all other attrs (action, type, etc.) from 'attrs' dict
569 self._set_attrs(attrs) #解析attrs字典(包含属性设置参数)
576 for checker in self.CHECK_METHODS: #检查每个属性设置是否合法
577 checker(self)
self._set_attrs(attrs)的代码如下,Option类包含一个属性值列表 ATTRS = ['action', 'type', 'dest', 'default', 'nargs', 'const', 'choices',
'callback', 'callback_args', 'callback_kwargs', 'help', 'metavar']。
609 def _set_attrs(self, attrs):
610 for attr in self.ATTRS: #遍历这个列表的每个attr
611 if attr in attrs: #如果attr也存在于创建Option对象时声明的attrs字典中
612 setattr(self, attr, attrs[attr]) #从字典中取出,并设置为对象的一个属性。
#例如dest='port',则会得到self.dest='port'
613 del attrs[attr] #从attrs字典中弹出
614 else:
615 if attr == 'default':
616 setattr(self, attr, NO_DEFAULT)
617 else:
618 setattr(self, attr, None) #设置这个属性的值为None
619 if attrs: #如果attrs字典不为空,表明含有非法的属性名称,抛出异常。
620 attrs = attrs.keys()
621 attrs.sort()
622 raise OptionError(
623 "invalid keyword arguments: %s" % ", ".join(attrs),
624 self)
CHECK_METHODS是一个回调函数列表,每个回调函数以Option对象为参数,执行一项检查。每个检查是独立的,可以很方便地增加或者修改一个检查。
736 CHECK_METHODS = [_check_action,
737 _check_type,
738 _check_choice,
739 _check_dest,
740 _check_const,
741 _check_nargs,
742 _check_callback]
629 def _check_action(self):
630 if self.action is None:
631 self.action = "store"
632 elif self.action not in self.ACTIONS:
633 raise OptionError("invalid action: %r" % self.action, self)
Option类的其他方法,都是些辅助的方法。比较重要的两个就是process和take_action,这是在解释命令行参数时会被OptionParser(容器对象)调用到的,目的的是检查对应的参数选项的值是否合法,以及对这个参数值应该怎么处理。例如,当一个选项声明自己是‘int’类型的参数,那么它的值就不能是‘string’。如果命令行输入‘-h'选项参数,则take_action会输出帮助信息,并退出。一般的情况下,则是存储参数值,然后执行下一步。
2. OptionParser类
OptionParesr继承于OptionContainer类。整个模块里,也就只有这两个容器类,所以可以简单地认为就是这两个类功能的合集。我们只主要查看我们使用到的两个方法add_option() 和parse_args()。
首先,OptionParser包含的几个主要的属性值:
_short_opt: {string: Optiong}字典对象。一个短选项名(如, "-f")对应一个Option对象。
_long_opt: {string: Option }。一个长选项名(如:"--file")对应一个Option对象。
defaults: {string: andy } 。 键为dest , 值为缺省值。
option_list: 列表。容器所包含的Option对象。
下面是add_option的代码内容:
1007 def add_option(self, *args, **kwargs):
1011 if type(args[0]) in types.StringTypes:
1012 option = self.option_class(*args, **kwargs) #创建一个Option类
1013 elif len(args) == 1 and not kwargs: #传入的参数是一个Option对象的情况,较少用
1014 option = args[0]
1015 if not isinstance(option, Option):
1016 raise TypeError, "not an Option instance: %r" % option
1017 else:
1018 raise TypeError, "invalid arguments"
1019
1020 self._check_conflict(option) #检查新增的参数选项是否和之前的有冲突。
#方法:创建一个空的冲突选项列表。遍历option对象的所有短选项名,
#如果存在于容器对象的_shor_opt字典里,则添加到冲突选项列表。
#同样的方法,处理长选项名。
#最后如果冲突选项列表不空,则表示有冲突,调用冲突处理方法conflict_handler。
1021
1022 self.option_list.append(option) #添加到列表中
1023 option.container = self
1024 for opt in option._short_opts: #对短选项名处理,添加到_short_opt字典中
1025 self._short_opt[opt] = option
1026 for opt in option._long_opts: #对长选项名处理。
1027 self._long_opt[opt] = option
1028
1029 if option.dest is not None: #选项对应的缺省值处理
1030 if option.default is not NO_DEFAULT:
1031 self.defaults[option.dest] = option.default
1032 elif option.dest not in self.defaults:
1033 self.defaults[option.dest] = None
1034
1035 return option
下面是parse_args()的代码:
1367 def parse_args(self, args=None, values=None):
1381 rargs = self._get_args(args) #通常args为None,返回命令行参数列表。(不包含程序名)
#调用sys.argv[1:]
1382 if values is None: #values是一个Value对象,可以认为是一个属性值容器。
#每个选项最终都会对应一个键值对。例如, (’-p‘, dest='port', default=7890)。
#最终变成values.port=7890。这是缺省值。如果命令行参数输入 “-p 8890”,
#则变成values.port=8890。
1383 values = self.get_default_values()
1394 self.rargs = rargs
1395 self.largs = largs = []
1396 self.values = values
1397
1398 try:
1399 stop = self._process_args(largs, rargs, values)#解析命令行参数,主要是处理选项参数,
#如果有选项设置改变了缺省值,最终改变values这个属性值容器的某个属性。
#末尾的参数放在rargs里
1400 except (BadOptionError, OptionValueError), err:
1401 self.error(str(err))
1402
1403 args = largs + rargs
1404 return self.check_values(values, args) #这个方法未作任何处理,最终返回(values, args)
处理的流程很简单。接下来看_process_args()的代码,如下。主要看其如何处理长选项名参数,和短选项名参数。
1419 def _process_args(self, largs, rargs, values):
1429 while rargs: #rargs是命令行参数列表,由命令行参数字符串用空格切割得到。
#遍历每个参数,分别对其进行解析。
1430 arg = rargs[0]
1434 if arg == "--":
1435 del rargs[0]
1436 return
1437 elif arg[0:2] == "--": #以‘--’开头,说明这是一个长选项名参数
1438 # process a single long option (possibly with value(s))
1439 self._process_long_opt(rargs, values)
1440 elif arg[:1] == "-" and len(arg) > 1:#以‘-’开头,说明这是一个短选项名参数
1441 # process a cluster of short options (possibly with
1442 # value(s) for the last one only)
1443 self._process_short_opts(rargs, values)
1444 elif self.allow_interspersed_args:
1445 largs.append(arg)
1446 del rargs[0]
1447 else:
1448 return # stop now, leave this arg in rargs
_process_long_opt()是处理长选项名的方法,代码如下:
1479 def _process_long_opt(self, rargs, values):
1480 arg = rargs.pop(0) #从命令行参数列表rargs弹出
1484 if "=" in arg: #对于“--file=outfile”的形式
1485 (opt, next_arg) = arg.split("=", 1)
1486 rargs.insert(0, next_arg) #将‘outfile’又放入命令行参数中,后面的处理会用到这个。
#等同于,变换成“--file outfile”
1487 had_explicit_value = True #表明这个参数选项,确定跟着一个参数值。
1488 else: #对于“--file outfile”的形式
1489 opt = arg
1490 had_explicit_value = False
1491
1492 opt = self._match_long_opt(opt) #检查这个选项名,是不是我们所定义过的
1493 option = self._long_opt[opt] #取得它所对应的Option对象
1494 if option.takes_value(): #这个选项名后应该跟着一个或者多个参数
1495 nargs = option.nargs #获得这个选项后应该跟着的参数个数,缺省值为1个
1496 if len(rargs) < nargs: #命令行参数列表的个数已经少于需要的个数,抛出异常
1497 if nargs == 1:
1498 self.error(_("%s option requires an argument") % opt)
1499 else:
1500 self.error(_("%s option requires %d arguments")
1501 % (opt, nargs))
1502 elif nargs == 1: #只需要一个参数,直接取出后面的一个参数放在value中
1503 value = rargs.pop(0)
1504 else: #多于一个参数,从后面取出指定数量的参数放在列表value中
1505 value = tuple(rargs[0:nargs])
1506 del rargs[0:nargs]
1507
1508 elif had_explicit_value: #选项后不需要跟着参数,但实际确有参数,抛出异常。
#例如,('--quiet', action=’store_true',),‘quiet'选项不许要参数,
#但是命令行输入’--quiet=yes', 这样是错误的。
1509 self.error(_("%s option does not take a value") % opt)
1510
1511 else:
1512 value = None
1513
1514 option.process(opt, value, values, self) #将这个选项对应的参数值进行处理,并放在values中。
#对于‘-h’选项,则是输出帮助信息,然后退出程序。
_process_short_opts对于短参数名称的处理,跟_process_long_opts()类似。
总结一下,OptionParser处理命令行参数的过程。以我们创建的client.py为例,当输入python client.py -s 10.2.20.99 --quiet “it's nice"时,处理的流程如下:
a. 从sys.argv[:1]获取命令行参数(如:[" -s" , " 10.2.20.99", " --quiet", "It's nice."])
b.取出‘-s', 这是以’-s'开头,调用_process_short_opts(),查找容器对象的_short_opt字典,找到对应的Option对象。没找到则抛出异常。
c.调用option.takes_value()。得到这个Option对象后面应该跟着一个参数,从命令行参数列表取出“10.2.20.99”,放入value中。
d.调用option.process(opt, value, values, self)。将检查value是否合法,然后存在values中,即values.server_addr = "10.2.20.99"。
e.处理下一个"--quiet", 这是以‘--’开头的,调用_process_long_opts(), 查找容器对象的_long_opt字典,找到对应的Option对象。
f. 调用option.takes_value(), 发现这个Option对象后面不需要跟着参数。value=None。
g.调用option.process(opt, value, values, self)。根据这个Option对象的action=‘store_false',即将值设置为False。所以,values.verbose=False(因为dest=’verbose‘)。
h.取出“It’s nice”,这不是以‘-’或‘--’开头的,所以不是一个选项参数。命令行参数处理结束,将“It‘s nice”放在args中返回。
i.从opt,args=parser.parse_args()等到的是一个Value对象opt, args参数列表。在这里args=“it’s nice”, opt.server_addr="10.2.20.99", opt.verbose=False。
总结:
本文只是介绍了两个主要的类:Option和OptionParser,和两个主要的方法add_option()和parse_args()。其余的部分,主要似乎Error异常类的,和Formatter(帮助信息输出格式化)。看别人代码如何实现其主要的功能,这是个简单的过程。但是要想理解作者但是在创建的过程中的想法,这是比较难的。更难的是,把这种想法变成自己的,并加以利用和改进。这是向别人学习的过程,没那么容易。
收获: 1.一直对标准库代码有点恐惧。那得是多么高深的啊,我怎么可能看懂?其实,也没那么难,至少optparse是我能消化的。当然也有很复杂的。首先消除了恐惧的心里,从简单的入手,复杂的以后再解决。2.在看代码的过程中,可以学习很多东西,代码的编写风格,算法思路,很多。这会是一个潜移默化的过程。我会坚持一段时间,每周分析一个简单的标准库模块。下一个是logging模块。
写这个文章花了我5个小时,但我还是觉得,别人不一定能看懂。也许我应该考虑更好的表述方式,流程图,类的描述图。这也是我欠缺的能力。