前言

本文介绍了Python Fire。

Python Fire: https://github.com/google/python-fire

Python Fire是一个库,用于从绝对任何Python对象自动生成命令行界面(CLI)。

  • Python Fire是在Python中创建CLI的一种简单方法。
  • Python Fire是开发和调试Python代码的有用工具。
  • Python Fire有助于探索现有代码或将其他人的代码转换为CLI。
  • Python Fire使Bash和Python之间的转换更容易。
  • Python Fire通过使用已经导入和创建的模块和变量设置REPL,使使用Python REPL变得更加容易。

要使用pip安装Python Fire,请运行:pip install fire

操作系统:Windows 11 家庭中文版

参考文档

  1. The Python Fire Guide

基本用法

您可以在任何Python对象上调用Fire:函数、类、模块、对象、字典、列表、元组等。它们都有效!

这是在函数上调用Fire的示例。

1
2
3
4
5
6
7
import fire

def hello(name="World"):
return "Hello %s!" % name

if __name__ == '__main__':
fire.Fire(hello)

然后,从命令行,您可以运行:

1
2
3
python hello.py  # Hello World!
python hello.py --name=David # Hello David!
python hello.py --help # Shows usage information.

这是一个在类上调用Fire的例子。

1
2
3
4
5
6
7
8
9
10
import fire

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

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

if __name__ == '__main__':
fire.Fire(Calculator)

然后,从命令行,您可以运行:

1
2
python calculator.py double 10  # 20
python calculator.py double --number=15 # 30

Hello World

fire.Fire()

使用Fire最简单的方法是获取任何Python程序,然后在程序末尾简单地调用fire.Fire(),这会将程序的全部内容公开到命令行。

1
2
3
4
5
6
7
import fire

def hello(name):
return 'Hello {name}!'.format(name=name)

if __name__ == '__main__':
fire.Fire()

以下是我们如何从命令行运行我们的程序:

1
2
$ python example.py hello World
Hello World!

fire.Fire(<fn>)

让我们稍微修改一下我们的程序,只向命令行公开hello函数。

1
2
3
4
5
6
7
import fire

def hello(name):
return 'Hello {name}!'.format(name=name)

if __name__ == '__main__':
fire.Fire(hello)

以下是我们如何从命令行运行它:

1
2
$ python example.py World
Hello World!

请注意,我们不再需要指定运行hello函数,因为我们调用了fire.Fire(hello)。

Using a main

我们也可以像这样编写这个程序:

1
2
3
4
5
6
7
8
9
10
import fire

def hello(name):
return 'Hello {name}!'.format(name=name)

def main():
fire.Fire(hello)

if __name__ == '__main__':
main()

或者如果我们使用入口点,那么简单地说:

1
2
3
4
5
6
7
import fire

def hello(name):
return 'Hello {name}!'.format(name=name)

def main():
fire.Fire(hello)

Fire Without Code Changes

如果您有一个文件example.py甚至不导入fire:

1
2
def hello(name):
return 'Hello {name}!'.format(name=name)

然后你可以像这样将它与Fire一起使用:

1
2
$ python -m fire example hello --name=World
Hello World!

您还可以指定example.py的文件路径,而不是它的模块路径,如下所示:

1
2
$ python -m fire example.py hello --name=World
Hello World!

公开多个命令

在前面的示例中,我们将单个函数公开给命令行。现在我们将看看将多个函数公开给命令行的方法。

fire.Fire()

公开多个命令的最简单方法是编写多个函数,然后调用Fire。

1
2
3
4
5
6
7
8
9
10
import fire

def add(x, y):
return x + y

def multiply(x, y):
return x * y

if __name__ == '__main__':
fire.Fire()

我们可以这样使用它:

1
2
3
4
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200

您会注意到Fire正确地将10和20解析为数字,而不是字符串。

fire.Fire(<dict>)

在版本1中,我们将程序的所有功能公开给命令行。通过使用dict,我们可以有选择地将函数公开给命令行。

1
2
3
4
5
6
7
8
9
10
11
12
13
import fire

def add(x, y):
return x + y

def multiply(x, y):
return x * y

if __name__ == '__main__':
fire.Fire({
'add': add,
'multiply': multiply,
})

我们可以像以前一样使用它:

1
2
3
4
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200

fire.Fire(<object>)

Fire也适用于对象,就像在这个变体中一样。这是公开多个命令的好方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
import fire

class Calculator(object):

