Python 编码风格指南

Python 编码风格指南

目次

基本遵从 PEP 准则

…… 但是,命名和单行长度更灵活。

PEP8 涵盖了诸如空格、函数/类/方法之间的换行、import、对已弃用功能的警告之类的寻常东西,大都不错。

应用这些准则的最佳工具是 flake8,还可以用来发现一些愚蠢的语法错误。

PEP8 原本只是一组指导原则,不必严格甚至虔诚地信奉。一定记得阅读 PEP8 「愚蠢的一致性就是小人物的小妖精」一节。若要进一步了解,可以听一下 Raymond Hettinger 的精彩演讲,「超越 PEP8」

唯一引起过多争议的准则事关单行长度和命名。要调整起来也不难。

灵活的单行长度

若是厌烦 flake8 死板的单行长度不得超过 79 个字符的限制,完全可以忽略或修改这一准则。这仍然不失为一条不错的经验法则,就像英语中句子不能超过 50 个单词,段落不能超过 10 个句子之类的规则一样。这是 flake8 配置文件 的链接,可以看到 max-line-length 配置选项。值得注意的是,可以给要忽略 flake8 检查的那一行加上 # noqa 注释,但是请勿滥用。

尽管如此,超过九成的代码行都不应该超过 79 个字符,原因很简单,「扁平胜于嵌套」。如果函数每一行都超出了 79 个字符,肯定有别的东西出错了,这时要看看代码而不是 flake8 配置。

一致的命名

关于命名,遵循几条简单的准则就可以避免众多足以影响整个小组的麻烦。

推荐的命名规则

下面这些准则大多改编自 Pacoo 小组

  • 类名:驼峰式 和首字母缩略词:HTTPWriter 优于 HttpWriter
  • 变量名:lower_with_underscores
  • 方法名和函数名:lower_with_underscores
  • 模块名:lower_with_underscores.py。(但是不带下划线的名字更好!)
  • 常量名:UPPER_WITH_UNDERSCORES
  • 预编译的正则表达式:name_re

通常都应该遵循这些准则,除非要参照其他工具的命名规范,比如数据库 schema 或者消息格式。

还可以用 驼峰式 给类似类却不是类的东西命名。使用 驼峰式 的主要好处在于让人们以「全局名词」来关注某个东西,而不是看作局部标记或动词。值得注意的是,Python 给 TrueFalseNone 这些根本不是类的东西命名也是用 驼峰式

不要用前缀后缀

…… 比如 _prefixsuffix_ 。函数和方法名可以用 _prefix 标记来暗示其是「私有的」,但是最好只在编写预期会广泛使用的 API 以及用 _prefix 标记来隐藏信息的时候谨慎使用。

PEP8 建议使用结尾的下划线来避免与内置关键字重名,比如:

临时这样用也可以,不过最好还是选一个别的名字。

__mangled 这种双下划线前缀给类/实例/方法命名的情况非常少,这实际上涉及特殊的名字修饰,非常罕见。不要起 __dunder__ 这种格式的名字,除非要实现 Python 标准协议,比如 __len__;这是为 Python 内部协议保留的命名空间,不应该在其中增加自定义的东西。

不要用单字符名字

(不过)一些常见的单字符名字可以接受。

lambda 表达式中,单参数函数可以命名为 x 。比如:

解包元组时可以用 _ 丢弃不需要的标记。比如:

意思就是说「忽略第一个元素」。

lambda 类似,在解析列表/字典/集合的时候,以及在生成器表达式或者一到两行的 for 循环中,可以使用单字符迭代标记。通常选择 x,比如:

可以求 items 序列中所有正整数之和。

此外比较常见的是 i,代表 index,通常和内置的 枚举 一起使用。比如:

除却上述情形,要极少甚至避免使用单字符用作标记/参数/方法的名字。因为这样就无法用 grep 进行检索了。

使用 self 及类似的惯例

应该:

  • 永远将方法的第一个变量命名为 self
  • 永远将 @classmethod 的第一个参数命名为 cls
  • 永远在变量参数列表中使用 *args**kwargs

不要在这些地方吹毛求疵

不遵循如下准则没有什么好处,干脆照它说的做。

永远继承自 object 并使用新式类

bad

class JSONWriter:
pass

# good class JSONWriter(object): pass
1
2
3
4
5
6
7
# bad
class JSONWriter :
     pass
 
