巧用Google Fire簡化Python命令行程序

Hello World

要介紹Fire是什麼,看一個簡單的例子就明白了python

# calc.py
import fire

class Calculator(object):
  """A simple calculator class."""

  def double(self, number):
    return 2 * number

if __name__ == '__main__':
  fire.Fire(Calculator)
複製代碼

接下來咱們進入bash來執行上面編寫的腳本redis

> python calc.py double 10
20
> python calc.py double --number=16
32
複製代碼

上面是官方的示例代碼,有了fire,編寫Python的命令行程序就變得很是簡單,咱們無需再去處理繁瑣的命令行參數解析了。接下來咱們仿照HelloWorld,編寫一個圓周率和階乘計算的命令行腳本。shell

實戰

import math
import fire


class Math(object):

    def pi(self, n):
        s = 0.0
        for i in range(n):
            s += 1.0/(i+1)/(i+1)
        return math.sqrt(6*s)

    def fact(self, n):
        s = 1
        for i in range(n):
            s *= (i+1)
        return s


if __name__ == '__main__':
    fire.Fire(Math)
複製代碼

接下來咱們運行一下bash

>  python maths.py pi 10000
3.14149716395
>  python maths.py pi 100000
3.14158310433
>  python maths.py pi 1000000
3.14159169866
>  python maths.py fact 10
3628800
>  python maths.py fact 15
1307674368000
>  python maths.py fact 20
2432902008176640000
複製代碼

Cool,真的很是方便!fire對當前對象結構進行了暴露,將結構信息映射到shell命令行參數上。fire其實有多種暴露模式,接下來咱們逐個來看fire都有哪些暴露模式。python2.7

暴露模塊

fire若是不傳遞任何參數就能夠直接暴露當前模塊結構,咱們對上面的例子作一下改造,去掉類信息函數

import math
import fire


def pi(n):
    s = 0.0
    for i in range(n):
        s += 1.0/(i+1)/(i+1)
    return math.sqrt(6*s)

def fact(n):
    s = 1
    for i in range(n):
        s *= (i+1)
    return s


if __name__ == '__main__':
    fire.Fire()
複製代碼

注意Fire函數調用沒有任何參數,運行一下ui

>  python maths.py fact 20
2432902008176640000
>  python maths.py pi 1000000
3.14159169866
複製代碼

暴露函數

fire還能夠傳遞一個函數對象來暴露單個函數,可讓咱們在命令行參數上省掉函數名稱spa

import math
import fire


def pi(n):
    s = 0.0
    for i in range(n):
        s += 1.0/(i+1)/(i+1)
    return math.sqrt(6*s)


if __name__ == '__main__':
    fire.Fire(pi)
複製代碼

若是暴露函數那就只能暴露一個函數,若是暴露了兩個,那就只有後面一個生效,運行一下命令行

>  python maths.py 1000
3.14063805621
複製代碼

暴露字典

fire能夠直接暴露一個模塊,將當前模塊的全部函數所有暴露,函數名和第一個參數名一致。咱們也能夠不用暴露整個模塊的全部函數,使用字典暴露法就能夠選擇性地對模塊的某些函數進行暴露,順便還能夠替換暴露出來的函數名稱。3d

import math
import fire


def pi(n):
    s = 0.0
    for i in range(n):
        s += 1.0/(i+1)/(i+1)
    return math.sqrt(6*s)

def fact(n):
    s = 1
    for i in range(n):
        s *= (i+1)
    return s


if __name__ == '__main__':
    fire.Fire({
        "pi[n]": pi
    })
複製代碼

咱們只暴露了pi函數,而且把名字還換掉了,運行一下,看效果

>  python maths.py pi[n] 1000
3.14063805621
複製代碼

若是咱們使用原函數名稱,就會看到fire列出的友好的報錯信息

>  python maths.py pi 1000
Fire trace:
1. Initial component
2. ('Cannot find target in dict:', 'pi', {'pi[n]': <function pi at 0x10a062c08>})

Type:        dict
String form: {'pi[n]': <function pi at 0x10a062c08>}
Length:      1

Usage:       maths.py
             maths.py pi[n]
複製代碼

暴露對象

import math
import fire


class Maths(object):

    def pi(self, n):
        s = 0.0
        for i in range(n):
            s += 1.0/(i+1)/(i+1)
        return math.sqrt(6*s)

    def fact(self, n):
        s = 1
        for i in range(n):
            s *= (i+1)
        return s


if __name__ == '__main__':
    fire.Fire(Maths())
複製代碼

運行

>  python maths.py pi 1000
3.14063805621
>  python maths.py fact 20
2432902008176640000
複製代碼

暴露類

這個咱們在上面的實戰環節已經演示過了,這裏就不在重複粘貼

類 vs 對象

經過上面的例子,咱們發現暴露類和暴露對象彷佛沒有任何區別,那到底該選哪一種比較優雅呢?這個要看類的構造器有沒有參數,若是是不帶參數的構造器,那麼類和對象的暴露是沒有區別的,可是若是類的構造器有參數,那就不同了,下面咱們改造一下Maths類,增長一個放大係數。

import math
import fire


class Maths(object):

    def __init__(self, coeff):
        self.coeff = coeff

    def pi(self, n):
        s = 0.0
        for i in range(n):
            s += 1.0/(i+1)/(i+1)
        return self.coeff * math.sqrt(6*s)

    def fact(self, n):
        s = 1
        for i in range(n):
            s *= (i+1)
        return self.coeff * s