def add(self, x, y):
return x + y

def multiply(self, x, y):
return x * y

if __name__ == '__main__':
calculator = Calculator()
fire.Fire(calculator)

我们可以像以前一样使用它:

1
2
3
4
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200

fire.Fire(<class>)

Fire也适用于类。这是公开多个命令的另一种好方法。

1
2
3
4
5
6
7
8
9
10
11
12
import fire

class Calculator(object):

def add(self, x, y):
return x + y

def multiply(self, x, y):
return x * y

if __name__ == '__main__':
fire.Fire(Calculator)

我们可以像以前一样使用它:

1
2
3
4
$ python example.py add 10 20
30
$ python example.py multiply 10 20
200

为什么您更喜欢类而不是对象?一个原因是您也可以传递构造类的参数,就像这个破碎的计算器示例一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import fire

class BrokenCalculator(object):

def __init__(self, offset=1):
self._offset = offset

def add(self, x, y):
return x + y + self._offset

def multiply(self, x, y):
return x * y + self._offset

if __name__ == '__main__':
fire.Fire(BrokenCalculator)

当您使用损坏的计算器时,您会得到错误的答案:

1
2
3
4
$ python example.py add 10 20
31
$ python example.py multiply 10 20
201

但你总是可以修复它:

1
2
3
4
$ python example.py add 10 20 --offset=0
30
$ python example.py multiply 10 20 --offset=0
200

普通函数的调用既可以使用位置参数,也可以使用命名参数(–标记语法),与此不同,__init__函数的参数必须使用–标记语法传递。

分组命令

下面是一个示例,说明如何使用分组命令创建命令行界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class IngestionStage(object):

def run(self):
return 'Ingesting! Nom nom nom...'

class DigestionStage(object):

def run(self, volume=1):
return ' '.join(['Burp!'] * volume)

def status(self):
return 'Satiated.'

class Pipeline(object):

def __init__(self):
self.ingestion = IngestionStage()
self.digestion = DigestionStage()

def run(self):
ingestion_output = self.ingestion.run()
digestion_output = self.digestion.run()
return [ingestion_output, digestion_output]

if __name__ == '__main__':
fire.Fire(Pipeline)

这是命令行的外观:

1
2
3
4
5
6
7
8
9
$ python example.py run
Ingesting! Nom nom nom...
Burp!
$ python example.py ingestion run
Ingesting! Nom nom nom...
$ python example.py digestion run
Burp!
$ python example.py digestion status
Satiated.

如果你感到脾气暴躁或喜欢冒险,你可以用任意复杂的方式嵌套你的命令。

访问属性

在我们目前看到的示例中,我们对python example.py的调用都运行了示例程序中的一些函数。在此示例中,我们只需访问一个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
from airports import airports

import fire

class Airport(object):

def __init__(self, code):
self.code = code
self.name = dict(airports).get(self.code)
self.city = self.name.split(',')[0] if self.name else None

if __name__ == '__main__':
fire.Fire(Airport)

现在我们可以使用这个程序来了解机场代码!

1
2
3
4
5
6
$ python example.py --code=JFK code
JFK
$ python example.py --code=SJC name
San Jose-Sunnyvale-Santa Clara, CA - Norman Y. Mineta San Jose International (SJC)
$ python example.py --code=ALB city
Albany-Schenectady-Troy

顺便说一下,你可以在这里找到这个机场模块。

链接函数调用

当您运行Fire CLI时,您可以对Fire调用的结果执行与传入的原始对象相同的操作。

例如,我们可以像这样使用前面示例中的Airport CLI:

1
2
$ python example.py --code=ALB city upper
ALBANY-SCHENECTADY-TROY

这是有效的,因为upper是所有字符串的方法。

所以,如果你想把你的函数设置得很好,你所要做的就是有一个类,它的方法返回self。这里有一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import fire

class BinaryCanvas(object):
"""A canvas with which to make binary art, one bit at a time."""

def __init__(self, size=10):
self.pixels = [[0] * size for _ in range(size)]
self._size = size
self._row = 0 # The row of the cursor.
self._col = 0 # The column of the cursor.

def __str__(self):
return '\n'.join(' '.join(str(pixel) for pixel in row) for row in self.pixels)

def show(self):
print(self)
return self

def move(self, row, col):
self._row = row % self._size
self._col = col % self._size
return self

def on(self):
return self.set(1)

def off(self):
return self.set(0)