# good
class JSONWriter ( object ) :
     pass

对于 Python 2 来说遵循这条准则很重要。不过由于 Python 3 所有的类都隐式继承自 object,这条准则就没有必要了。

不要在类中重复使用实例标记

# good class JSONWriter(object): def __init__(self, handler): self.handler = handler
1
2
3
4
5
6
7
8
9
10
# bad
class JSONWriter ( object ) :
     handler = None
     def __init__ ( self , handler ) :
         self . handler = handler
 
# good
class JSONWriter ( object ) :
     def __init__ ( self , handler ) :
         self . handler = handler

列表/字典/集合推导式优于 map/filter

尽管在大多数简单情况下最好使用解析表达式,不过有时候 map() 或者 filter() 可读性更佳,需要自己判断。

用圆括号 (...) 折行

# good from itertools import (groupby, chain, izip, islice)
1
2
3
4
5
6
7
# bad
from itertools import groupby , chain ,
                       izip , islice
 
# good
from itertools import ( groupby , chain ,
                       izip , islice )

用圆括号 (...) 写 API 更利落

# good response = (Search(using=client) .filter(“term”, cat=”search”) .query(“match”, title=”python”))
1
2
3
4
5
6
7
8
9
# bad
response = Search ( using = client )
           . filter ( “term” , cat = “search” )
           . query ( “match” , title = “python” )
 
# good
response = ( Search ( using = client )
             . filter ( “term” , cat = “search” )
             . query ( “match” , title = “python” ) )

函数调用中使用隐式行延续

# good return set((key.lower(), val.lower()) for key, val in mapping.iteritems())
1
2
3
4
5
6
7
# bad – simply unnecessary backslash
return set ( ( key . lower ( ) , val . lower ( ) )
           for key , val in mapping . iteritems ( ) )
 
# good
return set ( ( key . lower ( ) , val . lower ( ) )
           for key , val in mapping . iteritems ( ) )

isinstance(obj, cls), 不要用 type(obj) == cls

因为 isinstance 涵盖更多情形,包括子类和抽象基类。同时,不要过多使用 isinstance ,因为通常应该使用鸭子类型!

with 处理文件和锁

with 语句能够巧妙地关闭文件并释放锁,哪怕是在触发异常的情况下。所以:

# good with open(“somefile.txt”, “w”) as somefile: somefile.write(“sometext”) return
1
2
3
4
5
6
7
8
9
# bad
somefile = open ( “somefile.txt” , “w” )
somefile . write ( “sometext” )
return
 
# good
with open ( “somefile.txt” , “w” ) as somefile :
     somefile . write ( “sometext” )
return

None 相比较要用 is

None 值是一个单例,但是检查 None 的时候,实际上很少真的要在 左值上调用 __eq__。所以:

# good if item is None: continue
1
2
3
4
5
6
7
# bad
if item == None :
     continue
 
# good
if item is None :
   continue

好的写法不仅执行更快,而且更准确。使用 == 并不会更简洁,所以请记住本规则!

不要修改 sys.path

通过 sys.path.insert(0, "../") 等操作来控制 Python 的导入方法或许让人心动,但是要坚决避免这样做。

Python 有一套有几分复杂,却易于理解的模块路径解决方法。可以通过 PYTHONPATH 或诸如 setup.py develop 的技巧来调整 Python 导入模块的方法。还可以用 -m 运行 Python 得到需要的效果,比如使用 python -m mypkg.mymodule 而不是 python mypkg/mymodule.py。代码能否正常运行不应依赖于当前执行 Python 的工作路径。David Beazley 用 PDF 幻灯片再一次扭转了大家的看法,值得一读,“Modules and Packages: Live and Let Die!”

尽量不要自定义异常类型

…… 如果一定要,也不要创建太多。

要知道 Python 引入了 丰富的内建异常类。值得充分利用。而且通过描述那些触发特定错误条件的字符串消息,来实例化这些异常类,就能达到「定制」的目的。在用户代码中抛出 ValueError(参数错误),LookupError(键错误)以及 AssertionError(用 assert 语句)最为常见。

