用什麼庫寫 Python 命令行程序?看這一篇就夠了

做者:HelloGitHub- Prodesire

涉及的示例代碼和歷史文章,已同步更新到 HelloGitHub-Team 倉庫python

1、前言

在近半年的 Python 命令行旅程中,咱們依次學習了 argparsedocoptclickfire 庫的特色和用法,逐步瞭解到 Python 命令行庫的設計哲學與演變。 本文做爲本次旅程的終點,但願從一個更高的視角對這些庫進行橫向對比,總結它們的異同點和使用場景,以期在應對不一樣場景時可以分析利弊,選擇合適的庫爲己所用。git

本系列文章默認使用 Python 3 做爲解釋器進行講解。
若你仍在使用 Python 2,請注意二者之間語法和庫的使用差別哦~
複製代碼

2、設計理念

在討論各個庫的設計理念以前,咱們先設計一個計算器程序,其實這個例子在 argparse 庫的第一篇講解中出現過,也就是:github

  • 命令行程序接受一個位置參數,它能出現屢次,且是數字
  • 默認狀況下,命令行程序會求出給定的一串數字的最大值
  • 若是指定了選項參數 --sum,那麼就會將求出給定的一串數字的和

但願從各個庫實現該例子的代碼中能進一步體會它們的設計理念。函數

2.一、argparse

argparse 的設計理念就是提供給你最細粒度的控制,你須要詳細地告訴它參數是選項參數仍是位置參數、參數值的類型是什麼、該參數的處理動做是怎樣的。 總之,它就像是一個沒有智能分析能力的初代機器人,你須要告訴它明確的信息,它纔會根據給定的信息去幫助你作事情。工具

如下示例爲 argparse 實現的 計算器程序學習

import argparse

# 1. 設置解析器
parser = argparse.ArgumentParser(description='Calculator Program.')

# 2. 定義參數
# 添加位置參數 nums,在幫助信息中顯示爲 num
# 其類型爲 int,且支持輸入多個,且至少須要提供一個
parser.add_argument('nums',  metavar='num', type=int, nargs='+',
                    help='a num for the accumulator')
# 添加選項參數 --sum,該參數被 parser 解析後所對應的屬性名爲 accumulate
# 若不提供 --sum,默認值爲 max 函數,不然爲 sum 函數
parser.add_argument('--sum', dest='accumulate', action='store_const',
                    const=sum, default=max,
                    help='sum the nums (default: find the max)')


# 3. 解析參數
args = parser.parse_args(['--sum', '1', '2', '3'])
print(args) # 結果:Namespace(accumulate=<built-in function sum>, nums=[1, 2, 3])

# 4. 業務邏輯
result = args.accumulate(args.nums)
print(result)  # 基於上文的 ['--sum', '1', '2', '3'] 參數,accumulate 爲 sum 函數,其結果爲 6
複製代碼

從上述示例能夠看到,咱們須要經過 add_argument 很明確地告訴 argparse 參數長什麼樣:ui

  • 它是位置參數 nums,仍是選項參數 --sum
  • 它的類型是什麼,好比 type=int 表示類型是 int
  • 這個參數能重複出現幾回,好比 nargs='+' 表示至少提供 1 個
  • 參數的是存什麼的,好比 action='store_const' 表示存常量

而後它才根據給定的這些元信息來解析命令行參數(也就是示例中的 ['--sum', '1', '2', '3'])。spa

這是很計算機的思惟,雖然冗長,但也帶來了靈活性。命令行

2.二、docopt

argparse 的理念能夠看出,它是命令式的。這時候 docopt 另闢蹊徑,聲明式是否是也能夠?一個命令行程序的幫助信息其實已然包含了這個命令行的完整元信息,那不就能夠經過定義幫助信息來定義命令行?docopt 就是基於這樣的想法去設計的。設計

聲明式的好處在於只要你掌握了聲明式的語法,那麼定義命令行的元信息就會很簡單。