if __name__ == '__main__':
    fire.Fire(Maths)
複製代碼

由於Maths的構造器帶有參數,全部運行命令行時須要指定構造器參數值

> python maths.py pi 1000 --coeff=2
6.28127611241
複製代碼

若是不指定參數的值,運行時就會報錯

> python maths.py pi 1000
Fire trace:
1. Initial component
2. ('The function received no value for the required argument:', 'coeff')

Type:        type
String form: <class '__main__.Maths'>
File:        ~/source/rollado/maths.py
Line:        5

Usage:       maths.py COEFF
             maths.py --coeff COEFF
複製代碼

若是改爲暴露對象,那麼放大係數就是在代碼裏寫死的,沒法在命令行進行參數定製了。這就是暴露對象和暴露類的差異,彷佛暴露類在功能上更強大一些。

暴露屬性

上面的全部例子咱們最終暴露的都是函數,要麼是模塊裏的函數,要麼是類裏的函數。但實際上fire還能夠暴露屬性,好比咱們能夠將上面的coeff參數經過命令行進行輸出。

> python maths.py coeff --coeff=2
2
> python maths.py coeff --coeff=3
3
複製代碼

再來一個更加簡單的例子

# example.py
import fire
english = 'Hello World'
spanish = 'Hola Mundo'
fire.Fire()
複製代碼

運行

$ python example.py english
Hello World
$ python example.py spanish
Hola Mundo
複製代碼

原理

命令行中的參數順序和代碼內部對象的樹狀層次結構呈現一一對應關係。若是fire不帶參數暴露了當前的模塊,那麼第一個參數就應該是這個模塊內部的函數名、類名或者是變量名。若是第一個參數是函數,那麼接下來的參數就是函數的參數。若是第一個參數是類,那麼接下來的參數多是這個類實例內部的方法或者字段。若是第一個參數是變量名,後面沒有參數的話,就直接顯示這個變量。若是後面還有參數,那麼就把這個變量當作一個對象,而後繼續使用後續參數來深刻解析這個對象。

在Python裏面全部的變量都是對象,包括普通的整數、字符串、浮點數、布爾值等。理論上能夠一直將對象結構遞歸下去,造成一個複雜的鏈式調用。

鏈式暴露

接下來咱們驗證這個理論,嘗試一下複雜的鏈式暴露。

import fire


class Chain(object):

    def __init__(self):
        self.value = 1

    def incr(self):
        print "incr", self.value
        self.value += 1
        return self

    def decr(self):
        print "decr", self.value
        self.value -= 1
        return self

    def get(self):
        return self.value


if __name__ == '__main__':
    fire.Fire(Chain)
複製代碼

運行一下

> python chains.py incr incr incr decr decr get
incr 1
incr 2
incr 3
decr 4
decr 3
2
複製代碼

Cool! 咱們經過在每一個方法裏面方法self對象自身來實現了漂亮的鏈式調用效果。

接下來咱們嘗試對內置字符串對象進行解構

# xyz.py
import fire

value = "hello"

if __name__ == '__main__':
    fire.Fire()
複製代碼

字符串有upper和lower方法,咱們反覆使用upper和lower,而後觀察結果

> python xyz.py value
hello
> python xyz.py value upper
HELLO
> python xyz.py value upper lower
Traceback (most recent call last):
  File "xyz.py", line 7, in <module>
    fire.Fire()
  File "/Users/pyloque/source/pys/.py/lib/python2.7/site-packages/fire/core.py", line 127, in Fire
    component_trace = _Fire(component, args, context, name)
  File "/Users/pyloque/source/pys/.py/lib/python2.7/site-packages/fire/core.py", line 366, in _Fire
    component, remaining_args)
  File "/Users/pyloque/source/pys/.py/lib/python2.7/site-packages/fire/core.py", line 542, in _CallCallable
    result = fn(*varargs, **kwargs)
TypeError: upper() takes no arguments (1 given)
複製代碼

很不幸,內置的字符串對象彷佛不支持鏈式調用,第一個upper卻是執行成功了。不過fire提供了一個特殊的符號用來解決這個問題。

> python xyz.py value upper - lower
hello
> python xyz.py value upper - lower - upper
HELLO
> python xyz.py value upper - lower - upper - lower
hello
複製代碼

減號用來表示參數的結束,這樣後續的參數就不會被當成函數的參數來映射了。

讓redis-py秒變命令行

最後咱們再來一個酷炫的小例子,把redis-py的StrictRedis暴露一下變身命令行

import fire
import redis


if __name__ == '__main__':
    fire.Fire(redis.StrictRedis)
複製代碼

就這麼簡單,接下來就能夠玩命令行了

>  python client.py flushdb
True
>  python client.py set codehole superhero
True
>  python client.py get codehole
superhero
>  python client.py exists codehole
True
>  python client.py keys "*"
codehole
>  python client.py delete codehole
1
# 指定地址
> python client.py set codehole superhero --host=127.0.0.1 --port=6379
True
複製代碼

總結

有了Google Fire這樣一個小巧的類庫,咱們就能夠從複雜的命令行參數分析中解脫出來了。咱們常說寫代碼要漂亮優雅,沒有好的類庫,這種理想也不是很是容易實現的。若是沒有fire,你有本事試試把複雜的命令行參數解析代碼寫優雅了給老師我看看。

閱讀更多高級文章,關注公衆號「碼洞」

相關文章
相關標籤/搜索