至于是否应该自己创建异常类有一个不错的经验法则,也就是搞清楚函数调用方每次调用函数之时是否都应该捕获该异常。如果是,那么的确应该自己创建异常类。不过这相当少见。关于这类明显不得不使用的自定义异常类有一个不错的例子,tornado.web.HTTPError。但是要留心 Tornado 是如何避免走极端的:框架或用户代码抛出的所有 HTTP 错误同属一个异常类。

短文档字符串应是名副其实的单行句子

good

def reverse_sort(items):
“”“Sort items in reverse order.”“”








1
2
3
4
5
6
7
8
9

# bad
def reverse_sort ( items ) :
     “”“
    sort items in reverse order
    ”“”
 
# good
def reverse_sort ( items ) :
     “”“Sort items in reverse order.”“”



把三引号 """ 放在同一行,首字母大写,以句号结尾。四行精简到两行,__doc__ 属性没有糟糕的换行,最吹毛求疵的人也会满意的!

文档字符串使用 reST

标准库和大多数开源项目皆是如此。Sphinx 提供支持,开箱即用。赶紧试试吧!Python requests 模块由此取得了极佳的效果。看看requests.api 模块的例子。

删除结尾空格

最挑剔也不过如此了吧,可是若做不到这一点,有些人可能会被逼疯。不乏能自动搞定这一切的编辑器;这是我用 vim 的实现

文档字符串要写好

下面是在函数文档字符串中使用 Sphinx 风格的 reST 的快速参考:

不要为文档而写文档。写文档字符串要这样思考:

也就是说,上例中没有必要说 timeoutfloat,默认值 5.0,显然是 float。在文档中指出其语义是「秒」更有用,就是说 5.0 意思是 5 秒钟。同时调用方不知道 qsargs 应该是什么,所以用 type 注释给出提示,调用方也无从知道函数的预期返回值是什么,所以 rtype 注释是合适的。

最后一点。吉多·范罗苏姆曾说过,他对 Python 的主要领悟是「读代码比写代码频率更高」。直接结论就是有些文档有用,更多的文档有害

基本上只需要给预计会频繁重用的函数写文档。如果给内部模块的每一个函数都写上文档,最后只能得到更加难以维护的模块,因为重构代码之时文档也要重构。不要「船货崇拜」文档字符串,更不要用工具自动生成文档。

范式和模式

是函数还是类

通常应该用函数而不是类。函数和模块是 Python 代码重用的基本单元,还是最灵活的形式。类是一些 Python 功能的「升级路径」,比如实现容器,代理,描述符,类型系统等等。但是通常函数都是更好的选择。

或许有人喜欢为了更好地组织代码而将关联的函数归在类中。但这是错的。关联的函数应该归在模块中。

尽管有时可以把类当作「小型命名空间」(比如用 @staticmethod)比较有用,一组方法更应该对同一个对象的内部操作有所贡献,而不仅仅作为行为分组。

与其创建 TimeHelper 类,带有一堆不得不引入子类才能使用的方法,永远不如直接为时间相关的函数创建 lib.time 模块。类会增殖出更多的类,会增加复杂性,降低可读性。

生成器和迭代器

生成器和迭代器是 Python 中最强大的特性 —— 应该掌握迭代器协议,yield 关键字和生成器表达式。

生成器不仅仅对要在大型数据流上反复调用的函数十分重要,而且可以让自定义迭代器更加简单,从而简化了代码。将代码重构为生成器通常可以在使得代码在更多场景下复用,从而简化代码。

Fluent Python 的作者 Lucinao Ramalho 通过 30 分钟的演讲,「迭代器和生成器: Python 之道」,给出了一个出色的,快节奏的概述。Python Essential Reference 和 Python Cookbook 的作者 David Beazley 有个深奥的三小时视频教程,「生成器:最后的前沿」,给出了令人满足的生成器用例的详细阐述。因为应用广泛,掌握这一主题非常有必要。

声明式还是命令式

声明式编程优于命令式编程。代码应该表明你想要做什么,而不是描述如何去做。Python 的函数式编程概览介绍了一些不错的细节并给出了高效使用该风格的例子。

使用轻量级的数据结构更好,比如 列表字典元组集合。将数据展开,编写代码对其进行转换,永远要优于重复调用转换函数/方法来构建数据。

一个例子是重构常见的列表解析:

应该改成 :

但是还有一个不错的例子是将 if/elif/else 链改成 字典 查询。

「纯」函数和迭代器更好

这是个从函数式编程社区借来的概念。这种函数和迭代器亦被描述为「无副作用」,「引用透明」或者有「不可变输入/输出」。

一个简单的例子,要避免这种代码:

这个函数应该重新写成:

这是个惊人的例子。函数不仅更加纯粹,而且更加精简了。不仅更加精简,而且更好。这里的纯粹是说 assert dedupe(items) == dedupe(items) 在「好」版本中恒为真。在「坏」版本中, num_dupes 在第二次调用时0,这会在使用时导致难以理解的错误。

这个例子也阐明了命令式风格和声明式风格的区别:改写后的函数读起来更像是对需要的东西的描述,而不是构建需要的东西的一系列操作。

简单的参数和返回值类型更好

函数应该尽可能处理数据,而不是自定义的对象。简单的参数类型更好,比如字典集合元组列表intfloatbool。从这些扩展到标准库类型,比如 datetime, timedelta, array, Decimal 以及 Future。只有在真的必要时才使用自定义类型。

判断函数是否足够精简有个不错的经验法则,问自己参数和返回值是否总是可以 JSON 序列化。结果证明这个经验法则相当有用:可以 JSON 序列化通常是函数在并行计算时可用的先决条件。但是,就本文档而言,主要的好处在于:可读性,可测试性以及总体的函数简单性。

避免「传统的」面向对象编程

在「传统的面向对象编程语言」中,比如 Java 和 C++ ,代码重用是通过类的继承和多态或者语言声称的类似机制实现的。对 Python 而言,尽管可以使用子类和基于类的多态,事实上在地道的 Python 程序中这些功能极少使用。

通过模块和函数实现代码重用更为普遍,通过鸭子类型实现动态调度更为常见。如果发现自己通过超类实现代码重用,停下来,重新思考。如果发现自己大量使用多态,考虑一下是否用 Python 的 dunder 协议或者鸭子类型策略会更好。

看一下另一个不错的 Python 演讲,一位 Python 核心贡献者的 「不要再写类了」。演讲者建议,如果构建的类只有一个命名像一个类的方法(比如 Runnable.run()),那么实际上只是用函数模拟了一个类,这时候应该停下来。因为在 Python 中,函数是「最高级的」类型,没有理由这样做。

Mixin 有时也没问题

可以使用 Mixin 实现基于类的代码重用,同时不需要走极端使用类型层次。但是不要滥用。「扁平胜于嵌套」也适用于类型层次,所以应该避免仅仅为了分解行为而引入不必要的必须层次的一层。

Mixin 实际上不是 Python 的特性,多亏了 Python 支持多重继承。可以创建基类将功能「注入」到子类中,而不必构成类型层次的「重要」组成部分,只需要将基类列入 bases 列表中的第一个元素。

要考虑顺序,同时不妨记住:bases 自底向上构成层次结构。这里可读性的好处在于关于这个类所需要知道的一切都包含在定义本身:「它混入了权限行为,是专门定制的 Tornado RequestHandler。」

小心框架

Python 有大量的 web,数据库等框架。Python 语言的一大乐趣在于创建自定义框架相当简单。使用开源框架时,应该注意不要将自己的「核心代码」和框架本身结合得过于紧密。

考虑为自己的代码创建框架的时候应当慎之又慎。标准库有很多内置的东西,PyPI 有的就更多了,而且通常你不会需要它

尊重元编程

Python 通过一些特性来支持 「元编程」,包括修饰器,上下文管理器,描述符,import 钩子,元类和抽象语法树(AST)转换。

应该能够自如地使用并理解这些特性,作为 Python 的核心组成部分这些特性有着充分地支持。但是应当意识到使用这些特性之时,也引入了复杂的失败场景。所以,要把为自己的代码创建元编程工具与决定「创建自定义框架」同等对待。它们意味着同一件事情。真要这么做的时候,把元编程工具写成独立的模块,写好文档!

不要害怕 「双下划线」方法

许多人将 Python 的元编程工具和其对 「双下划线」或「dunder」方法(比如 __getattr__)的支持混为一谈。

正如博文 ——「Python 双下划线,双倍惊喜」—— 所言,双下划线没有什么「特殊的」。它们只不过是 Python 核心开发人员为所有的 Python 内部协议所起的轻量级命名空间。毕竟,__init__ 也是双下划线,没有什么神奇的。

的确,有些双下划线比其他的会导致更多令人困惑的结果,比如,没有很好的理由就重载操作符通常不是什么好主意。但是它们中也有许多,比如 __repr____str____len__ 以及 __call__ 是 Python 语言的完整组成部分,应该在地道的 Python 代码中充分利用。不要回避!

代码风格小禅理

作为一位核心 Python 开发者,Barry Warsaw 曾经说过「Python 之禅」(PEP 20)被用作 Python 代码风格指南使他沮丧,因为这本是为 Python 的内部设计所作的一首小诗。也就是语言的设计以及语言的实现本身。不过必须承认,PEP 20 中有些行可以当作相当不错的地道 Python 代码指南,所以我们就把它加上了。

美胜于丑

这一条有些主观,实际上等同于问:接手代码的人会被折服还是感到失望?如果接手的人就是三年后的你呢?

显胜于隐

有时为了重构以去除重复的代码,会有一点抽象。应该能够将代码翻译成显现的英语并且大致了解它是干什么的。不应该有太多的「神奇之处」。

扁平胜于嵌套

这一条很好理解。最好的函数没有嵌套,既不用循环也不用 if 语句。第二好的函数只有一层嵌套。如果有两层及以上的嵌套,最好重构成更小的函数。

同样,不要害怕将嵌套的 if 语句重构为多部分布尔条件。比如:

最好写成:

可读性确实重要

不要害怕用 # 添加行注释。也不要滥用或者写过多文档。一点点逐行解释,通常很有帮助。不要害怕使用稍微长一些的名字,因为描述性更好。将 「response」写成「rsp」没有任何好处。使用 doctest 风格的例子在文档字符串中详细说明边界情况。简洁至上!

错误不应被放过

单独的except: pass 子句危害最大。永远不要使用。制止所有的异常实在危险。将异常处理限制在一行代码,并且总是将 except 处理器限制在特定的类型下。除此之外,可以自如地使用 logging 模块和 log.exception(…)

如果实现难以解释,那就是个坏主意

这虽是通用软件工程原则,但是特别适用于 Python 代码。大多数 Python 函数和对象都可以有易于解释的实现。如果难以解释,很可能是一个坏主意。通常可以通过「分而治之」将一个难以解释的函数重写成易于解释的函数,也就是分割成多个函数。

测试是个好主意

好吧,我们篡改了「Python 之禅」中的这一行,原文中「命名空间」才是个绝妙的好主意。

不过说正经的,优雅却没有测试的代码简直比哪怕是最丑陋却测试过的代码还要差劲。至少丑陋的代码可以重构成优雅的,但是优雅的代码却不能重构为可以证明是正确的代码,至少不写测试是做不到的!所以,写测试吧!拜托!

平分秋色

我们把宁愿不去解决的争论放在这个部分。不要因为这些重写别人的代码。这里的东西可以自由地交替使用。

str.format 还是重载格式化操作符 %

str.format 更健壮,然而 % 使用 "%s %s" printf 风格的字符串更加简洁。两者会永远共存。

如果需要保存 unicode,记得在格式模板中使用 unicode 字符串:

如果最后选择 %,应该考虑 "%(name)s" 语法,从而可以使用字典而不是元组,比如:

此外,不要重新发明轮子。str.format 有一点毫无疑问比 % 更好,那就是支持各种格式化模式,比如人性化的数字和百分数。直接用。

但是选择哪一个都没有问题。我们没有强制规定。

if item 还是 if item is not None

本条和之前的对于 None 是用 == 还是 is 没有关系。这里我们实际上利用了 Python 的 「真实性规则」来处理 if item,这实际上是「item 不是 None 或者空字符串」的简写。

Python 中的真实性有些复杂。显然第二种写法对于某些错误而言更安全。但是第一种写法在 Python 代码中非常常见,而且更短。对此我们并没有强制规定。

隐式多行字符串还是三引号 """