如下示例爲 docopt 實現的 計算器程序

# 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. 業務邏輯
nums = (int(num) for num in arguments['<num>'])

if arguments['--sum']:
    result = sum(nums)
else:
    result = max(nums)

print(result) # 基於上文的 ['--sum', '1', '2', '3'] 參數,處理函數爲 sum 函數,其結果爲 6
複製代碼

從上述示例能夠看到,咱們經過 __doc__ 定義了接口描述,這和 argparseadd_argument 是等價的,而後 docopt 便會根據這個元信息把命令行參數轉換爲一個字典。業務邏輯中就須要對這個字典進行處理。

對比與 argparse

  • 對於更爲複雜的命令程序,元信息的定義上 docopt 會更加簡單
  • 然而在業務邏輯的處理上,因爲 argparse 在一些簡單參數的處理上會更加便捷(好比示例中的情形),相對來講 docopt 轉換爲字典後就把全部處理交給業務邏輯的方式會更加複雜

2.三、click

命令行程序本質上是定義參數和處理參數,而處理參數的邏輯必定是與所定義的參數有關聯的。那可不能夠用函數和裝飾器來實現處理參數邏輯與定義參數的關聯呢?而 click 正好就是以這種使用方式來設計的。

click 使用裝飾器的好處就在於用裝飾器優雅的語法將參數定義和處理邏輯整合在一塊兒,從而暗示了路由關係。相比於 argparsedocopt 須要自行對解析後的參數來作路由關係,簡單了很多。

如下示例爲 click 實現的 計算器程序

import sys
import click

sys.argv = ['calculator.py', '--sum', '1', '2', '3']

# 2. 定義參數
@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. 業務邏輯
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'] 參數,處理函數爲 sum 函數,其結果爲 6

calculator()
複製代碼

從上述示例能夠看出,參數和對應的處理邏輯很是好地綁定在了一塊兒,看上去就很直觀,使得咱們能夠明確瞭解參數會怎麼處理,這在有大量參數時顯得尤其重要,這邊是 click 相比於 argparsedocopt 最明顯的優點。

此外,click 還內置了不少實用工具和額外能力,好比說 Bash 補全、顏色、分頁支持、進度條等諸多實用功能,可謂是如虎添翼。

2.四、fire

fire 則是用一種面向廣義對象的方式來玩轉命令行,這種對象能夠是類、函數、字典、列表等,它更加靈活,也更加簡單。你都不須要定義參數類型,fire 會根據輸入和參數默認值來自動判斷,這無疑進一步簡化了實現過程。

如下示例爲 click 實現的 計算器程序

import sys
import fire

sys.argv = ['calculator.py', '1', '2', '3', '--sum']

builtin_sum = sum

# 1. 業務邏輯
# sum=False,暗示它是一個選項參數 --sum,不提供的時候爲 False
# *nums 暗示它是一個能提供任意數量的位置參數
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'] 參數,處理函數爲 sum 函數,其結果爲 6


fire.Fire(calculator)
複製代碼

從上述示例能夠看出,fire 提供的方式無疑是最簡單、而且最 Pythonic 的了。咱們只需關注業務邏輯,而命令行參數的定義則和函數參數的定義融爲了一體。

不過,有利天然也有弊,好比 nums 並無說是什麼類型,也就意味着輸入字符串'abc'也是合法的,這就意味着一個嚴格的命令行程序必須在本身的業務邏輯中來對指望的類型進行約束。

3、橫向對比

最後,咱們橫向對比下argparsedocoptclickfire 庫的各項功能和特色:

Python 的命令行庫種類繁多、各具特點。結合上面的總結,能夠選擇出符合使用場景的庫,若是幾個庫都符合,那麼就根據你更偏心的風格來選擇。這些庫都很優秀,其背後的思想非常值得咱們學習和擴展。


關注 HelloGitHub 公衆號獲取第一手的更新

相關文章
相關標籤/搜索