Python Style Rules(python风格规则)
3.1 Semicolons(分号)
不要用分号作为语句的结束,也不要用分号将两个语句放在一行。
3.2 Line Length(行长度)
一行最大长度为80个字符。
例外:
- 长的导入声明
- URL、路径名称或者注释中长标记
- 不含空格的模块级长字符串常量,比如URL或路径名称,跨行显示不方便。
- pylint中disable注释。例如,
pylint: disable=invalid-name
。
不要用反斜杠来续行,除非是需要三行及以上上下文管理的with
语句。
使用python的小括号、大括号和中括号隐式续行。如果必要,你可以在表达式两边额外加一组小括号。
Yes: foo_bar(self, width, height, color='black', design=None, x='foo',
emphasis=None, highlight=0)
if (width == 0 and height == 0 and
color == 'red' and emphasis == 'strong'):
当一个字符串常量一行放不下,就用小括号来隐式续行。
x = ('This will build a very long long '
'long long long long long long string')
在注释中,如果必要,把长的URL地址放在它们自己的行里。
Yes: # See details at
# http://www.example.com/us/developer/documentation/api/content/v2.0/csv_file_name_extension_full_specification.html
No: # See details at
# http://www.example.com/us/developer/documentation/api/content/\
# v2.0/csv_file_name_extension_full_specification.html
当定义一个长度跨了三行及以上的with
表达式时,允许使用反斜杠来续行。对于两行的表达式,使用嵌套的with
声明。
Yes: with very_long_first_expression_function() as spam, \
very_long_second_expression_function() as beans, \
third_thing() as eggs:
place_order(eggs, beans, spam, beans)
No: with VeryLongFirstExpressionFunction() as spam, \
VeryLongSecondExpressionFunction() as beans:
PlaceOrder(eggs, beans, spam, beans)
Yes: with very_long_first_expression_function() as spam:
with very_long_second_expression_function() as beans:
place_order(beans, spam)
记住上面续行例子的元素缩进。参考indentation章节的解释。
3.3 Parentheses(小括号)
保守使用小括号。
可以在元组两边使用小括号,但不是必须的。不要再return语句或条件语句中使用小括号,除非用小括号来隐式续行或表明一个元组。
Yes: if foo:
bar()
while x:
x = bar()
if x and y:
bar()
if not x:
bar()
# For a 1 item tuple the ()s are more visually obvious than the comma.
onesie = (foo,)
return foo
return spam, beans
return (spam, beans)
for (x, y) in dict.items(): ...
No: if (x):
bar()
if not(x):
bar()
return (foo)
3.4 Indentation(缩进)
使用4个空格缩进你的代码块。
不要使用tab或者混用tab和空格。在隐式续行情况下,应该按照line length章节的例子来垂直对齐这些元素;或者悬挂缩进4个空格,这种情况下第一行打开的括号后面什么都不能添加。
Yes: # Aligned with opening delimiter
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# Aligned with opening delimiter in a dictionary
foo = {
long_dictionary_key: value1 +
value2,
...
}
# 4-space hanging indent; nothing on first line
foo = long_function_name(
var_one, var_two, var_three,
var_four)
meal = (
spam,
beans)
# 4-space hanging indent in a dictionary
foo = {
long_dictionary_key:
long_dictionary_value,
...
}
No: # Stuff on first line forbidden
foo = long_function_name(var_one, var_two,
var_three, var_four)
meal = (spam,
beans)
# 2-space hanging indent forbidden
foo = long_function_name(
var_one, var_two, var_three,
var_four)
# No hanging indent in a dictionary
foo = {
long_dictionary_key:
long_dictionary_value,
...
}
3.5 Blank Lines(空行)
顶层定义之间用两个空行相隔,方法定义之间用一个空行。
顶层定义之间用两个空行相隔,它们应当是函数或类定义。方法定义之间用一个空行相隔,class
所在行与第一个方法之间用一个空行。在函数和方法内部通过你的判断来使用单个空行。
3.6 Whitespace(空格)
标点周围使用空格要遵守标准的排版规则。
括号(大中小)中不要有空格。
Yes: spam(ham[1], {eggs: 2}, [])
No: spam( ham[ 1 ], { eggs: 2 }, [ ] )
逗号、分号和冒号前面不要有空格。务必在逗号、分号和冒号后面使用,除非他们在该行末尾。
Yes: if x == 4:
print(x, y)
x, y = y, x
No: if x == 4 :
print(x , y)
x , y = y , x
开始参数列表、索引或切片的打开的圆括号方括号前不要有空格。
Yes: spam(1)
No: spam (1)
Yes: dict['key'] = list[index]
No: dict ['key'] = list [index]
二元运算符两边各放一个空格,赋值符号=
、比较符号(==, <, >, !=, <>, <=, >=, in, not in, is, is not
)和布尔运算符(and, or, not
)。根据你的判断在算数运算符周围使用空格,但是要注意二元运算符周围的空格使用保持一致的方式。
Yes: x == 1
No: x<1
当传递关键字参数时千万不要在赋值符号=
两边使用空格,只有当有类型注释的时候,定义默认参数值时才在=
两边使用空格。否则不要在默认参数值的=
两边使用空格。
Yes: def complex(real, imag=0.0): return Magic(r=real, i=imag)
Yes: def complex(real, imag: float = 0.0): return Magic(r=real, i=imag)
No: def complex(real, imag = 0.0): return Magic(r = real, i = imag)
No: def complex(real, imag: float=0.0): return Magic(r = real, i = imag)
不要再连续行中使用空格垂直对齐符号,这样会引发维护负担。(应用于:
,#
,=
等):
Yes:
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo': 1,
'long_name': 2,
}
No:
foo = 1000 # comment
long_name = 2 # comment that should not be aligned
dictionary = {
'foo' : 1,
'long_name': 2,
}
3.7 Shebang Line(系统行)
大多数.py
文件不需要以#!
行开始。程序main文件以行#!/usr/bin/python
开始,该行后可以根据PEP-394选填一个数字后缀2
或3
。(指定python版本)
内核使用这一行来寻找python编译器,但是在导入模块时python会忽略该行。它只在直接执行的文件中才是必须的。
3.8 Comments and Docstrings(注释和文档字符串)
确认对模块、函数和方法的文档字符串以及行内注释使用合适的风格。
3.8.1 Docstrings(文档字符串)
python用docstrings来注释代码。一个文档字符串就是在一个包、模块、类或函数中的第一个声明字符串。这些字符串可以被对象的__doc__
成员自动解析并被pydoc
使用。
对你的模块运行pydoc
看看出现什么。文档字符串总是用3个双引号"""
格式,参考PEP
257。一个文档字符串应当被组织为一行摘要或一个物理行,并以一个句点、问号或者叹号结束。后面跟一个空行,然后是以与第一行第一句话光标相同位置开始的其他文档字符串。下面有更多关于格式的指导规则。
3.8.2 Modules(模块)
每个文件都应包括许可版本。选择适合项目的证书版本,例如Apache 2.0、BSD、LGPL和GPL等。
3.8.3 Functions and Methods(函数和方法)
在本章见中使用的"function(函数)"适用于方法、函数和生成器。
一个函数必须有一个文档字符串,除非满足下面所有的标准:
- 外部不可见
- 很短
- 很明显
文档字符串应该提供足够的信息,使得不用阅读函数的代码就可以调用函数。文档字符串应当具有描述性("""Fetches rows from a Bigtable."""
),不要用命令口气("""Fetch rows from a Bigtable."""
)。文档字符串应当描述函数的调用语法和语义,而不是它的实现。对于复杂的代码,注释紧跟代码要比文档字符串更合适。
覆盖了基类中的方法时,可以用简单的文档字符串让读者去查阅基类方法的文档字符串,比如"""See base class."""
。原则是在许多地方,在基方法中出现的文档字符串就没有必要再次重复。然而,如果重写的方法比被重写的方法有较大不同,或者需要提供重写后方法的细节(例如,记录额外的副作用),那么重写后的方法就需要有文档字符串,至少包括那些不同之处。
一个函数的某些方面应该文档化为几个特殊部分,下面进行罗列。每个部分以一个标题行开始,标题行以冒号结束。这些部分应当缩进两个空格,除了标题。
Args:
- 列出每个参数的名字。名字后面是描述,并用一个冒号和空格将两者分开。如果描述太长,一行的80个字符放不下,那就悬挂缩进2或4个空格。文件的其余部分保持一致(缩进2个就都缩进2个,4个亦然)。
- 如果代码不包含相应的类型注释,那么描述应该包含必要的类型说明。
- 如果函数接收
*foo
(可变参数类型)或者(和)**bar
(任意关键字参数),它们就以*foo
和**bar
的形式列出。
Returns: (或者 Yields:,对生成器而言)
- 描述返回值的类型和语义。如果函数返回None,那么这部分不是必须的。
- 如果文档字符串以Returns或Yields开头(例如,
"""Returns row from Bigtable as a tuple of strings."""
),或者开头语足以描述返回值,那么这部分也可以忽略。
Raises:
- 列出所有接口相应的异常。
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
"""Fetches rows from a Bigtable.
Retrieves rows pertaining to the given keys from the Table instance
represented by big_table. Silly things may happen if
other_silly_variable is not None.
Args:
big_table: An open Bigtable Table instance.
keys: A sequence of strings representing the key of each table row
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{'Serak': ('Rigel VII', 'Preparer'),
'Zim': ('Irk', 'Invader'),
'Lrrr': ('Omicron Persei 8', 'Emperor')}
If a key from the keys argument is missing from the dictionary,
then that row was not found in the table.
Raises:
IOError: An error occurred accessing the bigtable.Table object.
"""
3.8.4 Classes(类)
类定义下面应当有文档字符串来描述这个类。如果你的类里有公有属性,它们应当在属性部分进行文档化,并且跟函数的参数部分遵循一样的格式化规则。
class SampleClass(object):
"""Summary of class here.
Longer class information....
Longer class information....
Attributes:
likes_spam: A boolean indicating if we like SPAM or not.
eggs: An integer count of the eggs we have laid.
"""
def __init__(self, likes_spam=False):
"""Inits SampleClass with blah."""
self.likes_spam = likes_spam
self.eggs = 0
def public_method(self):
"""Performs operation blah."""
3.8.5 Block and Inline Comments(块和内联注释)
最后一个写注释的地方是代码中棘手的部分。如果在下一步 code
review 中你必须要解释它的话,你应该现在就对它做一下注释。一些复杂的操作应该在开始之前就写几行注释。意图不容易阅读的代码,在该行后面加一行注释。
# We use a weighted dictionary search to find out where i is in
# the array. We extrapolate position based on the largest num
# in the array and the array size and then do binary search to
# get the exact number.
if i & (i-1) == 0: # True if i is 0 or a power of 2.
为了提高可读性,这些注释应该离代码至少2个空格远。
另一方面,千万不要描述代码。想像一下,读代码的人如果比你还了解python...(尽管你不希望这样)
# BAD COMMENT: Now go through the b array and make sure whenever i occurs
# the next element is i+1
3.8.6 Punctuation, Spelling and Grammar(标点、拼写和语法)
注意一下标点、拼写和语法,写得好的注释比那些糟糕的更容易阅读。
注释应该跟叙事性文本一样具有可读性,有合适的大小写以及标点。许多情况下,一个完整的句子要比一个句子片段可读性更好。更短的注释,比如一行代码后面的注释,有时不那么正式,但是你应该保持你的代码风格一致。
尽管让一个代码审查员指出本应该用分号的时候用了逗号让你感到有点沮丧,但是保持源代码高度的清晰性和可读性非常重要。恰当的标点、拼写和语法大有裨益。
3.9 Classes(类)
如果一个类没有继承自其他类,那么让它显式继承自object
类。这对嵌套类也一样。
Yes: class SampleClass(object):
pass
class OuterClass(object):
class InnerClass(object):
pass
class ChildClass(ParentClass):
"""Explicitly inherits from another class already."""
No: class SampleClass:
pass
class OuterClass:
class InnerClass:
pass
在python2中为了让属性正常工作,继承自object
类是必要的;同时这样也可以避免你的代码与python3的一些潜在不兼容。它也定义了一些特殊的方法,这些方法实现了对象的默认语义,包括__new__
,__init__
,__delattr__
,__getattribute__
,__setattr__
,__hash__
,__repr__
,和__str__
。
3.10 Strings(字符串)
使用format
方法或者%
操作符来格式化字符串,即便参数全是字符串。然而你需要好好判断用+
还是%
(或format
)。
Yes: x = a + b
x = '%s, %s!' % (imperative, expletive)
x = '{}, {}'.format(first, second)
x = 'name: %s; score: %d' % (name, n)
x = 'name: {}; score: {}'.format(name, n)
x = f'name: {name}; score: {n}' # Python 3.6+
No: x = '%s%s' % (a, b) # use + in this case
x = '{}{}'.format(a, b) # use + in this case
x = first + ', ' + second
x = 'name: ' + name + '; score: ' + str(n)
避免在循环中使用+
和+=
操作符连接字符串。因为字符串是不可变的,这种做法会创建不必要的临时对象,结果导致平方时间复杂度,而不是线性复杂度。取而代之,把每一个子字符串放到列表里面,循环结束后用''.join
处理列表(或者把每个子字符串写到io.ButesIO
缓存中)。
Yes: items = ['']
for last_name, first_name in employee_list:
items.append('%s, %s ' % (last_name, first_name))
items.append('
')
employee_table = ''.join(items)
No: employee_table = ''
for last_name, first_name in employee_list:
employee_table += '%s, %s ' % (last_name, first_name)
employee_table += '
'
在一个文件中使用字符串引号时保持风格一致。选择'
或"
并一直使用。为了在字符串中避免转义符\\
而使用另一种引号是可以的。gpylint
强制执行这一点。
Yes:
Python('Why are you hiding your eyes?')
Gollum("I'm scared of lint errors.")
Narrator('"Good!" thought a happy Python reviewer.')
No:
Python("Why are you hiding your eyes?")
Gollum('The lint. It burns. It burns us.')
Gollum("Always the great lint. Watching. Watching.")
对跨多行的字符串来说,优先使用"""
而不是'''
。项目中,当前仅当对常规字符串使用'
时,才可选择对所有跨行的非文档字符串用'''
。当然,文档字符串要使用"""
。注意,使用隐式续行通常更简洁,因为多行字符串不会随着程序其他部分的缩进而流动。
Yes:
print("This is much nicer.\n"
"Do it this way.\n")
No:
print("""This is pretty ugly.
Don't do this.
3.11 Files and Sockets(文件和套接字)
当使用完文件和套接字时显式关闭它们。
让文件、套接字或其他类文件对象处于不必要的打开状态,会有很多隐患,包括:
- 消耗有限的系统资源,比如文件描述符。处理这种对象的代码在使用完毕后如果没有立即把资源归还给系统就会耗尽这些资源。
- 保持文件打开状态会阻止其他动作操作它们,比如移动或删除。
- 在整个程序中共享的文件或套接字在逻辑上被关闭后可能无意中被读取或写入。如果它们真的被关闭了,试图对它们进行读写会抛出异常,使这个问题及早被发现。
此外,尽管文件对象析构后文件和套接字会自动关闭,但是把文件对象的生命周期和文件的状态联系在一起不是一个好做法,有以下几个原因: - 运行时何时运行文件的析构是没有保证的。不同的python实现使用不同的内存管理机制,例如滞后垃圾回收,可能会任意(不确定)加大对象的生命周期。
- 对文件意想不到的引用可能让它的活动时间比预想的要长。例如在异常追踪、全局作用域内等。
处理文件的优选方式是用"with"
语句:
with open("hello.txt") as hello_file:
for line in hello_file:
print(line)
对于不支持"with"语句的类文件对象来说,用contextlib.closing()
:
import contextlib
with contextlib.closing(urllib.urlopen("http://www.python.org/")) as front_page:
for line in front_page:
print(line)
3.12 TODO Comments(TODO注释)
TODO
注释用来修饰临时代码、短期解决方案或者足够好但是不够完美的代码。
TODO
包含全部大写形式的字符串TODO
,后面跟人的姓名、邮箱地址或其他标识符或者TODO
所引用问题的最佳上下文,并用圆括号括起来。注释给出了还有什么需要去做。主要目的是让TODO
的格式一致,以便查询关于需求的细节。TODO
不是说提到的这个人要去解决这个问题。因此当你创建一个TODO
的时候,几乎总是写你自己的名字。
# TODO([email protected]): Use a "*" here for string repetition.
# TODO(Zeke) Change this to use relations.
如果你的TODO
是“在未来某个时间做什么事情”,那么确保是一个非常具体的时间(“2009年11月解决”)或者是一个具体的事件(“当所有客户能处理XML响应时删除此代码”)。
3.13 Imports formatting(导入的格式)
导入应当在单独的行中。例如:
Yes: import os
import sys
No: import os, sys
导入语句总是放在文件的顶端,就在模块注释和文档字符串后面并且在模块全局变量和常量前面。模块组织的顺序应当是从最常用的到最不常用的:
-
python标准库导入,例如:
import sys
-
第三方模块或包导入,例如:
import tensorflow as tf
-
代码库的子包导入,例如:
from otherproject.ai import mind
-
特定应用程序导入,它们是与本文件相同的顶级子包的一部分,例如:
from myproject.backend.hgwells import time_machine
在每个分组内,每个包根据模块的包的全路径以字典序排序,不计大小写。可以在导入的代码块之间用空行分隔。
import collections
import Queue
import sys
import argcomplete
import BeautifulSoup
import cryptography
import tensorflow as tf
from otherproject.ai import body
from otherproject.ai import mind
from otherproject.ai import soul
from myproject.backend.hgwells import time_machine
from myproject.backend.state_machine import main_loop
3.14 Statements(语句)
通常一行只有一条语句。
然而,你可以将测试结果跟测试放在相同行里,前提是整个语句在一行上。特别地,try
/except
不能这么做,因为try
和except
不能同时适应同一行。你只能对if语句这么做,前提是没有else语句。
Yes:
if foo: bar(foo)
No:
if foo: bar(foo)
else: baz(foo)
try: bar(foo)
except ValueError: baz(foo)
try:
bar(foo)
except ValueError: baz(foo)
3.15 Access Control(访问控制)
如果访问函数没什么意义,那你应该使用公有变量来替代访问函数,来避免python中函数调用带来的额外花销。当添加更多功能时你可以使用property
来保持语法一致。
另一方面,如果访问变得更加复杂,或者访问变量的花销变得很大,这时候应该使用函数中调用(参考nameing指导)比如get_foo()
和set_foo()
。如果过去的行为允许使用属性访问,不要把函数调用和属性绑定在一起。通过使用旧有的方法访问变量的任何代码都应该明显中断以便让它们意识到复杂度的变化。
3.16 命名
module_name
, package_name
, ClassName
, method_name
, ExceptionName
, function_name
, GLOBAL_CONSTANT_NAME
, global_var_name
, instance_var_name
, function_parameter_name
, local_var_name
。
函数名字、变量名字和文件名应当具有描述性,避免缩写。特别地,不要使用让项目外阅读者感到模糊或不熟悉的代码,也不要通过删除单词中的字母来进行缩写。
总是使用.py
文件扩展名,不要使用破折号。
3.16.1 避免使用的名字
- 除计数器和迭代器之外的单个字符名。在
try
/except
块中可以使用"e"作为异常标识符。 - 包或模块中使用破折号
-
__double_leading_and_trailing_underscore__
(双下划线开头和结尾的)名字(python保留)。
3.16.2 命名约定
- "Internal"意味着模块内部或者类内受保护的或私有的
- 头部添加单个下划线(
_
)会使模块变量和函数受保护(不包含from module import *
)。然而对实例变量和方法头部添加双下划线使类的变量和方法成为私有(使用名称编码)。不推荐双下划线的使用,因为它影响可读性和可测试性,而且它也不是真正的私有。 - 把相关的类和顶层方法一起放在一个模块中。跟java不一样,没有必要限制自己一个模块中只有一个类。
- 类名使用驼峰式方法,模块名使用小写和下划线 。虽然有许多老模块使用驼峰命名,但现在这种方式不再推荐因为当模块名和类名一样的时候会让人感到很困惑。(
from StringIO import StringIO
??) - 下划线可以出现在单元测试的以
test
开头的方法名中,来区分名字中的逻辑部分,即使那些部分使用驼峰命名。一种可能的模式是test
,例如_ testPop_EmptyStack
是可以的。没有一种正确的方法来命名测试方法。
3.16.3 文件命名
python文件名必须以.py
作为扩展名,而且不能包含破折号。这样它们才能被导入和测试。如果你想访问一个没有扩展名的可执行文件,请使用符号链接或使用包含exec "$0.py" "$@"
的简单bash。
3.16.4 源自吉多的建议
Type | Public | Internal |
---|---|---|
Packages | lower_with_under |
|
Modules | lower_with_under |
_lower_with_under |
Classes | CapWords |
CapWords |
Exceptions | CapWords |
|
Functions | lower_with_under() |
_lower_with_under() |
Global/Class Constants | CAPS_WITH_UNDER |
_CAPS_WITH_UNDER |
Global/Class Variables | lower_with_under |
_lower_with_under |
Instance Variables | lower_with_under |
_lower_with_under (protected) |
Method Names | lower_with_under() |
_lower_with_under (protected) |
Function/Method Parameters | lower_with_under |
|
Local Variables | lower_with_under |
虽然python支持使用前置双下划线作为名字前缀来让一些东西变为私有,但这种做法并不推荐。优先使用但下划线。它们更容易输入、阅读和从小型单元测试访问。lint警告处理对受保护成员的无效访问。
3.17 Main
即使一个文件想用做可执行文件,他也应该可以被导入。仅仅导入不应该对程序的main函数有影响。主要功能应该在main()
函数中。
在python中,pydoc
和单元测试要求模块能够被导入。在执行main程序之前,你的代码应当总是进行if __name__ == '__main__'
检查。这样,当模块被导入的时候,main函数不会执行。
def main():
...
if __name__ == '__main__':
main()
当模块被导入时,模块的所有顶层代码都会执行。当文件被pydoc
ed时2,注意不要进行函数调用、创建对象或其他不该执行的操作。
3.18 函数长度
函数要轻量、聚焦。
我们承认长函数有时是很合适的,所以对于函数长度没有强硬的限制。如果一个函数超过了40行,考虑一下在不改变程序结构前提下,函数能不能分解。
即使你的长函数现在依然完美运行,几个月后可能有人需要改变它来添加新功能。这可能导致难以发现的bug。保持函数简短可以让其他人更容易理解并改变你的代码。
当你处理某些代码时,你可能会发现又长又复杂的函数。不要被改变现有代码吓住:如果处理这样的函数很艰难,或发现错误很难调试,再或者你想在几个不同的环境中复用其代码段,那么考虑分解成更轻量的函数以及更容易维护的代码段。
3.19 Type Annotations(类型注释)
3.19.1 一般规则
- 熟悉PEP-484
- 方法中不要注释
self
或者cls
- 如果任何其他变量或返回类型不应该被表达,使用
Any
- 没有必要对模块中的所有函数进行注释
- 至少注释公共接口(API)
- 做好判断,在安全性清晰性和灵活性之间做好权衡
- 注释容易出现类型错误的代码(先前的错误或复杂度)
- 注释较难理解的代码
- 从类型的角度看,当代码稳定的时候注释代码。在许多情况下,你可以注释成熟代码的所有函数,也不会丧失太多灵活性
3.19.2 换行
遵循现有的indentation规则。总是优先在变量之间换行。
注释之后,很多函数变成“一个参数占一行”。
def my_method(self,
first_var: int,
second_var: Foo,
third_var: Optional[Bar]) -> int:
然而,如果所有代码适合一行,那就一行。
def my_method(self, first_var: int) -> int:
如果函数名、最后一个参数和返回类型放在一行太长,那就在新的一行缩进4个空格。
def my_method(
self, first_var: int) -> Tuple[MyLongType1, MyLongType1]:
如果返回类型跟最后一个参数放在一行不合适,更好的方式是在新的一行将所有参数缩进4个空格,并将右括号跟def对齐。
Yes:
def my_method(
self, **kw_args: Optional[MyLongType]
) -> Dict[OtherLongType, MyLongType]:
...
pylint
允许将右括号放在新行并和左括号对齐,但是这样可读性比较差。
No:
def my_method(self,
**kw_args: Optional[MyLongType]
) -> Dict[OtherLongType, MyLongType]:
...
如果单个名称或类型很长,考虑对类型使用alias。最后的手段是在冒号后面断行,并缩进4个字符。
Yes:
def my_function(
long_variable_name:
long_module_name.LongTypeName,
) -> None:
...
No:
def my_function(
long_variable_name: long_module_name.
LongTypeName,
) -> None:
...
3.19.3 前向声明
如果你需要在当前相同模块中使用一个类名,而这个类还没有进行定义。例如,如果你想在类定义中使用这个类,抑或你想使用一个在下方定义的类,那么使用类名的字符串形式。
class MyClass(object):
def __init__(self,
stack: List["MyClass"]) -> None:
3.19.4 默认值
依据PEP-008,当结合使用参数注释和默认值时,'='两边都要添加空格(但仅对同时使用注释和默认值的参数适用)。
Yes:
def func(a: int = 0) -> int:
...
No:
def func(a:int=0) -> int:
...
3.19.5 NoneType
在python类型系统中,NoneType是“第一类”类型,处于方便输入的目的,None
是Nonetype
的别名。如果一个参数可以是None
,那么必须要显示声明。你可以使用Union
,但如果只有一个其他类型,Optional
更方便。
Yes:
def func(a: Optional[str]) -> str:
...
No:
def func(a: Union[None, str]) -> str:
...
如果一个参数的默认值是None
,那么Optional
变量是可选的。
Yes:
def func(a: Optional[str] = None) -> str:
...
def func(a: str = None) -> str:
...
3.19.6 Type Aliases(类型别名)
你可以为复杂的类型声明别名。别名的名称应当使用驼峰命名。描述该组合类型并以Type
结尾(对于元组使用Types
)。如果别名只在本模块中使用,那么应该私有化(使用_
)。
例如,如果模块与类型的名字连起来很长时:
SomeType = module_with_long_name.TypeWithLongName
其他例子是复杂嵌套类型和函数的多个返回变量(比如元组)。
3.19.7 忽略类型
你可以用特殊的注释# type: ignore
来禁用某一行的类型检查。pytype
对特定的错误有禁用的功能(类似lint)。
# pytype: disable=attribute-error
3.19.8 内部变量的类型
如果内部变量的类型很难或不可能推断出来,那么你可以提供一行特殊的注释:
a = SomeUndecoratedFunction() # type: Foo
3.19.9 元组 vs 列表
与列表只有一种类型不同,元组既可以有一种单一的类型,也可以有一组不同类型的元素。后者通常用在函数中的返回类型。
a = [1, 2, 3] # type: List[int]
b = (1, 2, 3) # type: Tuple[int, ...]
c = (1, "2", 3.5) # type Tuple[int, str, float]
3.19.10 TypeVar
python系统类型也有泛型。工厂函数TypeVar
是使用泛型的通用方法。例如:
from typing import List, TypeVar
T = TypeVar("T")
...
def next(l: List[T]) -> T:
return l.pop()
TypeVar可以添加约束条件:
AddableType = TypeVar("AddableType", int, float, str)
def add(a: AddableType, b: AddableType) -> AddableType:
return a + b
在typing
模块中常见的预定义类型变量是AnyStr
,它主要用于可以是bytes
或unicode
的参数。
AnyStr = TypeVar("AnyStr", bytes, unicode)
3.19.11 字符串类型
当注释接收字符串参数或返回字符串的函数时,避免使用str
,因为在python2和python3中它是不一样的。在Python 2中, str
是bytes
;在Python 3中,它是unicode
。所以尽可能显式使用。
No:
def f(x: str) -> str:
...
处理byte数组的代码,就用bytes
:
def f(x: bytes) -> bytes:
...
处理Unicode数组的代码就用Text
:
from typing import Text
...
def f(x: Text) -> Text:
...
如果数组既可以是byte又可以是Unicode就用Union
:
from typing import Text, Union
...
def f(x: Union[bytes, Text]) -> Union[bytes, Text]:
...
如果函数的字符串类型总是相同的,例如上面代码的参数类型和返回类型一样,这时使用AnyStr。
3.19.12 Imports For Typing(类型导入)
typing
模块中的类,总是导入类本身。这时允许你在一行代码中从typing
模块中导入多个类。例如:
from typing import Any, Dict, Optional
考虑到以这种方式从typing
模块中导入类会在本地名称空间中添加其中的项目。所以typing
中出现的名字都简单地当作关键字处理。因此就不要在你的python代码中再次定义,无论使用与否。如果类型和模块中已有的名字产生冲突,那么用import x as y
方式导入。
from typing import Any as AnyType
如果需要在运行时避免类型检查所需的附加导入,则可使用条件导入。不推荐这种模式,可以选择重构代码以允许顶层导入等替代方法。如果终究还是使用了这种模式,那么条件导致的类型需要使用字符串'sketch.Sketch'
而不是sketch.Sketch
。这是为了与注释表达式实际求值的python3前向兼容。仅仅类型注释才会需要的导入放在if typing.TYPE_CHECKING:
块中。
- 只定义跟typing相关的实体,包括别名。否则会出现运行时错误,因为运行时模块不会被导入。
- 这部分代码块应刚好放在普通导入之后
- 在typing的导入列表中不应有空行
- 像普通列表一样给这个列表排序,但是把有关typing模块的导入放在最后。
-
google3
模块也有一个TYPE_CHECKING
常量。如果你不想在运行时导入typing
,可以用它来作为替代。
import typing
...
if typing.TYPE_CHECKING:
import types
from MySQLdb import connections
from google3.path.to.my.project import my_proto_pb2
from typing import Any, Dict, Optional
3.19.13 Circular Dependencies(循环依赖)
由typing引起的循环依赖属于代码异味。这样的代码最好拿来重构。尽管技术上可以做到循环依赖,但是编译系统不允许这么做,因为每个模块都必须依赖于另一个。
用Any
替换引发循环依赖的导入。给alias一个有意义的名字,并使用模块中真实的类型名称(Any中的任何属性都是Any)。别名定义与最后一行导入隔开一行。
from typing import Any
some_mod = Any # some_mod.py imports this module.
...
def my_method(self, var: some_mod.SomeType) -> None:
...
4 Parting Words(写在最后的话)
保持一致。
如果你在编辑代码,那么花几分钟看一下你的代码,然后确定它的风格。如果算数运算符两旁都有空格,你应该保持这样。如果代码注释周围有哈希标记,你写的代码也应该要有。
风格指南的意义是提供一个编码词汇表,因此人们会更关注于要说什么而不是要怎么说上面。我们在这里给出总体的风格指南,让人们了解这个词汇表;但是自己的风格也很重要。如果你在一个文件中添加的代码与文件中原有的代码风格相差太大,那么读者读代码的时候就会云里雾里,避免这样。