巧用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,你有本事试试把复杂的命令行参数解析代码写优雅了给老师我看看。

阅读更多高级文章,关注公众号「码洞」

相关文章
相关标签/搜索