首页 > temp > 简明python教程 >
-
Python 代码风格指南谷歌版(3)
3.7 文件和socket
当使用结束后显式地关闭文件或socket.
在非必要的情况下,让文件,socket或者其他文件方式的对象打开着有很多缺点:
-
他们可能会消耗有限的系统资源,例如文件描述符.如果在使用没有即使归还系统,处理很多这样对象的代码可能会浪费掉很多不应浪费的资源.
-
保持一个文件可能会阻止其他操作诸如移动或删除.
-
被程序共享的文件和socket可能会无意中在逻辑上已被关闭的情况下仍被读写.如果实际上已经关闭,试图读写的操作会抛出异常,这样就可以立即发现问题.
此外,当文件或socket在文件对象被销毁的同时被自动关闭的时候,是不可能将文件的生命周期和文件状态绑定的:
-
不能保证合适会真正将文件对象销毁.不同的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注释
对于下述情况使用TODO注释:临时的,短期的解决方案或者足够好但是不完美的解决方案.
TODO注释以全部大写的字符串TODO开头,并带有写入括号内的姓名,email地址,或其他可以标识负责人或者包含关于问题最佳描述的issue.随后是这里做什么的说明.
有统一风格的TODO的目的是问了方便搜索并了解如何获取更多相关细节.TODO并不是保证被提及者会修复问题.因此在创建TODO注释的时候,基本上都是给出你的名字.
# TODO(kl@gmail.com): Use a "*" here for string repetition.
# TODO(Zeke) Change this to use relations.
如果TODO注释形式为"未来某个时间点会做什么事"的格式,确保要么给出一个非常具体的时间点(例如"将于2009年11月前修复")或者给出一个非常具体的事件(例如"当所有客户端都能够处理XML响应时就移除此代码").
3.13 import格式
imports应该在不同行.例如:
Yes:
import os
import sys
No:
import os, sys
import应集中放在文件顶部,在模块注释和docstring后面,模块globals和常量前面.应按照从最通用到最不通用的顺序排列分组:
-
Python未来版本import语句,例如:
from future__ import absolute_import from _future_ import division from __future import print_function
更多信息参看上文 -
Python标准基础库import,例如:
import sys -
第三方库或包的import,例如:
import tensorflow as tf -
代码库内子包import,例如:
from otherproject.ai import mind -
此条已弃用:和当前文件是同一顶级子包专用的import,例如:
from myproject.backend.hgwells import time_machine
在旧版本的谷歌Python代码风格指南中实际上是这样做的.但是现在不再需要了.新的代码风格不再受此困扰.简单的将专用的子包import和其他子包import同一对待即可.
在每个组内按照每个模块的完整包路径的字典序忽略大小写排序.可以根据情况在每个节质检增加空行.
import collectionsimport queueimport sys
from absl import appfrom absl import flagsimport bs4import cryptographyimport tensorflow as tf
from book.genres import scififrom myproject.backend.hgwells import time_machinefrom myproject.backend.state_machine import main_loopfrom otherproject.ai import bodyfrom otherproject.ai import mindfrom otherproject.ai import soul
# Older style code may have these imports down here instead:
# 旧版本代码风格可能会采用下述import方式
# from myproject.backend.hgwells import time_machine
# from myproject.backend.state_machine import main_loop
3.14 语句
每行只有一条语句.
不过如果测试语句和结果能够在一行内放下,就可以放在一行内.但是不允许将try/except语句和对应内容放于一行,因为try或者except都不能在一行内完成.对于没有else的if语句可以将if和对应内容合并到一行.
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 访问
对于琐碎又不太重要的访问函数,应用公共变量来替代访问函数,以避免额外的程序调用消耗,当添加了更多函数功能时,使用property来保持连续性
此外,如果访问过于复杂,或者访问变量的消耗过大,应该使用诸如get_foo()和set_foo()之类的函数式访问(参考命名指南).如果过去的访问方式是通过属性,新访问函数不要绑定到property上,这样使用property的旧方式就会失效,使用者就会知道函数有变化.
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 要避免的名字:
-
单字符名字,除非是计数或迭代元素,e可以作为Exception捕获识别名来使用..
-
-横线,不应出现在任何包名或模块名内
-
double_leading_and_trailing_underscore首尾都双下划线的名字,这种名字是python的内置保留名字
3.16.4 命名约定
-
internal表示仅模块内可用、或者类内保护的或者私有的
-
单下划线()开头表示是被保护的(from module import *不会import).双下划线(_也就是"dunder")开头的实例变量或者方法表示类内私有(使用命名修饰).我们不鼓励使用,因为这会对可读性和可测试性有削弱二期并非真正的私有.
-
相关的类和顶级函数放在同一个模块内,不必像是Java一样要一个类放在一个模块里.
-
对类名使用大写字母(如CapWords)开头的单词,命名,模块名应该使用小写加下划线的方式.尽管有一些旧的模块命名方式是大写字母的(如CapWords.py),现在不鼓励这样做了,因为在模块刚好是从某个类命名出发的时候可能会令人迷惑(例如是选择import StringIO还是from StringIO import StringIO?)
-
在unittest方法中可能是test开头来分割名字的组成部分,即使这些组成部分是使用大写字母驼峰式的.这种方式是可以的:test
_例如testPop_EmptyStack,对于命名测试方法没有明确的正确方法.
3.16.3 文件名
文件拓展名必须为.py,不可以包含-.这保证了能够被正常import和单元测试.如果希望一个可执行文件不需要拓展名就可以被调用,那么建立一个软连接或者一个简单的bash打包脚本包括exec "@".
3.16.4 Guido的指导建议
类型 公共 内部
包 lower_with_under
模块 lower_with_under _lower_with_under
类 CapWords _CapWords
异常 CapWords
函数 lower_with_under() _lower_with_under()
全局/类常量 CAPS_WITH_UNDER _CAPS_WITH_UNDER
全局/类变量 lower_with_under _lower_with_under
实例变量 lower_with_under _lower_with_under(受保护)
方法名 lower_with_under() _lower_with_under()(受保护)
函数/方法参数 lower_with_under
局部变量 lower_with_under
尽管Python支持通过双下划线__(即"dunder")来私有化.不鼓励这样做.优先使用单下划线.单下划线更易于打出来、易读、易于小的单元测试调用.Lint的警告关注受保护成员的无效访问.
3.17 Main
即便是一个用做脚本的py文件也应该是可以被import的,而只用于import时,也不应有执行了主函数的副作用.主函数的功能应该被放在main()里.
在Python中,pydoc和单元测试要求模块是可import的.所以代码在主程序执行前应进行if name__ == '__main':检查,以防止模块在import时被执行.
def main():
...
if __name__ == '__main__':
main()
所有顶级代码在模块被import时执行.因而要小心不要调用函数,创建对象或者执行其他在执行pydoc时不应该被执行的操作.
3.18 函数长度
优先写小而专一的函数.
长函数有时候是科室的,故而函数长度没有固定的限制.但是超过40行的时候就要考虑是否要在不影响程序结构的前提下分解函数.
尽管长函数现在运行的很好,但是在之后的时间里其他人修改函数并增加新功能的时候可能会引入新的难以发现的bug,保持函数的简短,这样有利于其他人读懂和修改代码.
在处理一些代码时,坑发现有些函数长而且复杂.不要畏惧调整现有代码,如果处理这个函数非常困难,如难以以对报错debug或者希望在许多不同上下文环境里使用代码的局部,那么将函数拆解成若干个更小更可控的片段.
3.19 类型注释
3.19.1 基本规则
-
熟悉PEP-484
-
在方法中,只在必要时给self或者cls增加合适的类型信息.例如@classmethod def create(cls: Type[T]) -> T: return cls()
-
如果其他变量或返回类型不定,使用Any
-
不需要注释每个函数
-
至少需要注明公共接口
-
使用类型检查来在安全性和声明清晰性以及灵活性之间平衡
-
标注容易因类型相关而抛出异常的代码(previous bugs or complexity,此处译者认为是与上一条一致,平衡安全性和复杂性)
-
标注难理解的代码
-
标注类型稳定的代码,成熟稳定的代码可以都进行标注而不会影响其灵活性
3.19.2 分行
遵循现有的缩进规范
标注类型后,函数签名多数都要是"每行一个参数".
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对齐.
def my_method(
self, other_arg: Optional[MyLongType]) -> Dict[OtherLongType, MyLongType]:
...
就像上面的例子一样,尽量不要分割类型注释,不过有时类型注释太长无法放入一行,(那就尽量让子注释不要被分割).
def my_method(
self,
first_var: Tuple[List[MyLongType1],
List[MyLongType2]],
second_var: List[Dict[
MyLongType3, MyLongType4]]) -> None:
...
如果某个命名和类型太长了,考虑使用别名.如果没有其他解决方案,在冒号后分行缩进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.
显式地使用Optional而非隐式地.PEP 484的早期版本容许a: Text = None被解释为a: Optional[Text] = None.但现在已经不推荐这样使用了.
Yes:
def func(a: Optional[Text], b: Optional[Text] = None) -> Text:
...
def multiple_nullable_union(a: Union[None, Text, int]) -> Text
...
No:
def nullable_union(a: Union[None, Text]) -> Text:
...
def implicit_optional(a: Text = None) -> Text:
...
3.19.6 类型别名
可以对复杂类型声明别名,同样大写字母驼峰命名,如果只用于当前模块,应加下划线私有化.
例如,如果带有模块名的类型名过长:
_ShortName = module_with_long_name.TypeWithLongNameComplexMap = Mapping[Text, List[Tuple[int, int]]]
3.19.7 忽略类型检查
可以通过增加特殊行注释# type: ignore来禁止类型检查.
pytype对于明确的报错有关闭选项(类似于lint):
# pytype: disable=attribute-error
3.19.8 对变量注释类型
对变量标注类型如果内部变量很难或者不可能指向,可以使用下述方式:
类型注释:
在行尾增加以# type开头的注释
a = SomeUndecoratedFunction() # type: Foo
注释绑定:
在变量名和赋值之间用冒号和类型注明,和函数参数一致.
a: Foo = SomeUndecoratedFunction()
3.19.9 元组和列表
不像是列表只能包含单一类型,元组可以既只有一种重复类型或者一组不同类型的元素,后者常用于函数返回.
a = [1, 2, 3] # type: List[int]
b = (1, 2, 3) # type: Tuple[int, ...]
c = (1, "2", 3.5) # type: Tuple[int, Text, float]
3.19.10 TypeVars
Python是有泛型的,工厂函数TypeVar是通用的使用方式.
例子:
from typing import List, TypeVarT = TypeVar("T")...def next(l: List[T]) -> T:
return l.pop()
TypeVar可以约束类型:
AddableType = TypeVar("AddableType", int, float, Text)def add(a: AddableType, b: AddableType) -> AddableType:
return a + b
在typing模块预定义好的类型变量是AnyStr,用于针对字符串可以是bytes也可为unicode并且保持一致的多个类型注释.
3.19.11 字符串类型
注释字符串的合适类型是基于Python版本的.
对于只有Python3的代码,使用str,Text可以用但是在选择上保持一致.
对于Python2兼容的代码,用Text,在一些很罕见的情况下,str可能可用.当在不同Python版本之间返回值类型不同的时候通常是为了照顾兼容性.避免使用unicode,因为Python3中不存在.
No:
def py2_code(x: str) -> unicode:
...
Yes:
def deals_with_binary_data(x: bytes) -> bytes:
...
对于Python2兼容,处理文本数据(Python中str或unicode,Python3中str)的代码,使用Text.对于只有Python3的代码,优先使用str.
from typing import Text...def py2_compatible(x: Text) -> Text:
...def py3_only(x: str) -> str:
...
如果既可以是byte也可以是文本,那么使用Union和合适的文本类型.
from typing import Text, Union...def py2_compatible(x: Union[bytes, Text]) -> Union[bytes, Text]:
...def py3_only(x: Union[bytes, str]) -> Union[bytes, str]:
...
如果一个函数中所有的字符串类型始终一致,例如前文例子中返回值类型和参数类型是一致的,那么使用AnyStr
像这样写能够简化代码向Python3的迁移过程.
3.19.12 typing的import
对于从typing模块import的类,要import类本身.明确的允许在一行内从typing模块import多个特定的类,如
from typing import Any, Dict, Optional
这种从typing模块import的方式会向命名空间内增加额外项,typing中的任何命名都应该和关键字同等对待并且不在你的Python代码中定义,typed or not(译者推测文无论是否引入).如果和已有的命名冲突,使用import x as y来import.
from typing import Any as AnyType
3.19.13 条件import
只在运行时一定要避免进行类型检查的情况下使用条件import.不鼓励使用这种模式.鼓励使用其他替代方式诸如重构代码以容许顶级import.
只用于类型注释的import可以被归于if TYPE_CHECKING:代码块中.
-
条件import的类型应被视为字符串引用,以和Python3.6兼容(在Python3.6中,注释表达式实际上被赋值的).
-
只有单独用于类型注释的实例才能在这里定义,包括了别名.否则将会报运行错误因为在运行时这些模块不会被引用.
-
代码块应该紧跟在正常import后面.
-
在类型import后不应有空行
-
按照正常import顺序对这一块代码进行排序
import typingif typing.TYPE_CHECKING:
import sketchdef f(x: "sketch.Sketch"): …
3.19.14 循环依赖
由于类型检查引发的循环依赖是一种code smell(代码异味),这样的代码应当被重构.尽管技术上是可以保留循环引用的.build system(系统)不允许这样做因为每个模块都要依赖于其他模块.
将造成循环依赖的模块替换为Any并赋予一个有意义的别名并使用从这个模块导入的真实类名(因为任何Any的属性都是Any).别名的定义用和最后一行import用一行空行分隔.
from typing import Any
some_mod = Any # some_mod.py imports this module.
...
def my_method(self, var: some_mod.SomeType) -> None:
...
3.19.15 泛型
当注释的时候,优先泛型类型专有类型参数,否则泛型的参数会被认为是Any.
def get_names(employee_ids: List[int]) -> Dict[int, Any]:
...
# These are both interpreted as get_names(employee_ids: List[Any]) -> Dict[Any, Any]
def get_names(employee_ids: list) -> Dict:
...
def get_names(employee_ids: List) -> Dict:
...
如果泛型最佳的参数类型是Any也将其显式地表示出来.但是在很多情况下TypeVar可能更合适.
def get_names(employee_ids: List[Any]) -> Dict[Any, Text]:
"""Returns a mapping from employee ID to employee name for given IDs."""
T = TypeVar('T')def get_names(employee_ids: List[T]) -> Dict[T, Text]:
"""Returns a mapping from employee ID to employee name for given IDs."""
4 最后的话
保持一致
如果你在编辑代码,花几分钟看看代码然后决定好要是用那种风格.如果现有代码在所有算术运算符两侧都加了空格,那么你也应该如此.如果现有的注释用连字符组成了包围框,那么你的注释也应如此.
有代码风格指南的目的是有一个变成的共识,这样人们能够集中在内容而非形式上.我们将通用的代码风格指南公布与此这样人们就能了解这个共识(译者:有巴别塔的意味.)但是各自的代码风格也很重要.如果你添加的代码与原有代码看起来完全不一致,就会打乱读者的阅读节奏.避免这样.