Python 编译器在语法分析时,如果多个字符串之间没有东西,会自动将其拼接为一个字符串。比如:

大致上等价于:

第一种写法可以保持缩进整洁,但是需要丑陋的换行符,第二种写法不需要换行符,但是打破了缩进。我们没有强制规定哪种更好。

在类还是实例上使用 raise

事实上给 raise 语句传入异常或者异常实例都可以。比如,下面两行代码大致等价:

本质上,Python 会将第一种写法自动转换为第二种。或许第二个写法更好,即使没有其他原因,它也能实际上提供一个有用的理由,就像有条有用的消息来解释为什么 ValueError 会发生一样。但是这两种写法等价的,不应该为这一点就重写代码。我们不做强制规定。

标准工具和项目结构

我们选择了一些「最佳组合」工具,以及像样的 Python 项目会用到的最小初始结构。

标准库

  • import datetime as dt: 永远像这样导入 datetime
  • dt.datetime.utcnow(): 优于 .now(), 后者使用的是当地时间
  • import json: 数据交换的标准
  • from collections import namedtuple: 用来做轻量级数据类型
  • from collections import defaultdict: 用来计数/分组
  • from collections import deque: 快速的双向队列
  • from itertools import groupby, chain: 为了声明式风格
  • from functools import wraps: 用来编写合乎规范的装饰器
  • argparse: 为了构建「健壮的」命令行工具
  • fileinput: 用来快速构建易于和 UNIX 管道结合使用的工具
  • log = logging.getLogger(__name__): 足够好用的日志
  • from __future__ import absolute_import: 修复导入别名

