Python 命令行库的大乱
當(dāng)你想實現(xiàn)一個命令行程序時,或許第一個想到的是用 Python 來實現(xiàn)。比如 CentOS 上大名鼎鼎的包管理工具?yum?就是基于 Python 實現(xiàn)的。
而 Python 的世界中有很多命令行庫,每個庫都各具特色。但我們往往不知道其背后的設(shè)計理念,也因此在選擇時感到迷茫。這些庫的作者為何在重復(fù)造輪子,他是從哪個角度來考慮,來讓命令行庫“演變”到一個新的更好用的形態(tài)。
為了能夠更加直觀地感受到命令行庫的設(shè)計理念,在此之前,我們不妨設(shè)計一個名為?calc?的命令行程序,它能:
支持?echo?子命令,對輸入的字符串做處理來輸出
若不提供任何選項,則輸出原始內(nèi)容
若提供?--lower?選項,則輸出小寫字符串
若提供?--upper?選項,則輸出大寫字符串
支持?eval?子命令,針對輸入調(diào)用 Python 的?eval?函數(shù),將結(jié)果輸出(作為示例,我們不考慮安全性問題)
argparse
argparse?作為 Python 的標(biāo)準(zhǔn)庫,可能會是你想到第一個命令行庫。
argparse?的設(shè)計理念就是提供給開發(fā)者最細粒度的控制。換句話說,你需要告訴它必不可少的細節(jié),比如參數(shù)的類型是什么,處理參數(shù)的動作是怎樣的。
在?argparse?的世界中,需要:
設(shè)置解析器,作為后續(xù)定義參數(shù)和解析命令行的基礎(chǔ)。如果要實現(xiàn)子命令,則還要設(shè)置子解析器。
定義參數(shù),包括名稱、類型、動作、幫助等。其中的動作是指對于此參數(shù)的初步處理,是直接存下來,還是作為布爾值,亦或是追加到列表中等等
解析參數(shù)
根據(jù)參數(shù)編寫業(yè)務(wù)邏輯
以下示例是基于?argparse?的?calc?程序:
import argparse
def echo_text(args):
if args.lower:
print(args.text.lower())
elif args.upper:
print(args.text.upper())
else:
print(args.text)
def eval_expression(args):
print(eval(args.expression))
1. 設(shè)置解析器
parser = argparse.ArgumentParser(description=‘Calculator Program.’)
subparsers = parser.add_subparsers()
2. 定義參數(shù)
2.1 echo 子命令
echo 子解析器
echo_parser = subparsers.add_parser(
‘echo’, help=‘Echo input text in multiple forms’)
添加位置參數(shù) text
echo_parser.add_argument(‘text’, help=‘Input text’)
–lower/–upper 互斥,需要設(shè)置互斥組
echo_group = echo_parser.add_mutually_exclusive_group()
添加選項參數(shù) --lower/–upper,這里action的作用就是將之變?yōu)椴紶栕兞?/h1>
echo_parser.add_argument(’–lower’, action=‘store_true’, help=‘Lower input text’)
echo_parser.add_argument(’–upper’, action=‘store_true’, help=‘Upper input text’)
設(shè)置此命令的處理函數(shù)
echo_parser.set_defaults(handle=echo_text)
eval 子解析器
eval_parser = subparsers.add_parser(
‘eval’, help=‘Eval input expression and return result’)
添加位置參數(shù) expression
eval_parser.add_argument(‘expression’, help=‘Expression to eval’)
設(shè)置此命令的處理函數(shù)
eval_parser.set_defaults(handle=eval_expression)
3. 解析參數(shù)
args = parser.parse_args([‘echo’, ‘–upper’, ‘Hello, World’])
print(args) # 結(jié)果:Namespace(lower=True, text=‘Hello, World’, upper=False)
args = parser.parse_args([‘eval’, ‘1+2*3’])
print(args) # 結(jié)果:Namespace(expression=‘1+2*3’)
4. 業(yè)務(wù)邏輯處理
args.handle(args)
從上述示例可以看到,要實現(xiàn)子命令,對應(yīng)地需要添加子解析器。然后最為關(guān)鍵的就是要定義參數(shù),需要通過?add_argument?很明確地告訴?argparse?參數(shù)長什么樣,需要怎么處理:
它是位置參數(shù)?text/expression,還是選項參數(shù)?--lower/–upper
若是選項參數(shù),是否互斥
參數(shù)的是存成什么形式,比如?action=‘store_true’?表示存成布爾
子命令的響應(yīng)函數(shù)
通過?argparse?實現(xiàn)的整個過程是很計算機思維的,且比較冗長。其優(yōu)點是靈活,所有的功能都涵蓋到了;但缺點則是將定義和處理割裂,尤其在程序功能復(fù)雜時會愈加凌亂和不直觀,難以理解和維護。
docopt
有人喜歡?argparse?這樣命令式的寫法,就會有人喜歡聲明式的寫法。而?docopt?恰巧這就是這樣一個命令行庫。設(shè)計它的初衷就是對于熟悉命令行程序幫助信息的開發(fā)者來說,直接通過編寫幫助信息來描述整個命令行參數(shù)定義的元信息會是更加簡單快捷的方式。這種聲明式的語法描述某種程度上會比過程式地定義參數(shù)來的更加簡單和直觀。
在?docopt?的世界中,需要:
定義接口描述/幫助信息,這一步是它的特色和重點
解析參數(shù),獲得一個字典
根據(jù)參數(shù)編寫業(yè)務(wù)邏輯
以下示例是基于?docopt?的?calc?程序:
1. 定義接口描述/幫助信息
“”"Calculator Program.
Usage:
calc echo [–lower | --upper]
calc eval
Commands:
echo Echo input text in multiple forms
eval Eval input expression and return result
Options:
-h --help Show help
–lower Lower input text
–upper Upper input text
“”"
from docopt import docopt
def echo_text(args):
if args[’–lower’]:
print(args[’’].lower())
elif args[’–upper’]:
print(args[’’].upper())
else:
print(args[’’])
def eval_expression(args):
print(eval(args[’’]))
2. 解析命令行
args = docopt(doc, argv=[‘echo’, ‘–upper’, ‘Hello, World’])
結(jié)果:{’–lower’: False, ‘–upper’: True, ‘’: None, ‘’: ‘Hello, World’, ‘echo’: True, ‘eval’: False}
print(args)
3. 業(yè)務(wù)邏輯
if args[‘echo’]:
echo_text(args)
elif args[‘eval’]:
eval_expression(args)
從上述示例可以看到,我們通過文檔字符串?doc?定義了接口描述,這和?argparse?中 一系列參數(shù)定義的行為是等價的,然后?docopt?便會根據(jù)這個元信息把命令行參數(shù)轉(zhuǎn)換為一個字典。業(yè)務(wù)邏輯中就需要對這個字典進行處理。
相比于?argparse:
對于較為復(fù)雜的命令,命令和參數(shù)元信息的定義上?docopt?會更加簡單
在業(yè)務(wù)邏輯的處理上,argparse?在一些簡單參數(shù)的處理上會更加便捷,且命令和處理函數(shù)之間可以方便路由(比如示例中的情形);相對來說?docopt?轉(zhuǎn)換為字典后就把所有處理交給業(yè)務(wù)邏輯的方式會更加復(fù)雜
click
不論是?argparse?還是?docopt,元信息的定義和處理都是割裂開的。而命令行程序本質(zhì)上是定義參數(shù)并對參數(shù)進行處理,而處理參數(shù)的邏輯一定是與所定義的參數(shù)有關(guān)聯(lián)的。那可不可以用函數(shù)和裝飾器來實現(xiàn)處理參數(shù)邏輯與定義參數(shù)的關(guān)聯(lián)呢?click?正好就是以這種使用方式來設(shè)計的。
裝飾器這樣一個優(yōu)雅的語法糖是元信息定義和處理邏輯之間的絕妙膠水,從而暗示了兩者的路有關(guān)系。對比于前兩個命令行庫的路由實現(xiàn)著實優(yōu)雅了不少。
在?click?的世界中:
通過裝飾器定義命令和參數(shù)的元信息
使用此裝飾器裝飾處理函數(shù)
對,就是這么簡單。
以下示例是基于?click?的?calc?程序:
import sys
import click
sys.argv = [‘calc’, ‘echo’, ‘–upper’, ‘Hello, World’]
@click.group(help=‘Calculator Program.’)
def cli():
pass
2. 定義參數(shù)
@cli.command(name=‘echo’, help=‘Echo input text in multiple forms’)
@click.argument(‘text’)
@click.option(’–lower’, is_flag=True, help=‘Lower input text’)
@click.option(’–upper’, is_flag=True, help=‘Upper input text’)
1. 業(yè)務(wù)邏輯
def echo_text(text, lower, upper):
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
@cli.command(name=‘eval’, help=‘Eval input expression and return result’)
@click.argument(‘expression’)
def eval_expression(expression):
print(eval(expression))
cli()
從上述示例可以看到,元信息定義和處理邏輯無縫綁定在一起,能夠直觀地看出對應(yīng)的參數(shù)會如何處理,這個優(yōu)勢在有大量參數(shù)需要處理時顯得尤為突出。在處理函數(shù)中,接收到不再是像?argparse?或?docopt?中的一個包含所有參數(shù)的變量,而是具體的參數(shù)變量,這讓處理邏輯在參數(shù)使用上也變得更加簡便。
此外,click?還內(nèi)置了很多實用工具和增強能力,如參數(shù)自動補全、分頁支持、顏色、進度條等功能,能夠有效提升開發(fā)效率。
fire
雖然前面三個庫已經(jīng)足夠強大,但是仍然會有人認為不夠簡單。是否還有進一步簡化的空間呢?如果只是定義函數(shù),是否能讓框架推測出參數(shù)元信息呢?理論上還真是可以。
fire?用一種面向廣義對象的方式來玩轉(zhuǎn)命令行,這種對象可以是類、函數(shù)、字典、列表等,它更加靈活,也更加簡單。你都不需要定義參數(shù)類型,fire?會根據(jù)輸入和參數(shù)默認值來自動判斷,這無疑進一步簡化了實現(xiàn)過程。
在?fire?的世界中,定義 Python 對象就夠了。
以下示例是基于?fire?的?calc?程序:
import sys
import fire
sys.argv = [‘calc’, ‘echo’, ‘“Hello, World”’, ‘–upper’]
業(yè)務(wù)邏輯
類中有幾個方法,就意味著命令行程序有幾個同名命令
class Calc:
# text 沒有任何默認值,視為位置參數(shù)
# lower/upper 有布爾類型的默認值,視為選項參數(shù) --lower/–upper,
# 且指定了為 True,不指定 False
def echo(self, text, lower=False, upper=False):
“”“Echo input text in multiple forms”""
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
fire.Fire(Calc)
從上面的示例可以看出,使用?fire?足夠的簡單,一切都是根據(jù)約定來進行推斷,包括支持哪些命令,每個命令接受的什么參數(shù)和選項。這種方式可以說是足夠的 Pythonic,相比于?click,fire?把命令行參數(shù)的定義和函數(shù)參數(shù)的定義融為了一體。通過它,我們真的就只用關(guān)注業(yè)務(wù)邏輯。
不過簡單往往也意味著對于復(fù)雜需求的捉襟見肘。僅僅通過默認值來推導(dǎo)命令行參數(shù)所能表達的情況是有限的,比如互斥選項、位置參數(shù)的類型限定都無法通過框架來表達,而只能由業(yè)務(wù)邏輯去判斷。
typer
那么該如何在保持像?fire?這樣簡單實現(xiàn)的方式下,增強參數(shù)元信息的表達能力呢?既然默認參數(shù)的能力有限,那么如果使用 Python 3 的類型注解呢?
typer?站在?click?巨人的肩膀上,借助 Python 3 類型注解的特性,既滿足了簡單直觀編寫的需要,又達到了應(yīng)對復(fù)雜場景的目的,可謂是現(xiàn)代化的命令行庫。
在?typer?的世界中,也是直接編寫業(yè)務(wù)邏輯,和?fire?稍稍不同的點是使用了類型注解和默認值來表達參數(shù)元信息定義。
以下示例是基于?typer?的?calc?程序:
import sys
import typer
sys.argv = [‘calc’, ‘echo’, ‘“Hello, World”’, ‘–upper’]
cli = typer.Typer(help=‘Calculator Program.’)
定義命令 echo,及處理函數(shù)
text 無默認值,視為位置參數(shù),類型為字符串
lower/upper 類型為 bool,默認值為 False,視為選項 --lower/–upper,
且指定了為 True,不指定 False
@cli.command(name=‘echo’)
def echo_text(text: str, lower: bool = False, upper: bool = False):
“”“Echo input text in multiple forms”""
if lower:
print(text.lower())
elif upper:
print(text.upper())
else:
print(text)
定義命令 eval,及處理函數(shù)
expression 無默認值,視為位置參數(shù),類型為字符串
@cli.command(name=‘eval’)
def eval_expression(expression: str):
“”“Eval input expression and return result”""
print(eval(expression))
cli()
從上面的示例可以看出,相比于?click,它免去了參數(shù)元信息的繁瑣定義,取而代之的是類型注解;相比于?fire,它的元信息定義能力則大大增強,可以通過指定默認值為?typer.Option?或?typer.Argument?來進一步擴展參數(shù)和選項的語義。可以說是,typer?達到了簡單與靈活的完美平衡。
橫向?qū)Ρ?br /> 最后,我們橫向?qū)Ρ认?argparse、docopt、click、fire、typer?庫的各項功能和特點:
argpase docopt click fire typer
使用步驟數(shù) 4 步 3 步 2 步 1 步 1 步
使用步驟數(shù) 1. 設(shè)置解析器
2. 定義參數(shù)
3. 解析命令行
4. 業(yè)務(wù)邏輯 1. 定義接口描述
2. 解析命令行
3. 業(yè)務(wù)邏輯 1. 業(yè)務(wù)邏輯
2. 定義參數(shù) 1. 業(yè)務(wù)邏輯 1 . 業(yè)務(wù)邏輯
選項參數(shù)
(如?--sum) ? ? ? ? ?
位置參數(shù)
(如?X Y) ? ? ? ? ?
參數(shù)默認值 ? ? ? ? ?
互斥選項
(如?--car?和?--bus?只能二選一) ? ? ?
可通過第三方庫支持 ? ?
可變參數(shù)
(如指定多個?--file) ? ? ? ? ?
嵌套/父子命令 ? ? ? ? ?
工具箱 ? ? ? ? ?
鏈?zhǔn)矫钫{(diào)用 ? ? ? ? ?
類型約束 ? ? ? ? ?
Python 的命令行庫種類繁多、各具特色,它們并非是重復(fù)造輪子的產(chǎn)物,其背后的思想值得學(xué)習(xí)。結(jié)合橫向?qū)Ρ鹊目偨Y(jié),可以選擇出符合使用場景的庫。如果幾個庫都符合,那么就選擇你所偏愛的風(fēng)格。
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的Python 命令行库的大乱的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2020亚太内容分发大会 阿里云荣获“边
- 下一篇: 存储计算解耦合,构建中国人英语语音数据库