def set(self, value):
self.pixels[self._row][self._col] = value
return self

if __name__ == '__main__':
fire.Fire(BinaryCanvas)

现在我们可以画东西了:)。

1
2
3
4
5
6
7
8
9
10
11
$ python example.py move 3 3 on move 3 6 on move 6 3 on move 6 6 on move 7 4 on move 7 5 on
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0
0 0 0 0 1 1 0 0 0 0
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0

它应该是一个笑脸。

自定义序列化

您会注意到在BinaryCanvas示例中,带有笑脸的画布被打印到屏幕上。您可以通过定义组件的__str__方法来确定如何序列化组件。

如果最终组件上存在自定义__str__方法,则序列化并打印对象。如果没有自定义__str__方法,则显示对象的帮助屏幕。

我们能做一个比Hello World更简单的例子吗?

是的,这个程序甚至比我们最初的Hello World示例更简单。

1
2
3
4
import fire
english = 'Hello World'
spanish = 'Hola Mundo'
fire.Fire()

你可以这样使用它:

1
2
3
4
$ python example.py english
Hello World
$ python example.py spanish
Hola Mundo

调用函数

构造函数的参数使用标志语法–name=value按名称传递。

例如,考虑这个简单的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import fire

class Building(object):

def __init__(self, name, stories=1):
self.name = name
self.stories = stories

def climb_stairs(self, stairs_per_story=10):
for story in range(self.stories):
for stair in range(1, stairs_per_story):
yield stair
yield 'Phew!'
yield 'Done!'

if __name__ == '__main__':
fire.Fire(Building)

我们可以实例化如下:python example.py --name=“Sherrerd Hall”

其他函数的参数可以使用标志语法按位置或按名称传递。

要实例化一个Build,然后运行climb_stairs函数,以下命令都是有效的:

1
2
3
4
$ python example.py --name="Sherrerd Hall" --stories=3 climb_stairs 10
$ python example.py --name="Sherrerd Hall" climb_stairs --stairs_per_story=10
$ python example.py --name="Sherrerd Hall" climb_stairs --stairs-per-story 10
$ python example.py climb-stairs --stairs-per-story 10 --name="Sherrerd Hall"

您会注意到连字符和下划线(-和_)在成员名称和标志名称中是可以互换的。

您还会注意到构造函数的参数可以在函数的参数之后或在函数之前。

您还会注意到标志名称与其值之间的等号是可选的。

Functions with *varargs and **kwargs

Fire支持采用*varargs**kwargs的函数。这是一个例子:

1
2
3
4
5
6
7
8
9
import fire

def order_by_length(*items):
"""Orders items by length, breaking ties alphabetically."""
sorted_items = sorted(items, key=lambda item: (len(str(item)), str(item)))
return ' '.join(sorted_items)

if __name__ == '__main__':
fire.Fire(order_by_length)

要使用它,我们运行:

1
2
$ python example.py dog cat elephant
cat dog elephant

您可以使用分隔符来表示您已完成向函数提供参数。分隔符之后的所有参数都将用于处理函数的结果,而不是传递给函数本身。默认分隔符是连字符-。

这是我们使用分隔符的示例。

1
2
$ python example.py dog cat elephant - upper
CAT DOG ELEPHANT

如果没有分隔符,upper将被视为另一个参数。

1
2
$ python example.py dog cat elephant upper
cat dog upper elephant

您可以使用--separator标志更改分隔符。标志始终通过隔离的–与Fire命令分开。这是我们更改分隔符的示例。

1
2
$ python example.py dog cat elephant X upper -- --separator=X
CAT DOG ELEPHANT

当函数接受*varargs**kwargs或您不想指定的默认值时,分隔符会很有用。如果要传递-作为参数,请记住更改分隔符也很重要。

参数解析

参数的类型由它们的值决定,而不是由使用它们的函数签名决定。您可以从命令行传递任何Python文字:数字、字符串、元组、列表、字典(仅在某些Python版本中支持集合)。您还可以任意嵌套集合,只要它们只包含文字。

为了演示这一点,我们将制作一个小示例程序,告诉我们我们给它的任何参数的类型:

1
2
import fire
fire.Fire(lambda obj: type(obj).__name__)

我们将这样使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python example.py 10
int
$ python example.py 10.0
float
$ python example.py hello
str
$ python example.py '(1,2)'
tuple
$ python example.py [1,2]
list
$ python example.py True
bool
$ python example.py {name:David}
dict

