用什么庫寫 Python 命令行程序?看這一篇就夠了
一、前言
在近半年的 Python 命令行旅程中,我們依次學習了 argparse、docopt、click 和 fire 庫的特點和用法,逐步了解到 Python 命令行庫的設計哲學與演變。本文作為本次旅程的終點,希望從一個更高的視角對這些庫進行橫向?qū)Ρ?,總結它們的異同點和使用場景,以期在應對不同場景時能夠分析利弊,選擇合適的庫為己所用。
本系列文章默認使用 Python 3 作為解釋器進行講解。若你仍在使用 Python 2,請注意兩者之間語法和庫的使用差異哦~
二、設計理念
在討論各個庫的設計理念之前,我們先設計一個計算器程序,其實這個例子在 argparse 庫的第一篇講解中出現(xiàn)過,也就是:
- 命令行程序接受一個位置參數(shù),它能出現(xiàn)多次,且是數(shù)字
- 默認情況下,命令行程序會求出給定的一串數(shù)字的最大值
- 如果指定了選項參數(shù) --sum,那么就會將求出給定的一串數(shù)字的和
希望從各個庫實現(xiàn)該例子的代碼中能進一步體會它們的設計理念。
2.1、argparse
argparse 的設計理念就是提供給你最細粒度的控制,你需要詳細地告訴它參數(shù)是選項參數(shù)還是位置參數(shù)、參數(shù)值的類型是什么、該參數(shù)的處理動作是怎樣的??傊拖袷且粋€沒有智能分析能力的初代機器人,你需要告訴它明確的信息,它才會根據(jù)給定的信息去幫助你做事情。
以下示例為 argparse 實現(xiàn)的 計算器程序:
- import argparse
- # 1. 設置解析器
- parser = argparse.ArgumentParser(description='Calculator Program.')
- # 2. 定義參數(shù)
- # 添加位置參數(shù) nums,在幫助信息中顯示為 num
- # 其類型為 int,且支持輸入多個,且至少需要提供一個
- parser.add_argument('nums', metavar='num', type=int, nargs='+',
- help='a num for the accumulator')
- # 添加選項參數(shù) --sum,該參數(shù)被 parser 解析后所對應的屬性名為 accumulate
- # 若不提供 --sum,默認值為 max 函數(shù),否則為 sum 函數(shù)
- parser.add_argument('--sum', dest='accumulate', action='store_const',
- const=sum, default=max,
- help='sum the nums (default: find the max)')
- # 3. 解析參數(shù)
- args = parser.parse_args(['--sum', '1', '2', '3'])
- print(args) # 結果:Namespace(accumulate=<built-in function sum>, nums=[1, 2, 3])
- # 4. 業(yè)務邏輯
- result = args.accumulate(args.nums)
- print(result) # 基于上文的 ['--sum', '1', '2', '3'] 參數(shù),accumulate 為 sum 函數(shù),其結果為 6
從上述示例可以看到,我們需要通過 add_argument 很明確地告訴 argparse 參數(shù)長什么樣:
- 它是位置參數(shù) nums,還是選項參數(shù) --sum
- 它的類型是什么,比如 type=int 表示類型是 int
- 這個參數(shù)能重復出現(xiàn)幾次,比如 nargs='+' 表示至少提供 1 個
- 參數(shù)的是存什么的,比如 action='store_const' 表示存常量
然后它才根據(jù)給定的這些元信息來解析命令行參數(shù)(也就是示例中的 ['--sum', '1', '2', '3'])。
這是很計算機的思維,雖然冗長,但也帶來了靈活性。
2.2、docopt
從 argparse 的理念可以看出,它是命令式的。這時候 docopt 另辟蹊徑,聲明式是不是也可以?一個命令行程序的幫助信息其實已然包含了這個命令行的完整元信息,那不就可以通過定義幫助信息來定義命令行?docopt 就是基于這樣的想法去設計的。
聲明式的好處在于只要你掌握了聲明式的語法,那么定義命令行的元信息就會很簡單。
以下示例為 docopt 實現(xiàn)的 計算器程序:
- # 1. 定義接口描述/幫助信息
- """Calculator Program.
- Usage:
- calculator.py [--sum] <num>...
- calculator.py (-h | --help)
- Options:
- -h --help Show help.
- --sum Sum the nums (default: find the max).
- """
- from docopt import docopt
- # 2. 解析命令行
- arguments = docopt(__doc__, options_first=True, argv=['--sum', '1', '2', '3'])
- print(arguments) # 結果:{'--help': False, '--sum': True, '<num>': ['1', '2', '3']}
- # 3. 業(yè)務邏輯
- nums = (int(num) for num in arguments['<num>'])
- if arguments['--sum']:
- result = sum(nums)
- else:
- result = max(nums)
- print(result) # 基于上文的 ['--sum', '1', '2', '3'] 參數(shù),處理函數(shù)為 sum 函數(shù),其結果為 6
從上述示例可以看到,我們通過 __doc__ 定義了接口描述,這和 argparse 中 add_argument是等價的,然后 docopt 便會根據(jù)這個元信息把命令行參數(shù)轉換為一個字典。業(yè)務邏輯中就需要對這個字典進行處理。
對比與 argparse:
- 對于更為復雜的命令程序,元信息的定義上 docopt 會更加簡單
- 然而在業(yè)務邏輯的處理上,由于 argparse 在一些簡單參數(shù)的處理上會更加便捷(比如示例中的情形),相對來說 docopt 轉換為字典后就把所有處理交給業(yè)務邏輯的方式會更加復雜
2.3、click
命令行程序本質(zhì)上是定義參數(shù)和處理參數(shù),而處理參數(shù)的邏輯一定是與所定義的參數(shù)有關聯(lián)的。那可不可以用函數(shù)和裝飾器來實現(xiàn)處理參數(shù)邏輯與定義參數(shù)的關聯(lián)呢?而 click 正好就是以這種使用方式來設計的。
click 使用裝飾器的好處就在于用裝飾器優(yōu)雅的語法將參數(shù)定義和處理邏輯整合在一起,從而暗示了路由關系。相比于 argparse 和 docopt 需要自行對解析后的參數(shù)來做路由關系,簡單了不少。
以下示例為 click 實現(xiàn)的 計算器程序:
- import sys
- import click
- sys.argv = ['calculator.py', '--sum', '1', '2', '3']
- # 2. 定義參數(shù)
- @click.command()
- @click.argument('nums', nargs=-1, type=int)
- @click.option('--sum', 'use_sum', is_flag=True, help='sum the nums (default: find the max)')
- # 1. 業(yè)務邏輯
- def calculator(nums, use_sum):
- """Calculator Program."""
- print(nums, use_sum) # 輸出:(1, 2, 3) True
- if use_sum:
- result = sum(nums)
- else:
- result = max(nums)
- print(result) # 基于上文的 ['--sum', '1', '2', '3'] 參數(shù),處理函數(shù)為 sum 函數(shù),其結果為 6
- calculator()
從上述示例可以看出,參數(shù)和對應的處理邏輯非常好地綁定在了一起,看上去就很直觀,使得我們可以明確了解參數(shù)會怎么處理,這在有大量參數(shù)時顯得尤為重要,這邊是 click 相比于 argparse 和 docopt 最明顯的優(yōu)勢。
此外,click 還內(nèi)置了很多實用工具和額外能力,比如說 Bash 補全、顏色、分頁支持、進度條等諸多實用功能,可謂是如虎添翼。
2.4、firefire
則是用一種面向廣義對象的方式來玩轉命令行,這種對象可以是類、函數(shù)、字典、列表等,它更加靈活,也更加簡單。你都不需要定義參數(shù)類型,fire 會根據(jù)輸入和參數(shù)默認值來自動判斷,這無疑進一步簡化了實現(xiàn)過程。
以下示例為 fire 實現(xiàn)的 計算器程序:
- import sys
- import fire
- sys.argv = ['calculator.py', '1', '2', '3', '--sum']
- builtin_sum = sum
- # 1. 業(yè)務邏輯
- # sum=False,暗示它是一個選項參數(shù) --sum,不提供的時候為 False
- # *nums 暗示它是一個能提供任意數(shù)量的位置參數(shù)
- def calculator(sum=False, *nums):
- """Calculator Program."""
- print(sum, nums) # 輸出:True (1, 2, 3)
- if sum:
- result = builtin_sum(nums)
- else:
- result = max(nums)
- print(result) # 基于上文的 ['1', '2', '3', '--sum'] 參數(shù),處理函數(shù)為 sum 函數(shù),其結果為 6
- fire.Fire(calculator)
從上述示例可以看出,fire 提供的方式無疑是最簡單、并且最 Pythonic 的了。我們只需關注業(yè)務邏輯,而命令行參數(shù)的定義則和函數(shù)參數(shù)的定義融為了一體。
不過,有利自然也有弊,比如 nums 并沒有說是什么類型,也就意味著輸入字符串'abc'也是合法的,這就意味著一個嚴格的命令行程序必須在自己的業(yè)務邏輯中來對期望的類型進行約束。
三、橫向?qū)Ρ?/h3>
最后,我們橫向?qū)Ρ认耡rgparse、docopt、click 和 fire 庫的各項功能和特點:
Python 的命令行庫種類繁多、各具特色。結合上面的總結,可以選擇出符合使用場景的庫,如果幾個庫都符合,那么就根據(jù)你更偏愛的風格來選擇。這些庫都很優(yōu)秀,其背后的思想很是值得我們學習和擴展。