常见第三方库

  • python-dateutil 用来解析时间和日历
  • pytz 用来处理时区
  • tldextract 为了更好地处理 URL
  • msgpack-python 比 JSON 更加紧凑地编码
  • futures 为了 Future/pool 并发原语
  • docopt 用来快速编写一次性命令行工具
  • py.test 用来做单元测试,与 mockhypothesis 结合使用

本地开发项目框架

对所有的 Python 包和库而言:

  • 根目录下不要有 __init__.py:目录名用作包名!
  • mypackage/__init__.py 优于 src/mypackage/__init__.py
  • mypackage/lib/__init__.py 优于 lib/__init__.py
  • mypackage/settings.py 优于 settings.py
  • README.rst 用来给新手描述本项目;使用 rst
  • setup.py 用来构建简单工具,比如 setup.py develop
  • requirements.txt 是为 pip 准备的包依赖环境
  • dev-requirements.txt 是为 tests/local 准备的额外的依赖环境
  • Makefile 用来简化 (!!!) build/lint/test/run 步骤

另外,永远记得详细说明包依赖环境

灵感来源

下面这些链接或许可以给你启发,有助于书写具有良好风格和品味的 Python 代码。

出发吧,写更具 Python 风格的代码!

    <div id="crayon-5afc28ea70c71346822493" class="crayon-syntax crayon-theme-github crayon-font-monaco crayon-os-pc print-yes notranslate" data-settings=" minimize scroll-always" style="margin-top: 12px; margin-bottom: 12px; font-size: 13px !important; line-height: 15px !important; height: auto;">

        <div class="crayon-toolbar" data-settings=" show" style="font-size: 13px !important;height: 19.5px !important; line-height: 19.5px !important;"><span class="crayon-title"></span>
        <div class="crayon-tools" style="font-size: 13px !important;height: 19.5px !important; line-height: 19.5px !important;"><div class="crayon-button crayon-nums-button crayon-pressed" title="切换是否显示行编号"><div class="crayon-button-icon"></div></div><div class="crayon-button crayon-plain-button" title="纯文本显示代码"><div class="crayon-button-icon"></div></div><div class="crayon-button crayon-wrap-button" title="切换自动换行"><div class="crayon-button-icon"></div></div><div class="crayon-button crayon-expand-button" title="点击展开代码" style="display: none;"><div class="crayon-button-icon"></div></div><div class="crayon-button crayon-copy-button" title="复制代码"><div class="crayon-button-icon"></div></div><div class="crayon-button crayon-popup-button" title="在新窗口中显示代码"><div class="crayon-button-icon"></div></div><span class="crayon-language">Python</span></div></div>
        <div class="crayon-info" style="min-height: 18.2px !important; line-height: 18.2px !important;"></div>
        <div class="crayon-plain-wrap"><textarea wrap="soft" class="crayon-plain print-no" data-settings="dblclick" readonly="" style="tab-size: 4; font-size: 13px !important; line-height: 15px !important; z-index: 0; opacity: 0;">$ python

>>> import antigravity








1
2

$ python
>>> import antigravity


猜你喜欢

转载自blog.csdn.net/tong_t/article/details/80343230