您会注意到在最后一个示例中,裸词会自动替换为字符串。

小心你的引号!如果你想传递字符串“10”,而不是int 10,你需要转义或引用你的引号。否则Bash会吃掉你的引号并将一个未引用的10传递给你的Python程序,Fire会将其解释为一个数字。

1
2
3
4
5
6
7
8
9
10
$ python example.py 10
int
$ python example.py "10"
int
$ python example.py '"10"'
str
$ python example.py "'10'"
str
$ python example.py \"10\"
str

小心你的引用!请记住,Bash首先处理您的参数,然后Fire解析结果。如果您想将字典{“name”: “David Bieber”}传递给您的程序,您可以尝试这样做:

1
2
3
4
5
6
7
8
9
10
$ python example.py '{"name": "David Bieber"}'  # Good! Do this.
dict
$ python example.py {"name":'"David Bieber"'} # Okay.
dict
$ python example.py {"name":"David Bieber"} # Wrong. This is parsed as a string.
str
$ python example.py {"name": "David Bieber"} # Wrong. This isn't even treated as a single argument.
<error>
$ python example.py '{"name": "Justin Bieber"}' # Wrong. This is not the Bieber you're looking for. (The syntax is fine though :))
dict

布尔参数

True和False被解析为布尔值。

您还可以通过标志语法–name和–noname指定布尔值,它们分别将名称设置为True和False。

继续前面的示例,我们可以运行以下任何操作:

1
2
3
4
5
6
7
8
$ python example.py --obj=True
bool
$ python example.py --obj=False
bool
$ python example.py --obj
bool
$ python example.py --noobj
bool

小心布尔标志!如果除另一个标志之外的标记紧跟在应该是布尔值的标志之后,该标志将采用标记的值,而不是布尔值。您可以解决这个问题:通过在最后一个标志之后放置分隔符,通过显式声明布尔标志的值(如–obj=True),或者通过确保在任何布尔标志参数之后都有另一个标志。

使用Fire标志

Fire CLI都带有一些标志。这些标志应该用一个隔离的参数与Fire命令分开。如果至少有一个隔离的参数,那么最后一个隔离的参数之后的参数被视为标志,而最后一个隔离的参数之前的所有参数都被视为Fire命令的一部分。

一个有用的标志是–interactive标志。使用任何CLI上的–interactive标志输入Python REPL,其中包含调用Fire的上下文中使用的所有模块和变量,这些模块和变量已经可供您使用。其他有用的变量,例如Fire命令的结果也将可用。像这样使用此功能:python example.py – --interactive。

您可以将help标志添加到任何命令中以查看帮助和使用信息。Fire会将您的文档字符串合并到它生成的帮助和使用信息中。即使您省略了隔离的--将标志与Fire命令分开,Fire也会尝试提供帮助,但可能并不总是能够,因为help是一个有效的参数名称。像这样使用此功能:python example.py – --help或python example.py --help(甚至python example.py -h)。

完整的可用标志集如下所示,在参考部分。

参考

Flags

Using a CLI Command Notes
Help command -- --help Show help and usage information for the command.
REPL command -- --interactive Enter interactive mode.
Separator command -- --separator=X This sets the separator to X. The default separator is -.
Completion command -- --completion [shell] Generate a completion script for the CLI.
Trace command -- --trace Gets a Fire trace for the command.
Verbose command -- --verbose Include private members in the output.

Note that flags are separated from the Fire command by an isolated -- arg.
Help is an exception; the isolated -- is optional for getting help.

Arguments for Calling fire.Fire()

Argument Usage Notes
component fire.Fire(component) If omitted, defaults to a dict of all locals and globals.
command fire.Fire(command='hello --name=5') Either a string or a list of arguments. If a string is provided, it is split to determine the arguments. If a list or tuple is provided, they are the arguments. If command is omitted, then sys.argv[1:] (the arguments from the command line) are used by default.
name fire.Fire(name='tool') The name of the CLI, ideally the name users will enter to run the CLI. This name will be used in the CLI’s help screens. If the argument is omitted, it will be inferred automatically.
serialize fire.Fire(serialize=custom_serializer) If omitted, simple types are serialized via their builtin str method, and any objects that define a custom __str__ method are serialized with that. If specified, all objects are serialized to text via the provided method.

结语

第一百四十八篇博文写完,开心!!!!

今天,也是充满希望的一天。