巧用Google Fire簡化Python命令列程式

老錢發表於2018-06-06

Hello World

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

# 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來執行上面編寫的指令碼

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

上面是官方的示例程式碼,有了fire,編寫Python的命令列程式就變得非常簡單,我們無需再去處理繁瑣的命令列引數解析了。接下來我們仿照HelloWorld,編寫一個圓周率和階乘計算的命令列指令碼。

巧用Google Fire簡化Python命令列程式

實戰

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)
複製程式碼

接下來我們執行一下

>  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都有哪些暴露模式。

暴露模組

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函式呼叫沒有任何引數,執行一下

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

暴露函式

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)


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

如果暴露函式那就只能暴露一個函式,如果暴露了兩個,那就只有後面一個生效,執行一下

>  python maths.py 1000
3.14063805621
複製程式碼

暴露字典

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({
        "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
複製程式碼

原理

巧用Google Fire簡化Python命令列程式

命令列中的引數順序和程式碼內部物件的樹狀層次結構呈現一一對應關係。如果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,你有本事試試把複雜的命令列引數解析程式碼寫優雅了給老師我看看。

巧用Google Fire簡化Python命令列程式

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

相關文章