Google 内部的 Python 代码风格指南

图片

来自:Why GitHub?  

https://github.com/shendeguize/GooglePythonStyleGuideCN

这是一位朋友翻译的 Google Python代码风格指南,很全面。可以作为公司的 code review 标准,也可以作为自己编写代码的风格指南,希望对你有帮助

Translator: shendeguize@github

Link: https://github.com/shendeguize/GooglePythonStyleGuideCN

本翻译囿于水平,可能有不准确的地方,欢迎指出,谢谢大家

1、背景

Python 是谷歌主要使用的动态语言,本风格指导列举了使用 Python 编程时应该做和不该做的事项( dos & don’ts )

为了帮助你正确地组织代码,我们编写了一个 Vim 的设置文件.对于 Emacs,默认设置即可.

许多团队使用 yapf 自动格式工具来避免格式争议

2、Python语言规则

2.1 Lint

对代码使用pylint

2.1.1Definition(以下都译为定义)

pylint是一个用于在Python代码中发现bug和代码风格问题的工具,,pylint查找那些常在非动态语言(例如C或C++)编译器中捕获的问题.由于Python是动态语言,一些警告可能不正确,不过应该非常少有错误警告.

2.1.2 Pros

能够发现一些易被遗漏的错误,类似拼写错误,调用早于声明等等.

2.1.3 Cons

pylint并不完美,为了更好的利用工具,我们有时候需要

a. Write around it(适配上下文风格)

b. 压制一些警告

c. 优化工具

2.1.4 Decision(以下都译为建议)

确保对代码应用pylint

如果一些警告是不合适的,就抑制这些警告,这是为了让其他警告不会被隐藏.为了压制警告,可以设置行级别的注释:

dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

pylint警告包含标识名(empty-docstring),谷歌专有的警告以g-开头.

如果抑制警告的原因在标识名称中表述不够清晰,请额外添加注解.

用这种方式来抑制警告的优点是我们能够简单地找到抑制的警告并且重新访问这些警告.

可以通过下述方式来获得pylint警告列表:

pylint --list-msgs

用下述方式来获取某个特定消息的更多具体信息:

pylint --help-msg=C6409

优先使用pylint: disable而非旧方法(pylint: disable-msg)如果要抑制由于参数未使用的警告,可以在函数开头del,并注释为什么要删除这些未使用参数,仅仅一句”unused”是不够的:

def viking_cafe_order(spam, beans, eggs=None):
    del beans, eggs  # Unused by vikings.
    return spam + spam + spa

其他可以用来抑制警告的方式包括用'_'作为未使用参数的标识,在参数名前增加'unused_',或者分配这些参数到'_'.这些方式是可以的,但是已经不鼓励继续使用.前两种方式会影响到通过参数名传参的调用方式,而最后一种并不能保证参数确实未被使用.

2.2 Imports

只在import包和模块的时候使用import,而不要应用在单独的类或函数.(这一条对于typing_module有特别的意外)

2.2.1 定义

一个模块到另一个模块之间共享代码的复用性机制

2.2.2 Pros

命名空间管理约定简单,每个标识的源都一致性地被指明了.例如x.Obj表示Obj是在模块x中定义的

2.2.3 Cons

模块名可能会有冲突,一些模块名可能很长,比较不方便

2.2.4 建议

  • import x(当x是包或模块)
  • from x import y (当x是包前缀,y是不带前缀的模块名)
  • from x import  y as z (当有重复模块名yy过长不利于引用的时候)
  • import y as z (仅在非常通用的简写的时候使用例如import numpy as np

sound.effects.echo为例:

from sound.effects import echo...echo.EchoFilter(input, output, delay=0.7, atten=4)

不要使用相对引用,即便在同一包内,也使用完整包名import,这有助于避免无意重复import包.

从typing module和six.moves module import不适用上述规则

2.3 包

每一模块都要从完整路径import

2.3.1 Pros

能够避免模块名冲突以及由于模块搜索路径与作者预期不符而造成的错误引用.让查找模块更简单.

2.3.2 Cons

让部署代码时有些困难,因为包架构也需要赋值,不过对于现在的部署机制而言,这其实不是问题.

2.3.3 建议

所有的新代码都要从完整包名来import模块

import示例应该像这样:

Yes:

# Reference absl.flags in code with the complete name (verbose).
# 在代码中使用完整路径调用absl.flags
import absl.flagsfrom doctor.who import jodie

FLAGS = absl.flags.FLAGS
# Reference flags in code with just the module name (common).
# 在代码中只用包名来调用flags
from absl import flagsfrom doctor.who import jodie

FLAGS = flags.FLAGS

No:(假设文件在doctor/who中,jodie.py也在这里)

# Unclear what module the author wanted and what will be imported.  The actual
# import behavior depends on external factors controlling sys.path.
# Which possible jodie module did the author intend to import?
# 不清楚作者想要哪个包以及最终import的是哪个包,
# 实际的import操作依赖于受到外部参数控制的sys.path
# 那么哪一个可能的jodie模块是作者希望import的呢?
import jodie

不应该假设主代码所在路径被包含在sys.path中,即使有些时候可以work.在上一例代码中,我们应该认为import jodie指的是import一个叫做jodie的第三方包或者顶级目录中的jodie,而非一个当前路径的jodie.py

2.4 异常

异常处理是允许使用的,但使用务必谨慎

2.4.1 定义

异常是一种从正常代码段控制流中跳出以处理错误或者其他异常条件的手段.

2.4.2 Pros

正常代码的控制流时不会被错误处理代码影响的.异常处理同样允许在某些情况下,控制流跳过多段代码,例如在某一步从N个嵌入函数返回结果而非强行延续错误代码.

2.4.3 Cons

可能会让控制流变的难于理解,也比较容易错过调用库函数的报错.

2.4.4 建议

异常必定遵循特定条件:

  • 使用raise MyError('Error message')或者raise MyError(),不要使用两段raise MyError, 'Error message'
  • 当内置异常类合理的时候,尽量使用内置异常.例如:抛出ValueError来表示一个像是违反预设前提(例如传参了一个负数给要求正数的情况)的程序错误发生.

不要使用assert来片段公共结构参数值.assert是用来确认内部计算正确性也不是用来表示一些预期外的事件发生的.如果异常是后续处理要求的,用raise语句来处理,例如:

Yes:

def connect_to_next_port(self, minimum):
"""Connects to the next available port.

Args:
    minimum: A port value greater or equal to 1024.

Returns:
    The new minimum port.

Raises:
    ConnectionError: If no available port is found.
"""

if minimum < 1024:
    # Note that this raising of ValueError is not mentioned in the doc
    # string's "Raises:" section because it is not appropriate to
    # guarantee this specific behavioral reaction to API misuse.
    # 注意抛出ValueError这件事是不在docstring中的Raises中提及, 因为这样并适合保障对于API误用的特殊反馈
    raise ValueError('Minimum port must be at least 1024, not %d.' % (minimum,))
port = self._find_next_open_port(minimum)
if not port:
    raise ConnectionError('Could not connect to service on %d or higher.' % (minimum,))
assert port >= minimum, 'Unexpected port %d when minimum was %d.' % (port, minimum)
return port

No:

def connect_to_next_port(self, minimum):
"""Connects to the next available port.

Args:
    minimum: A port value greater or equal to 1024.

Returns:
    The new minimum port.
"""

assert minimum >= 1024'Minimum port must be at least 1024.'
port = self._find_next_open_port(minimum)
assert port is not None
return port
  • 库或者包可能会定义各自的异常.当这样做的时候,必须要继承一个已经存在的异常类,异常类的名字应该以Error结尾,并且不应该引入重复(foo.FooError)

  • 永远不要用捕获全部异常的except:语句,或者捕获Exception或者StandardError除非:

    Python在这个方面容忍度很高,并且except:语句会捕获包括拼写错误,sys.exit(),Ctrl+C终止,单元测试失败和和所有你并没有想到捕获的其他异常.

    • 再次抛出这个异常
    • 在程序中异常不会继续但是会被记录以及消除(例如通过保护最外层的方式保护线程不会崩溃)的地方创造一个孤立点.
  • 最精简try/except表达式内部的代码量,try代码块里的代码体量越大,月可能会在你不希望抛出异常的代码中抛出异常,进而在这种情况下,try/except掩盖了一个真实的异常

  • 使用finally来执行代码,这些代码无论是否有异常在try代码块被抛出都会被执行.这在清理(即关闭文件)时非常有用.

  • 当捕获了异常时,用as而不是逗号分段.

try:
    raise Error()
except Error as error:
    pass

2.5 全局变量

避免全局变量

2.5.1 定义

在模块级别或者作为类属性声明的变量

2.5.2 Pros

有些时候有用

2.5.3 Cons

在import的过程中,有可能改变模块行为,因为在模块首次被引入的过程中,全局变量就已经被声明

2.5.4 建议

避免全局变量

作为技术变量,模块级别的常量是允许并鼓励使用的.例如MAX_HOLY_HANDGRENADE_COUNT = 3, 常量必须由大写字母和下划线组成,参见下方命名规则

如果需要,全局变量需要在模块级别声明,并且通过在变量名前加_来使其对模块内私有化.外部对模块全局变量的访问必须通过公共模块级别函数,参见下方命名规则

2.6 内嵌/局部/内部 类和函数

内嵌局部函数或类在关闭局部变量时是可以的.内部类意识可用的.(译注:这里我的理解是当内嵌局部函数或类是和局部变量在同一个封闭作用域内是可以的.)

2.6.1 定义

类可以在方法,函数,类内定义.函数可以在方法或函数内定义.内嵌函数对封闭作用域的变量具有只读访问权限.

2.6.2 Pros

允许定义只在非常有限作用域内可用的工具类或工具函数.Very ADT-y(??符合抽象数据类型要求???),通常用于实现装饰器

2.6.3 Cons

内嵌或局部类的实例是不能被pickle的,内嵌函数或类是不能被直接测试的.嵌套会让外部函数更长并且更难读懂.

2.6.4 建议

除了一些特别声明,这些内嵌/局部/内部类和函数都是可以的.避免内嵌函数或类除了需要关闭一个局部值的时候.(译者理解可能是除了将局部变量封闭在同一个作用域的情况以外).不要把一个函数转为内嵌指示为了避免访问.在这种情况下,把函数置于模块级别并在函数名前加_以保证测试是可以访问该函数的.

2.7 列表推导和生成器表达式

在简单情况下是可用的

2.7.1 定义

List, Dict和Set推导生成式以及生成器表达式提供了一个简明有效的方式来生成容器和迭代器而不需要传统的循环,map(),filter()或者lambda表达式

2.7.2 Pros

简单地推导表达比其他的字典,列表或集合生成方法更加简明清晰.生成器表达式可以很有效率,因为完全避免了生成列表.

2.7.3 Cons

负载的推导表达式或生成器表达式很难读懂

2.7.4 建议

简单情况下使用时可以的.每个部分(mapping表达式,filter表达式等)都应该在一行内完成.多个for条款或者filter表达式是不允许的.当情况变得很复杂的适合就使用循环.

Yes:

result = [mapping_expr for value in iterable if filter_expr]

result = [{'key': value} for value in iterable
          if a_long_filter_expression(value)]

result = [complicated_transform(x)
          for x in iterable if predicate(x)]

descriptive_name = [
    transform({'key': key, 'value': value}, color='black')
    for key, value in generate_iterable(some_input)
    if complicated_condition_is_met(key, value)
]

result = []
for x in range(10):
    for y in range(5):
        if x * y > 10:
            result.append((x, y))

return {x: complicated_transform(x)
        for x in long_generator_function(parameter)
        if x is not None}

squares_generator = (x**2 for x in range(10))

unique_names = {user.name for user in users if user is not None}

eat(jelly_bean for jelly_bean in jelly_beans
    if jelly_bean.color == 'black')

No:

result = [complicated_transform(
          x, some_argument=x+1)
          for x in iterable if predicate(x)]

result = [(x, y) for x in range(10for y in range(5if x * y > 10]

return ((x, y, z)
        for x in range(5)
        for y in range(5)
        if x != y
        for z in range(5)
        if y != z)

2.8 默认迭代器和运算符

对支持默认迭代器和云算法的类型例如列表,字典和文件等使用它们

2.8.1 定义

容器类型(例如字典,列表等)定义了的默认的迭代器和成员检查运算符.

Pros

默认迭代器和操作符是简单有效的,能够直接不需额外调用方法地表达操作.使用默认操作符的函数是通用的.能被用于任何支持这些操作的类型.

Cons

不能通过方法名来分辨类型,例如has_key()意味着字典,当然这也是一种优势.

建议

对于支持的类型诸如列表,字典和文件,使用默认迭代器和操作符.内置类型同样定义了迭代器方法.优先使用这些方法而非那些返回列表的方法.除非能够确定在遍历容器的过程中不会改变容器.不要使用Python 2专有迭代方法除非必要.

Yes:

for key in adict: ...
if key not in adict: ...
if obj in alist: ...
for line in afile: ...
for k, v in adict.items(): ...
for k, v in six.iteritems(adict): ...

No:

for key in adict.keys(): ...
if not adict.has_key(key): ...
for line in afile.readlines(): ...
for k, v in dict.iteritems(): ...

2.9 生成器

需要时使用生成器

2.9.1 定义

生成器函数返回一个迭代器,每次执行yield语句的时候生成一个值.在生成一个值之后,生成器函数的运行被挂起直到需要下一个值.

2.9.2 Pros

简化代码,因为局部变量和控制流在每次调用时被保留,生成器相比于一次性生成整个一个列表值要更节省内存.

2.9.3 Cons

2.9.4 建议

建议使用.在生成器函数的文档字符串中使用”Yields:”而非”Returns:”

2.10 Lambda表达式

单行代码时是可以的

2.10.1 定义

lambda在一个表达式内定义了匿名函数,而不在语句里.lambda表达式常被用于定义高阶函数(例如map()filter())使用的回调函数或者操作符.

2.10.2 Pros

方便

2.10.3 Cons

比局部函数更难读懂和debug,匿名意味着堆栈跟踪更难懂.表达性受限因为lambda函数只包含一个表达式

2.10.4 建议

对于单行代码而言,可以使用lambda表达式.如果lambda表达式内的代码超过60-80个字符,最好定义成为常规的内嵌函数.

对于一般的操作诸如乘法,使用operator模块内置函数而非重新定义匿名函数,例如使用operator.mul而非lambda x,y: x * y

2.11 条件表达式

简单情况下可以使用.

2.11.1 定义

条件表达式(也称为三元运算符)是一种更短替代if语句的机制.例如x = 1 if cond else 2

2.11.2 Pros

相对于if语句更短也更方便

2.11.3 Cons

比if语句可能更难读懂,当表达式很长的时候条件部分可能很难定位.

2.11.4 建议

简单情况可以使用.每个部分(真值表达式,if表达式,else表达式)必须在一行内完成.如果使用条件表达式很富的时候使用完整的if语句.

Yes:

one_line = 'yes' if predicate(value) else 'no'
slightly_split = ('yes' if predicate(value)
                  else 'no, nein, nyet')
the_longest_ternary_style_that_can_be_done = (
    'yes, true, affirmative, confirmed, correct'
    if predicate(value)
    else 'no, false, negative, nay')

No:

bad_line_breaking = ('yes' if predicate(value) else
                     'no')portion_too_long = ('yes'
                    if some_long_module.some_long_predicate_function(
                        really_long_variable_name)
                    else 'no, false, negative, nay')

2.12 默认参数值

大多数情况下都OK

2.12.1 定义

在函数参数列表的最后可以为变量设定值,例如def foo(a, b=0):.如果foo在调用时只传入一个参数,那么b变量就被设定为0,如果调用时传入两个参数,那么b就被赋予第二个参数值.

2.12.2 Pros

通常一个函数可能会有大量默认值,但是很少会有需要修改这些默认值的时候.默认值就提供了一个很简单满足上述情况的方式,而不需要为这些少见的情况重新定义很多函数.因为Python不支持重载方法或函数,默认参数是一个很简单的方式来”假重载”行为.

2.12.3 Cons

默认参数在模块加载时就被复制.这在参数是可变对象(例如列表或字典)时引发问题.如果函数修改了这些可变对象(例如向列表尾添加元素).默认值就被改变了.

2.12.4 建议

使用时请注意以下警告—-在函数或方法定义时不要将可变对象作为默认值.

Yes:

def foo(a, b=None):
    if b is None:
        b = []
def foo(a, b: Optional[Sequence] = None):
    if b is None:
        b = []
def foo(a, b: Sequence = ()):  # Empty tuple OK since tuples are immutable 空元组是也不可变的
    ...

No:

def foo(a, b=[]):
    ...
def foo(a, b=time.time()):  # The time the module was loaded??? 模块被加载的时间???
    ...
def foo(a, b=FLAGS.my_thing):  # sys.argv has not yet been parsed... sys.argv还未被解析
    ...
def foo(a, b: Mapping = {}):  # Could still get passed to unchecked code 仍可传入未检查的代码(此处翻译可能有误)
    ...

2.13 属性

使用属性可以通过简单而轻量级的访问器和设定器方法来访问或设定数据.

2.13.1 定义

一种装饰器调用来在计算比较轻量级时作为标准的属性访问来获取和设定一个属性的方式

2.13.2 Pros

对于简单的属性访问,减少显式的get和set方法能够提升可读性.允许惰性计算.被认为是一种Python化的方式来维护类接口.在表现上,当直接对变量的访问更合理时,允许属性绕过所需的琐碎的访问方法.

2.13.3 Cons

在Python2中必须继承于object,可能会隐藏像是操作符重载之类的副作用.对于子类而言,属性可能有些迷惑性.

2.13.4 建议

在通常会有简单而且轻量级的访问和设定方法的新代码里使用属性来访问或设定数据.属性在创建时被@property装饰,参加装饰器

如果属性本身未被重写,带有属性的继承可能不够明晰,因而必须确保访问方法是被间接访问的,来确保子类的方法重载是被属性调用的(使用Template Method DP,译者:应是模板方法设计模式).

Yes:

class Square(object):
    """A square with two properties: a writable area and a read-only perimeter.

    To use:
    >>> sq = Square(3)
    >>> sq.area
    9
    >>> sq.perimeter
    12
    >>> sq.area = 16
    >>> sq.side
    4
    >>> sq.perimeter
    16
    """


    def __init__(self, side):
        self.side = side

    @property
    def area(self):
        """Area of the square."""
        return self._get_area()

    @area.setter
    def area(self, area):
        return self._set_area(area)

    def _get_area(self):
        """Indirect accessor to calculate the 'area' property."""
        return self.side ** 2

    def _set_area(self, area):
        """Indirect setter to set the 'area' property."""
        self.side = math.sqrt(area)

    @property
    def perimeter(self):
        return self.side * 4

2.14 True/False表达式

只要可能,就使用隐式False的if语句

2.14.1 定义

在布尔环境下,Python对某些值判定为False,一个快速的经验规律是所有”空”值都被认为是False,所以0, None, [], {}, ''的布尔值都是False

2.14.2 Pros

使用Python布尔类型的条件语句可读性更好而且更难出错,大多数情况下,这种方式也更快.

2.14.3 Cons

对于C/C++开发者而言可能有些奇怪

建议

如果可能的话,使用隐式False.例如使用if foo:而非if foo != []:下面列举了一些你应该牢记的警告:

  • 使用if foo is None(或者if foo is not None)来检查None.例如在检查一个默认值是None的变量或者参数是否被赋予了其他值的时候,被赋予的其他值的布尔值可能为False.
  • 不要用==来和布尔值为False的变量比较,使用if not x,如果需要区别FalseNone,那么使用链式的表达式如if not x and x is not None
  • 对于序列(如字符串,列表,元组),利用空序列为False的事实,故而相应地使用if seq:if not seq:而非if len(seq)if not len(seq):.
  • 在处理整数时,隐式的False可能会引入更多风险(例如意外地将None和0进行了相同的处理)你可以用一个已知是整形(并且不是len()的结果)的值和整数0比较.

Yes:

if not users:
    print('no users')

if foo == 0:
    self.handle_zero()

if i % 10 == 0:
    self.handle_multiple_of_ten()

def f(x=None):
    if x is None:
        x = []

No:

if len(users) == 0:
    print('no users')

if foo is not None and not foo:
    self.handle_zero()

if not i % 10:
    self.handle_multiple_of_ten()

def f(x=None):
    x = x or []

2.15 弃用的语言特性

尽可能利用字符串方法而非string模块.使用函数调用语法而非apply.在函数参数本就是一个行内匿名函数的时候,使用列表推导表达式和for循环而非filtermap

2.15.1 定义

当前Python版本提供了人们普遍更倾向的构建方式.

2.15.2 建议

我们不使用任何不支持这些特性的Python版本,因而没有理由不使用新方式.

Yes:

words = foo.split(':')

[x[1for x in my_list if x[2] == 5]

map(math.sqrt, data)    # Ok. No inlined lambda expression. 可以,没有行内的lambda表达式

fn(*args, **kwargs)

No:

words = string.split(foo, ':')

map(lambda x: x[1], filter(lambda x: x[2] == 5, my_list))

apply(fn, args, kwargs)

2.16 词法作用域

可以使用

2.16.1 定义

一个内嵌Python函数可以引用在闭包命名空间内定义的变量,但是不能对其复制.变量绑定是解析到使用词法作用域的,即基于静态程序文本.任何对块内命名的赋值都会让Python将对于这个命名的引用都作为局部变量,即使在使用先于赋值的情况下也是.如果有全局声明,这个命名就会被认为是全局变量.

一个使用这个特性的例子是:

def get_adder(summand1):
    """Returns a function that adds numbers to a given number."""
    def adder(summand2):
        return summand1 + summand2

    return adder

2.16.2 Pros

经常可以让代码更简明优雅,尤其会让有经验的Lisp和Scheme(以及Haskell和ML还有其他)的程序要很舒服.

2.16.3 Cons

可能会导致令人迷惑的bug例如这个基于PEP-0227的例子.

i = 4
def foo(x):
    def bar():
        print(i, end='')
    # ...
    # A bunch of code here
    # ...
    for i in x:  # Ah, i *is* local to foo, so this is what bar sees i对于foo来说是局部变量,所以在这里就是bar函数所获取的值
        print(i, end='')
    bar()

所以foo([1, 2, 3])会打印1 2 3 3而非1 2 3 4.

2.16.4 建议

可以使用

2.17 函数和方法装饰器

在明显有好处时,谨慎明智的使用,避免@staticmethod,控制使用@classmethod

2.17.1 定义

函数和方法装饰器(也就是@记号).一个常见的装饰器是@property,用于将普通方法转换成动态计算属性.然而装饰器语法也允许用户定义装饰器,尤其对于一些函数my_decorator如下:

class C(object):
    @my_decorator
    def method(self):
        # method body ...

是等效于

class C(object):
    def method(self):
        # method body ...
    method = my_decorator(method)

2.17.2 Pros

能够优雅的对方法进行某种转换,而该转换可能减少一些重复代码并保持不变性等等.

2.17.3 Cons

装饰器可以对函数的参数和返回值任意操作,导致非常隐形的操作行为.此外,装饰器在import的时候就被执行,装饰器代码的实效可能非常难恢复.

2.17.4 建议

在有明显好处的地方谨慎地使用装饰器.装饰器应该和函数遵守相同的import和命名指导规则.装饰器的文档应该清晰地声明该函数为装饰器函数.并且要为装饰器函数编写单元测试.

避免装饰器自身对外部的依赖,(如不要依赖于文件,socket,数据库连接等等),这是由于在装饰器运行的时候(在import时,可能从pydoc或其他工具中)这些外部依赖可能不可用.一个被传入有效参数并调用的装饰器应该(尽可能)保证在任何情况下都可用.

装饰器是一种特殊的”顶级代码”,参见main

永远不要使用@staticmethod,除非不得不整合一个API到一个已有的库,应该写一个模块等级的函数.

只在写一个命名的构造器或者一个类特定的,修改必要的全局状态(例如进程缓存等)的流程时使用@classmethod.

2.18 线程

不要依赖于内建类型的原子性

尽管Python内置数据类型例如字典等似乎有原子性操作,仍有一些罕见情况下,他们是非原子的(比如,如果__hash__或者__eq__被实现为Python方法),就不应该依赖于这些类型的原子性.也不应该依赖于原子变量赋值(因为这依赖于字典)

优先使用Queue模块的Queue类来作为线程之间通讯数据的方式.此外,要是用threading模块和其locking primitives(锁原语).了解条件变量的合理用法以便于使用threading.Condition而非使用更低级的锁.

2.19 过于强大的特性

尽量避免使用

2.19.1 定义

Python是一种非常灵活的语言并且提供了很多新奇的特性,诸如定制元类,访问字节码,动态编译,动态继承,对象父类重定义,import hacks,反射(例如一些对于getattr()的应用),系统内置的修改等等.

2.19.2 Pros

这些是非常强大的语言特性,可以让程序更紧凑

2.19.3 Cons

使用这些新特性是很诱人的.但是并不绝对必要,它们很难读很难理解.也很难debug那些在底层使用了不常见的特性的代码.对于原作者而言可能不是这样,但是再次看代码的时候,可能比更长但是更直接的代码要难.

2.19.4 定义

避免在代码中使用这些特性.

内部使用这些特性的标准库和类是可以使用的(例如abc.ABCMeta,collections.namedtuple,和enum)

2.20 新版本Python: Python3 和从__future__import

Python3已经可用了(译者:目前Python2已经不受支持了),尽管不是每个项目都准备好使用Python3,所有的代码应该兼容Python3并且在可能的情况下在Python3的环境下测试.

2.20.1 定义

Python3是Python的重大改变,尽管现有代码通常是Python2.7写成的,但可以做一些简单的事情来让代码更加明确地表达其意图,从而可以让代码更好地在Python3下运行而不用调整.

2.20.2 Pros

在考虑Python3编写的代码更清晰明确,一旦所有依赖已就绪,就可以更容易在Python3环境下运行.

2.20.3 Cons

一些人会认为默认样板有些丑,import实际不需要的特性到模块中是不常见的.

2.20.4 建议

from future imports

鼓励使用from __future__ import语句.所有新代码都应该包含下述代码,而现有代码应该被更新以尽可能兼容:

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

如果你不太熟悉这些,详细阅读这些:绝对import,新的/除法行为,和print函数

请勿省略或移除这些import,即使在模块中他们没有在使用,除非代码只用于Python3.最好总是在所有的文档中都有从future的import,来保证不会在有人使用在后续编辑时遗忘.

有其他的from __future__import语句,看喜好使用.我们的建议中不包含unicode_literals因为其并无明显优势,这是由于隐式默认的编码转换导致其在Python2.7内很多地方被引入了,必要时,大多数代码最好显式的使用b''u''btyes和unicode字符串表示.(译者:这段翻译可能不准确)

The six, future, or past libraries

当项目需要支持Python2和3时,根据需求使用six,future和past.

2.21 带有类型注释的代码

可以根据PEP-484对Python3代码进行类型注释,并且在build时用类型检查工具例如pytype进行类型检查.

类型注释可以在源码中或stub pyi file中.只要可能,注释就应写在源代码中.对于第三方或拓展模块使用pyi文件.

2.21.1 定义

类型注释(也称为”类型提示”)是用于函数或方法参数和返回值的:

def func(a: int) -> List[int]:

你也可以声明用一个单独的注释来声明变量的类型:

a = SomeFunc()  # type: SomeType

2.21.2 Pros

类型注释提升代码的可读性和可维护性,类型检查会将很多运行错误转化为构建错误,也减少了使用过于强力特性的能力.

2.21.3 Cons

需要不断更新类型声明,对于认为有效的代码可能会报类型错误,使用类型检查可能减少使用过于强力特性的能力.

2.21.4 建议

强烈鼓励在更新代码的时候进行Python类型分析.在对公共API进行补充和修改时,包括python类型声明并通过构建系统中的pytype进行检查.对Python来说静态类型检查比较新,我们承认,一些意料外的副作用(例如错误推断的类型)可能拒绝一些项目的使用.这种情况下,鼓励作者适当地增加一个带有TODO或到bug描述当前不接搜的类型注释的链接到BUILD文件或者在代码内.

3、Python代码风格规范

3.1 分号

不要在行尾加分号,也不要用分号把两行语句合并到一行

3.2 行长度

最大行长度是80个字符

超出80字符的明确例外:

  • 长import
  • 注释中的:URL,路径,flags等
  • 不包含空格不方便分行的模块级别的长字符串常量
  • pylint的diable注释使用(如# pylint: disable=invalid-name)

不要使用反斜杠连接,除非对于需要三层或以上的上下文管理器with语句

利用Python的implicit line joining inside parentheses, brackets and braces(隐式行连接方法–括号连接,包括(), [], {}).如果必要的话,也可在表达式外面额外添加一对括号.

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)

with very_long_first_expression_function() as spam:
    with very_long_second_expression_function() as beans:
        place_order(beans, spam)

No:

with VeryLongFirstExpressionFunction() as spam, \
     VeryLongSecondExpressionFunction() as beans:
    PlaceOrder(eggs, beans, spam, beans)

注意上述例子中的缩进,具体参看缩进

在其他一行超过80字符的情况下,而且yapf自动格式工具也不能使分行符合要求时,允许超过80字符限制.

3.3 括号

括号合理使用

尽管不必要,但是可以在元组外加括号.再返回语句或者条件语句中不要使用括号,除非是用于隐式的连接行或者指示元组.

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 缩进

缩进用4个空格

缩进代码段不要使用制表符,或者混用制表符和空格.如果连接多行,多行应垂直对齐,或者再次4空格缩进(这个情况下首行括号后应该不包含代码).

Yes:

# Aligned with opening delimiter
# 和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
# 缩进4个空格,首行括号后无内容
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.4.1 关于尾后逗号

关于在一序列元素中的尾号逗号,只推荐在容器结束符号],)或者}和最后元素不在同一行时使用.尾后逗号的存在也被用作我们Python代码自动格式化工具yapf的提示,在,最后元素之后出现的时候来自动调整容器元素到每行一个元素.

Yes:

golomb3 = [013]
golomb4 = [
    0,
    1,
    4,
    6,
]

No:

golomb4 = [
    0,
    1,
    4,
    6
]

3.5 空行

在顶级定义(函数或类)之间要间隔两行.在方法定义之间以及class所在行与第一个方法之间要空一行,def行后无空行,在函数或方法内你认为合适地方可以使用单空行.

3.6 空格

遵守标准的空格和标点排版规则.

括号(),[],{}内部不要多余的空格.

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)
dict['key'] = list[index]

No:

spam (1)
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)
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)
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

大部分.py文件不需要从#!行来开始.根据PEP-394,程序的主文件应该以#!/usr/bin/python2#!/usr/bin/python3起始

这行被用于帮助内核找到Python解释器,但是在导入模块时会被Python忽略/只在会被直接运行的文件里有必要写.

3.8 注释和文档字符串

确保使用正确的模块,函数,方法的文档字符串和行内注释.

3.8.1 文档字符串

Python使用文档字符串来为代码生成文档.文档字符串是包,模块,类或函数的首个语句.这些字符串能够自动被__doc__成员方法提取并且被pydoc使用.(尝试在你的模块上运行pydoc来看看具体是什么).文档字符串使用三重双引号"""(根据PEP-257).文档字符串应该这样组织:一行总结(或整个文档字符串只有一行)并以句号,问好或感叹号结尾.随后是一行空行,随后是文档字符串,并与第一行的首个引号位置相对齐.更多具体格式规范如下.

3.8.2 模块

每个文件都应包含许可模板.选择合适的许可模板用于项目(例如Apache 2.0,BSD,LGPL,GPL)

文档应该以文档字符串开头,并描述模块的内容和使用方法.

"""A one line summary of the module or program, terminated by a period.

Leave one blank line.  The rest of this docstring should contain an
overall description of the module or program.  Optionally, it may also
contain a brief description of exported classes and functions and/or usage
examples.

  Typical usage example:

  foo = ClassFoo()
  bar = foo.FunctionBar()
"""

3.8.3 函数和方法

在本节,”函数”所指包括方法,函数或者生成器.

函数应有文档字符串,除非符合以下所有条件:

  • 外部不可见
  • 非常短
  • 简明

文档字符串应该包含足够的信息以在无需阅读函数代码的情况下调用函数.文档字符串应该是叙事体("""Fetches rows from a Bigtable.""")的而非命令式的("""Fetch rows from a Bigtable."""),除了@property(应与attribute使用同样的风格).文档字符串应描述函数的调用语法和其意义,而非实现.对比较有技巧的地方,在代码中使用注释更合适.

覆写了基类的方法可有简单的文档字符串向读者指示被覆写方法的文档字符串例如"""See base class.""".这是因为没必要在很多地方重复已经在基类的文档字符串中存在的文档.不过如果覆写的方法行为实际上与被覆写方法不一致,或者需要提供细节(例如文档中表明额外的副作用),覆写方法的文档字符串至少要提供这些差别.

一个函数的不同方面应该在特定对应的分节里写入文档,这些分节如下.每一节都由以冒号结尾的一行开始, 每一节除了首行外,都应该以2或4个空格缩进并在整个文档内保持一致(译者建议4个空格以维持整体一致).如果函数名和签名足够给出足够信息并且能够刚好被一行文档字符串所描述,那么可以忽略这些节.

Args:

列出每个参数的名字.名字后应有为冒号和空格,后跟描述.如果描述太长不能够在80字符的单行内完成.那么分行并缩进2或4个空格且与全文档一致(译者同样建议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 类

类定义下一行应为描述这个类的文档字符串.如果类有公共属性,应该在文档字符串中的Attributes节中注明,并且和函数的Args一节风格统一.

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 块注释和行注释

最后要在代码中注释的地方是代码技巧性的部分.如果你将要在下次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个空格后,并以#后接至少1个空格开始注释部分.

另外,不要描述代码,假定阅读代码的人比你更精通Python(他只是不知道你试图做什么).

3.8.6 标点,拼写和语法

注意标点,拼写和语法,写得好的注释要比写得差的好读.

注释应当是和叙事性文本一样可读,并具有合适的大小写和标点.在许多情况下,完整的句子要比破碎的句子更可读.更简短的注释如行尾的注释有时会不太正式,但是应该全篇保持风格一致.

尽管被代码审核人员指出在应该使用分号的地方使用了逗号是很令人沮丧的,将源代码维护在高度清楚可读的程度是很重要的.合适的标点,拼写和语法能够帮助达到这个目标.

3.9 类

如果类并非从其他基类继承而来,那么就要明确是从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

object类继承保证了属性能够在Python2正确运行并且保护代码在Python3下出现潜在的不兼容.这样也定义了object包括__new__,__init__,__delattr__,__getattribute__,__setattr__,__hash__,__repr__,和__str__等默认特殊方法的实现.

3.10 字符串

使用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:

employee_table = '<table>'
for last_name, first_name in employee_list:
    employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'

避免使用++=操作符来在循环内累加字符串,因为字符串是不可变对象.这会造成不必要的临时变量导致运行时间以四次方增长而非线性增长.应将每个字符串都记入一个列表并使用''.join来将列表在循环结束后连接(或将每个子字符串写入io.BytesIO缓存)

Yes:

items = ['<table>']
for last_name, first_name in employee_list:
    items.append('<tr><td>%s, %s</td></tr>' % (last_name, first_name))
items.append('</table>')
employee_table = ''.join(items)

No:

employee_table = '<table>'
for last_name, first_name in employee_list:
    employee_table += '<tr><td>%s, %s</td></tr>' % (last_name, first_name)
employee_table += '</table>'

在同一个文件内,字符串引号要一致,选择''或者""并且不要改变.对于需要避免\\转义的时候,可以更改.

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.")

多行字符串多行字符串优先使用”””而非''',当且只当对所有非文档字符串的多行字符串都是用'''而且对正常字符串都使用'时才可使用三单引号.docstring不论如何必须使用"""

多行字符串和其余代码的缩进方式不一致.如果需要避免在字符串中插入额外的空格,要么使用单行字符串连接或者带有textwarp.dedent()的多行字符串来移除每行的起始空格.

No:

long_string = """This is pretty ugly.
Don't do this.
"""

Yes:

long_string = """This is fine if your use case can accept
    extraneous leading spaces."""


long_string = ("And this is fine if you can not accept\n" +
               "extraneous leading spaces.")

long_string = ("And this too is fine if you can not accept\n"
               "extraneous leading spaces.")

import textwrap

long_string = textwrap.dedent("""\
    This is also fine, because textwrap.dedent()
    will collapse common leading spaces in each line."""
)

3.11 文件和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和常量前面.应按照从最通用到最不通用的顺序排列分组:

  1. Python未来版本import语句,例如:

    from __future__ import absolute_import
    from __future__ import division
    from __future__ import print_function

    更多信息参看上文

  2. Python标准基础库import,例如:

    import sys
  3. 第三方库或包的import,例如:

    import tensorflow as tf
  4. 代码库内子包import,例如:

    from otherproject.ai import mind
  5. 此条已弃用:和当前文件是同一顶级子包专用的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<MethodUnderTest>_<state>例如testPop_EmptyStack,对于命名测试方法没有明确的正确方法.

3.16.3 文件名

文件拓展名必须为.py,不可以包含-.这保证了能够被正常import和单元测试.如果希望一个可执行文件不需要拓展名就可以被调用,那么建立一个软连接或者一个简单的bash打包脚本包括exec "$0.py" "$@".

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]:

  ...

pylint允许您将右括号移动到新行并与左括号对齐,但这不太容易理解.

No:

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是一等类型,为了方便输入,NoneNoneType的别名.如果一个参数可以是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 类型别名

可以对复杂类型声明别名,别名的名称应为CapWorded,如果只用于当前模块,应加下划线私有化.

例如,如果带有模块名的类型名过长:

_ShortName = module_with_long_name.TypeWithLongName
ComplexMap = 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 = [123]  # type: List[int]
b = (123)  # type: Tuple[int, ...]
c = (1"2"3.5)  # type: Tuple[int, Text, float]

3.19.10 TypeVars

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, Text)
def add(a: AddableType, b: AddableType) -> AddableType:
    return a + b

typing模块预定义好的类型变量是AnyStr,用于针对字符串可以是bytes也可为unicode并且保持一致的多个类型注释.

from typing import AnyStr
def check_length(x: AnyStr) -> AnyStr:
  if len(x) <= 42:
    return x
  raise ValueError()

3.19.11 字符串类型

注释字符串的合适类型是基于Python版本的.

对于只有Python3的代码,使用str,Text可以用但是在选择上保持一致.

对于Python2兼容的代码,用Text,在一些很罕见的情况下,str可能可用.当在不同Python版本之间返回值类型不同的时候通常是为了照顾兼容性.避免使用unicode,因为Python3中不存在.

No:

def py2_code(x: str) -> unicode:
  ...

对于处理二进制数据的代码,请使用bytes.

Yes:

def deals_with_binary_data(x: bytes) -> bytes:
  ...

对于Python2兼容,处理文本数据(Python中strunicode,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 typing
if typing.TYPE_CHECKING:
    import sketch
def 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、最后的话

如果你在编辑代码,花几分钟看看现有代码然后决定好要使用哪种风格.如果现有代码在所有算术运算符两侧都加了空格,那么你也应该如此.如果现有的注释用井号组成了包围框,那么你的注释也应如此.

有代码风格指南的目的是有一个编程的共识,这样人们能够集中在内容而非形式上.我们将通用的代码风格指南公布于此这样人们就能了解这个共识(译者:有巴别塔的意味.)但是各自的代码风格也很重要.如果你添加的代码与原有代码看起来完全不一致,就会打乱读者的阅读节奏,最好避免这样。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Cufflinks实战教程—1行python代码就可实现炫酷可视化

作者:python数据分析之禅

来源:小dull鸟

之前画图一直在用matlibplot、pyecharts,最近学习了一个新的可视化库–cufflinks,用了两天我已经深深爱上它了

主要是因为它用法简单、图形漂亮、代码量少,用一两行代码,就能画出非常漂亮的图形

下面我们一起来看看吧!

1.用法简单

cufflinks库主要和dataFrame数据结合使用,绘图函数就是 dataFrame.iplot,记住这个就行了,但是 iplot 函数里的参数很多,一些参数说明如下:

1. kind:图的种类,如 scatter、pie、histogram 等
2. mode:lines、markers、lines+markers,分别表示折线、点、折线和点
3. colors:轨迹对应的颜色
4. dash:轨迹对应的虚实线,solid、dash、dashdot 三种
5. width:轨迹的粗细
6. xTitle:横坐标名称
7. yTitle:纵坐标的名称
8. title:图表的标题

如下图,df为随机生成的dataFrame数据,kind=’bar’表示柱状图,title代表标题,xTitle命名X轴,yTitle命名Y轴:

import pandas as pd
import numpy as np
import cufflinks as cf
df=pd.DataFrame(np.random.rand(124), columns=['a''b''c''d'])
df.iplot(kind ='bar',title='示例', xTitle = 'X轴', yTitle ='Y轴')

图片

2.少量代码就能画出非常漂亮的图形

cufflinks为我们提供了丰富的主题样式,支持包括polar、pearl、henanigans、solar、ggplot、space和white等7种主题。

折线图

cf.datagen.lines(4,10).iplot(mode='lines+markers',theme='solar')

图片

cufflinks使用datagen生成随机数,figure定义为lines形式,cf.datagen.lines(2,10)的具体形式如下:

cf.datagen.lines(2,10)  #2代表2组,10代表10天
  WCB.EH OAA.CQ
2015-01-01 -0.052580 -0.351618
2015-01-02 1.056254 -1.476417
2015-01-03 0.078017 1.129168
2015-01-04 0.282141 0.908655
2015-01-05 0.960537 -0.223996
2015-01-06 1.420355 0.212851
2015-01-07 2.266144 0.358502
2015-01-08 0.008034 1.086130
2015-01-09 1.876946 2.226895
2015-01-10 1.855625 2.852383

散点图

df = pd.DataFrame(np.random.rand(504), columns=['a''b''c''d'])
df.iplot(kind='scatter',mode='markers',colors=['orange','teal','blue','yellow'],size=20,theme='solar')

图片

气泡图

df.iplot(kind='bubble',x='a',y='b',size='c',theme='solar')

图片

subplots 子图

df=cf.datagen.lines(4)
df.iplot(subplots=True,shape=(4,1),shared_xaxes=True,vertical_spacing=.02,fill=True,theme='ggplot')

图片

箱形图

cf.datagen.box(20).iplot(kind='box',legend=False,theme='ggplot')

图片

 

直方图

df.iloc[:,0:3].iplot(kind='histogram')

图片

 

3D图

cf.datagen.scatter3d(5,4).iplot(kind='scatter3d',x='x',y='y',z='z',text='text',categories='categories')

图片

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Python 想了解EventLoop?这篇文章就够了

原文来自 python-parallel-programming-cookbook-cn

Python的Asyncio模块提供了管理事件、协程、任务和线程的方法,以及编写并发代码的原语。此模块的主要组件和概念包括:

  • 事件循环: 在Asyncio模块中,每一个进程都有一个事件循环。
  • 协程: 这是子程序的泛化概念。协程可以在执行期间暂停,这样就可以等待外部的处理(例如IO)完成之后,从之前暂停的地方恢复执行。
  • Futures: 定义了 Future 对象,和 concurrent.futures 模块一样,表示尚未完成的计算。
  • Tasks: 这是Asyncio的子类,用于封装和管理并行模式下的协程。

本节中重点讨论事件,事实上,异步编程的上下文中,事件无比重要。因为事件的本质就是异步。

1. 什么是事件循环

在计算系统中,可以产生事件的实体叫做事件源,能处理事件的实体叫做事件处理者。此外,还有一些第三方实体叫做事件循环。它的作用是管理所有的事件,在整个程序运行过程中不断循环执行,追踪事件发生的顺序将它们放到队列中,当主线程空闲的时候,调用相应的事件处理者处理事件。最后,我们可以通过下面的伪代码来理解事件循环::

while(1) {
  events = getEvents();
  for (e in events)
    processEvent(e);
}

所有的事件都在 while 循环中捕捉,然后经过事件处理者处理。事件处理的部分是系统唯一活跃的部分,当一个事件处理完成,流程继续处理下一个事件。

2. 准备工作

Asyncio提供了一下方法来管理事件循环:

  • loop = get_event_loop(): 得到当前上下文的事件循环。
  • loop.call_later(time_delay, callback, argument): 延后 time_delay 秒再执行 callback 方法。
  • loop.call_soon(callback, argument): 尽可能快调用 callbackcall_soon() 函数结束,主线程回到事件循环之后就会马上调用 callback 。
  • loop.time(): 以float类型返回当前事件循环的内部时间。
  • asyncio.set_event_loop(): 为当前上下文设置事件循环。
  • asyncio.new_event_loop(): 根据此策略创建一个新的事件循环并返回。
  • loop.run_forever(): 在调用 stop() 之前将一直运行。

3. 如何做…

下面的代码中,我们将展示如何使用Asyncio库提供的事件循环创建异步模式的应用。

import asyncio
import datetime
import time

def function_1(end_time, loop):
    print("function_1 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_2, end_time, loop)
    else:
        loop.stop()

def function_2(end_time, loop):
    print("function_2 called ")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_3, end_time, loop)
    else:
        loop.stop()

def function_3(end_time, loop):
    print("function_3 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_1, end_time, loop)
    else:
        loop.stop()

def function_4(end_time, loop):
    print("function_5 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_4, end_time, loop)
    else:
        loop.stop()

loop = asyncio.get_event_loop()

end_loop = loop.time() + 9.0
loop.call_soon(function_1, end_loop, loop)
# loop.call_soon(function_4, end_loop, loop)
loop.run_forever()
loop.close()

运行结果如下::

python3 event.py
function_1 called
function_2 called
function_3 called
function_1 called
function_2 called
function_3 called
function_1 called
function_2 called
function_3 called

在这个例子中,我们定义了三个异步的任务,相继执行,入下图所示的顺序。

首先,我们要得到这个事件循环::

loop = asyncio.get_event_loop()

然后我们通过 call_soon 方法调用了 function_1() 函数。

end_loop = loop.time() + 9.0
loop.call_soon(function_1, end_loop, loop)

让我们来看一下 function_1() 的定义::

def function_1(end_time, loop):
    print("function_1 called")
    if (loop.time() + 1.0) < end_time:
        loop.call_later(1, function_2, end_time, loop)
    else:
        loop.stop()

这个函数通过以下参数定义了应用的异步行为:

  • end_time: 定义了 function_1() 可以运行的最长时间,并通过 call_later 方法传入到 function_2() 中作为参数
  • loop: 之前通过 get_event_loop() 方法得到的事件循环

function_1() 的任务非常简单,只是打印出函数名字。当然,里面也可以写非常复杂的操作。

print("function_1 called")

任务执行结束之后,它将会比较 loop.time() +1s和设定的运行时间,如果没有超过,使用 call_later 在1秒之后执行 function_2() 。

if (loop.time() + 1.0) < end_time:
    loop.call_later(1, function_2, end_time, loop)
else:
    loop.stop()

function_2() 和 function_3() 的作用类似。

如果运行的时间超过了设定,事件循环终止。

loop.run_forever()
loop.close()

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Python 超强大的PDF表格提取器 — Camelot

如果你有从PDF中批量提取表格的需求,那么这篇文章就是你的福音。

Python 第三方模块 Camelot 能够精准识别PDF中的表格信息,并提取为pandas数据结构,而且还能导出为多种格式:JSON,Excel,HTML和Sqlite。

下面给大家介绍这个模块的使用方法:

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install camelot-py[cv]

2.使用

最简单的使用方式如下:

import camelot
# 1.读取pdf
tables = camelot.read_pdf('foo.pdf', flavor='stream')
# 2.导出pdf所有的表格为csv文件
tables.export('foo.csv', f='csv') # json, excel, html, sqlite

第一行,导入了camelot这个模块。

第二行,以stream的模式读取当前目录的foo.pdf文件。

第三行,将所有表格数据导出为 foo.csv 文件,并保存在当前文件夹下。

相当简单,请注意,read_pdf 的 flavor 参数是可选的,如果你不带这个参数,请注意需要安装 ghostscript 这个驱动,因为它默认使用 ghostscript 去用 lattice 模式。

3.进阶

3.1 处理背景线:

可以看到,很多表格的线都隐藏在背景中。这种表格默认是不支持的,这时候我们需要让程序能够自动识别这样的表格:

tables = camelot.read_pdf('background_lines.pdf', process_background=True)

增加 process_background=True 参数即可。

3.2 指定表格区域

某些情况下无法正确识别到PDF中的表格,此时手动设定左上角和右下角的边界可能是有效果的:

tables = camelot.read_pdf('table_areas.pdf', flavor='stream', table_areas=['316,499,566,337'])

其中 table_areas 接受格式为 x1,y1,x2,y2 的字符串,其中(x1,y1) -> 左上角, (x2,y2) -> 右下角。在PDF坐标空间中,页面的左下角是原点,坐标为(0,0)。

本文的代码和示例,以及Camelot源仓库可在 Python实用宝典 公众号后台回复 camelot 下载。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

py-spy:Python 程序的性能监控器

py-spy是用于Python程序的性能监控器。它使你可以直观地看到Python程序花费的时间,而无需重新启动程序或以任何方式修改代码。

py-spy的开销非常低:为了最大化提高速度,它是用Rust编写的,并且与配置的Python程序不在同一进程中运行。这意味着 py-spy 可以安全地用于生产环境的Python程序。

py-spy 可在 Linux,OSX,Windows 和 FreeBSD 上运行,并支持所有最新版本的CPython解释器(2.3-2.7和3.3-3.8版)进行性能分析。

1.安装

可以通过以下方式从 PyPI 安装预构建的二进制wheel文件:

pip install py-spy

你也可以从 GitHub Release Page 下载预构建的二进制文件,如果网络无法连接GitHub,你也可在 Python实用宝典 公众号后台回复 pyspy 下载。

2.用法

py-spy 在命令行中进行工作,获取你要从监控的程序的PID或你要运行的python程序的文件。分别有三种分析方法  recordtop以及dump

record

py-spy支持使用record命令将配置文件记录到文件中。例如,您可以通过执行以下操作来生成python进程的热力图

py-spy record -o profile.svg --pid 12345
# OR
py-spy record -o profile.svg -- python myprogram.py

它将生成一个交互式SVG文件,如下所示:

你可以使用参数 –format 更改文件格式。请参阅参考资料,py-spy record --help 以获取有关其他选项的信息,包括更改采样率,仅包含GIL的线程进行过滤,对本机C扩展进行概要分析,显示线程ID,概要分析子进程等。

Top

Top显示了在python程序中花费最多时间的函数的实时视图,类似于Unix top命令。使用以下命令运行:

py-spy top --pid 12345
# OR
py-spy top -- python myprogram.py

将显示你的python程序的实时函数消耗:

Dump

py-spy 还可以使用 dump 命令显示每个 python 线程的当前调用堆栈:

py-spy dump --pid 12345

这会将每个线程的调用堆栈以及其他一些基本进程信息转发到控制台:

对于需要一个调用堆栈来确定python程序挂在何处的情况,这很有用。该命令还可以通过设置 --locals 标志来打印出与每个堆栈帧关联的局部变量。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Pandas 实现列表分列与字典分列及三个实例

本文讲解了列表和字典转化为pandas的列的多种方法及实战例子和教程。

1.问题来源

源于林胖发出的一道基础题:

图片
图片

2.解法

2.1 基础解法explode函数

这道题最简单的解法,相信大部分用过pandas的朋友都会,林胖也马上发出了自己的答案:

import pandas as pd

mydict = {'A': [1], 'B': [2, 3], 'C': [4, 5, 6]}
pd.DataFrame(mydict.items()).explode(1)

结果:

图片

详解

mydict.items()是python基础字典的内容,它返回了这个字典键值对组成的元组列表:

mydict.items()

返回:

dict_items([('A', [1]), ('B', [2, 3]), ('C', [4, 5, 6])])

将这个内部是元组的可迭代对象传入DataFrame的构造函数中:

pd.DataFrame(mydict.items())

返回结果:

图片

这是pandas最基础的开篇知识点使用可迭代对象构造DataFrame,列表的每个元素都是整个DataFrame对应的一行,而这个元素内部迭代出来的每个元素将构成DataFrame的某一列。

然后再看看这个explode函数,它是pandas 0.25版本才出现的函数,只有一个参数可以传入列名,然后该函数就可以把该列的列表每个元素扩展到多行上。

效果与hive使用lateral view+explode实现的效果几乎一致,类似于:

select a,b_i from df lateral view explode(b) tmp as b_i;

可以参考很早之前的一篇文章:https://blog.csdn.net/as604049322/article/details/105985770

2.2 没有exlode函数如何解决这个问题

但是,黄佬说版本太低没有这个函数,于是我给群友们出了一道题:

图片

在黄佬的邀请下,一位经过我多次辅导的群友率先使用了循环法解题:

图片

我觉得非常棒,但我也希望看到有人再用变形法实现一次。林胖和一位群友再次给出了简化版本的循环解法:

图片

经过一番提示后,小五哥和林胖终于给出了变形法的解法:

图片

非常不错,群友们终于独立的多思路解决了这个问题,真的要撒花呀!!!

下面我们详细分析一下,循环法和变形法的解法吧:

2.3 循环法解题

基本写法:

result = []
for k, vs in mydict.items():
    for v in vs:
        result.append((k, v))
pd.DataFrame(result)

本质上就是实现了一个笛卡尔积的拉平操作,将mydict.items这个可迭代对象的元组构造笛卡尔积并按照整体拉平。

上面的基本写法,应该99%以上的朋友都能看懂,但 林胖 的循环简化解法:

import itertools
result = []
for k, v in mydict.items():
    result.extend(itertools.product(k, v))
pd.DataFrame(result)

部分朋友可能没有看明白,这个就需要查询一下product方法的官方文档(https://docs.python.org/zh-cn/3.7/library/itertools.html?highlight=product#itertools.product):

product(*iterables, repeat=1) --> product object

参数:

  • iterables 为可迭代对象
  • 可选参数repeat 表示重复次数

用于生成可迭代对象输入的笛卡儿积,相当于生成器表达式中的嵌套循环。

例如:product(A, B) 中的元素A和B将共同构成可迭代元素[A, B]作为iterables传入和 ((x,y) for x in A for y in B) 返回结果一样。

返回示例:

  • product(‘ab’, range(3)) –> (‘a’,0) (‘a’,1) (‘a’,2) (‘b’,0) (‘b’,1) (‘b’,2)
  • product((0,1), (0,1), (0,1)) –> (0,0,0) (0,0,1) (0,1,0) (0,1,1) (1,0,0) …

也可以传入可选参数 repeat 表示重复的次数:例如,product(A, repeat=4)product(A, A, A, A) 的返回结果是一样的。


列表的extend方法是将可迭代对象的每个元素都添加到列表中,而append方法只能添加单个元素。

当然,我们还可以将整个for循环改写成列表生成式:

result = [(k, v) for k, vs in mydict.items() for v in vs]
pd.DataFrame(result)

也可以简化代码量。

2.4 变形法解题

df = pd.DataFrame(mydict.items(), columns=["a", "b"])
df

实现思路,上面的界面是下面最左边:

图片

2.4.1 列表分列的2种方法

列表分列的思路:Pandas的Series对象调用apply方法单个元素返回的结果是Series时,这个Series的每个数据会作为Datafrem的每一列,索引会作为列名。

对Series进行列表分列

例如:

df["b"].apply(pd.Series)

结果:

图片

不过这样会丢失原本的”a”列,我们可以先将”a”列设置为索引,再进行Series分列操作:

df.set_index("a")["b"].apply(pd.Series)

或者把结果设置成原本的”a”列为索引:

df["b"].apply(pd.Series).set_index(df["a"])

结果均为上述实现思路的第二步。

直接对Datafream进行列表分列

如果我们希望直接使用Datafream实现分列可以借助agg方法,因为agg方法是对每一列的Series对象操作:

df.agg({"a": lambda x: x, "b": pd.Series})

结果:

图片

但这操作导致列多了一个级别,需要删除:

df.agg({"a": lambda x: x, "b": pd.Series}).droplevel(0, axis=1)

结果:

图片

只要再执行set_index("a")

df.agg({"a": lambda x: x, "b": pd.Series}).droplevel(0, axis=1).set_index("a")

结果就会与实现思路的第二步结果一致。

2.4.2 将字典的键作为索引的2种读取方法

当然上面我只是为了给大家讲述分列的一些方法。对于这个例子,其实我们可以直接通过pd.DataFrame.from_dict方法orient参数传入’index’,直接获得第二步的结果(只是索引没有名称):

df = pd.DataFrame.from_dict(mydict, 'index')

或者分别传入data和索引index:

df = pd.DataFrame(data=mydict.values(), index=mydict.keys())

都能得到以下结果:

图片

2.4.3 melt实现逆透视

说起逆透视我个人首先想到了melt方法,然后才想到melt方法实现的本质用到了stack方法。

为了避免索引丢失,我们首先还原索引为普通的列:

df = df.rename_axis(index="a").reset_index()
df

结果:

图片

然后使用melt方法进行逆透视:

df.melt(id_vars='a', value_name='b')

结果:

图片

然后删除第二列,再删除空值行,再将数值列转换为整数类型就搞定。

最终代码:

df = pd.DataFrame.from_dict(mydict, 'index')
df = df.melt(id_vars='a', value_name='b').drop(columns="variable").dropna()
df.b = df.b.astype("int")
df

成功得到结果:

图片

2.4.4 stack实现逆透视

df = pd.DataFrame.from_dict(mydict, 'index')
df.stack()

结果:

A  0    1.0
B  0    2.0
   1    3.0
C  0    4.0
   1    5.0
   2    6.0
dtype: float64

结果返回了一个多级索引的Series,我们首先需要删除索引中多余的部分:

df.stack().droplevel(1)

结果:

A    1.0
B    2.0
B    3.0
C    4.0
C    5.0
C    6.0
dtype: float64

此时我们再还原索引到普通列:

df.stack().droplevel(1).reset_index()

再重新设置一下列名:

df.stack().droplevel(1).reset_index().set_axis(["a", "b"], axis=1)

最后重设一下B列的类型:

df.b = df.b.astype("int")

最终代码:

df = pd.DataFrame.from_dict(mydict, 'index')
df = df.stack().droplevel(1).reset_index().set_axis(["a", "b"], axis=1)
df.b = df.b.astype("int")
df

结果:

图片

2.实际应用

这次我将分享三个实际案例,让大家看看列表分列的一些实际应用。

首先,我们先导包并设置Pandas显示参数:

import pandas as pd
pd.set_option("display.max_colwidth"100)

正则提取并分列

需求:

图片

读取数据:

df = pd.read_excel("正则提取与分列.xlsm", usecols=[0])
df.head()

结果:

图片

实现代码:

result = df.copy()
result["tmp"] = result["补回原因"].str.findall("([\d.]+[到至][\d.]+)")
result = result.agg({"补回原因"lambda x: x, "tmp": pd.Series}).droplevel(0, axis=1)
result.head()

结果:

图片

分步解析:

df["tmp"] = df["补回原因"].str.findall("([\d.]+[到至][\d.]+)")
df.head(5)

结果:

图片

这步使用正则提取出每个日期字符串,[\d.]+表示连续的数字或.用于匹配时间字符串,两个时间之间的连接字符可能是到或至。

然后我使用agg函数直接对Datafream分列:

df.agg({"补回原因"lambda x: x, "tmp": pd.Series})

结果:

图片

由于列索引多了一级,所以需要删除:

df.agg({"补回原因"lambda x: x, "tmp": pd.Series}).droplevel(0, axis=1).head()

结果:

图片

droplevel(0, axis=1)用于删除多级索引指定的级别,axis=0可以删除行索引,axis=1则可以删除列索引,第一参数表示删除级别0。当然如果列索引存在名称时还可以传入名称字符串,可参考官网文档:

df = pd.DataFrame([
...     [1234],
...     [5678],
...     [9101112]
... ]).set_index([01]).rename_axis(['a''b'])
>>> df.columns = pd.MultiIndex.from_tuples([
...    ('c''e'), ('d''f')
... ], names=['level_1''level_2'])
>>> df
level_1   c   d
level_2   e   f
a b
1 2      3   4
5 6      7   8
9 10    11  12
>>> df.droplevel('a')
level_1   c   d
level_2   e   f
b
2        3   4
6        7   8
10      11  12
>>> df.droplevel('level2', axis=1)
level_1   c   d
a b
1 2      3   4
5 6      7   8
9 10    11  12

分组聚合并分列

需求:

图片

首先,读取数据:

df = pd.read_excel("分组聚合并分列.xlsx")
df

结果:

图片

实现代码:

(
    df.groupby("姓名")["得分"]
    .apply(list)
    .apply(pd.Series)
    .fillna("")
    .rename(columns=lambda x: f"得分{x+1}")
    .reset_index()
    .astype({"得分1":"int8"})
)

结果:

图片

分布解析:

首先将每个姓名的得分聚合成列表,并最终返回一个Series:

df.groupby("姓名")["得分"].apply(list)

结果:

姓名
孙四娘          [7, 28]
看见星光    [88, 28, 23]
看见月光    [69, 10, 87]
老祝          [51, 29]
马青梅             [99]
Name: 得分, dtype: object

当然,这步的标准写法应该是使用Series的内部方法:

df.groupby("姓名")["得分"].apply(lambda x:x.to_list())

使用Series内部方法的性能比python列表方法转换快一些。

作为一个Series就可以通过将每个列表元素转换为Series,从而最终返回一个分列的Datafream:

_.apply(pd.Series)

结果:

图片

注意:_在ipython表示上一个输出返回的结果,jupyter还额外支持_num表示num编号单元格的输出。

_.fillna("")

结果:

图片

fillna表示填充缺失值,传入””表示将缺失值填充为空字符串。

下面重命名一下列名:

_.rename(columns=lambda x: f"得分{x+1}")

结果:

图片

然后还原索引:

_.reset_index()

结果:

图片

发现结果中有一列,不是整数,所以还原成整数(总分100分,8位足够存储):

_.astype({"得分1":"int8"})

结果:

图片

解析json字符串并字典分列

需求:

图片

首先读取数据:

df = pd.read_excel("字典分列.xlsx")
df.head()

结果:

图片

处理代码:

result = df.features.apply(eval).apply(pd.Series)
result["counts"] = df.counts
result

结果:

  储存条件 品牌 推荐理由 品种 食用方式 是否进口 特色服务 是否有机 counts
0 常温 NaN NaN NaN NaN NaN NaN NaN 33
1 冷藏 NaN NaN NaN NaN NaN NaN NaN 24
2 常温 禾煜 NaN NaN NaN NaN NaN NaN 22
3 常温 妙洁 NaN NaN NaN NaN NaN NaN 16
4 冷冻 NaN NaN NaN NaN NaN NaN NaN 14
2083 常温 乐事 够薄够脆 NaN NaN NaN NaN NaN 1
2084 冷藏 NaN 生态种植 黄瓜 NaN NaN NaN 有机 1
2085 冷藏 NaN 腥味较淡 鲫鱼 NaN NaN 免费宰杀 NaN 1
2086 冷藏 NaN 甜脆可口 佛手瓜 NaN NaN NaN NaN 1
2087 冷藏 叮咚日日鲜 全程可追溯 猪小排 NaN NaN NaN NaN 1

2088 rows × 9 columns

浅析:

df.features.apply(eval)用于将features列的每个json字符串解析为字典对象。

**.apply(pd.Series)则可以将每个字典对象转换成Series,则可以将该字典扩展到多列,并将原始的Series转换为Datafream。

result["counts"] = df.counts则将原始数据的counts列添加到结果列中。

本文转自快学Python,有部分增删。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Pandas实战教程:将Excel转为html格式

大家谈及用Pandas导出数据,应该就会想到to.xxx系列的函数。

这其中呢,比较常用的就是pd.to_csv()pd.to_excel()。但其实还可以将其导成Html网页格式,这里用到的函数就是pd.to_html()

读取Excel

今天我们要实现Excel转为html格式,首先需要用读取Excel中的表格数据。

import pandas as pd
data = pd.read_excel('测试.xlsx')

查看数据

data.head()
图片

下面我们来学习把DataFrame转换成HTML表格的方法。

生成Html

to_html()函数可以直接把DataFrame转换成HTML表格,只需一行代码即可实现:

html_table = data.to_html('测试.html')

运行上面代码后,工作目录中多了测试.html文件,使用网页浏览器打开它,显示内容如下👇

图片
print(data.to_html())

通过print打印,可以看到DataFrame的内部结构被自动转换为嵌入在表格中的<TH>,<TR>,<TD>标签,保留所有内部层级结构。

图片

调整格式

我们还可以自定义修改参数,来调整生成HTML的格式。

html_table = data.to_html('测试.html',header = True,index = False,justify='center')

再次打开新生成的测试.html文件,发现格式已经发生了变化。

图片

如果想对格式进行进一步调整(增加标题、修改颜色等),就需要一些HTML知识了,可以对生成的测试.html文件中的文本进行调整。

对于有些小伙伴可能需要进行页面展示,就要搭配Flask库来使用了。

小结

Pandas提供read_html()to_html()两个函数用于读写html格式的文件。这两个函数非常有用,一个轻松将DataFrame等复杂的数据结构转换成HTML表格;另一个不用复杂爬虫,简单几行代码即可抓取Table表格型数据,简直是个神器!

今天篇幅很短,主要讲了Pandas中to_html()这个函数。使用该函数最大的优点是:我们在不了解html知识的情况下,就能生成一个表格型的HTML。本文转自快学Python

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Python 超方便的迭代进度条 (Tqdm)

Tqdm 是一个智能进度表。它能够显示所有可迭代对象当前执行的进度。

你只需要用 tqdm 对可迭代对象进行封装后再遍历即可实现进度条功能,比如说:

from tqdm import tqdm
for i in tqdm(range(10000)):
    ...

显示效果如下:

76%|████████████████████████ | 7568/10000 [00:33<00:10, 229.00it/s]

1.准备

开始之前,你要确保Python和pip已经成功安装在电脑上,如果没有,请访问这篇文章:超详细Python安装指南 进行安装。

(可选1) 如果你用Python的目的是数据分析,可以直接安装Anaconda:Python数据分析与挖掘好帮手—Anaconda,它内置了Python和pip.

(可选2) 此外,推荐大家用VSCode编辑器来编写小型Python项目:Python 编程的最好搭档—VSCode 详细指南

Windows环境下打开Cmd(开始—运行—CMD),苹果系统环境下请打开Terminal(command+空格输入Terminal),输入命令安装依赖:

pip install tqdm

2.基本使用

tqdm 非常灵活,可以使用多种方式调用。下面给出了三个主要的形式。

2.1 迭代的形式

使用tqdm()封装可迭代的对象:

from tqdm import tqdm
from time import sleep

text = ""
for char in tqdm(["a", "b", "c", "d"]):
    sleep(0.25)
    text = text + char

trange(i)是特殊的关键字,是封装了range的tqdm对象:

from tqdm import trange

for i in trange(100):
    sleep(0.01)

你还能控制进度条显示当前步骤的名称:

pbar = tqdm(["a", "b", "c", "d"])
for char in pbar:
    sleep(0.25)
    pbar.set_description("Processing %s" % char)

Processing d: 100%|█████████████████████████████████████████████| 4/4 [00:01<00:00, 3.99it/s]

2.2 手动的形式

除了迭代的形式,你可以手动控制进度,加一个tqdm上下文即可:

with tqdm(total=100) as pbar:
    for i in range(10):
        sleep(0.1)
        pbar.update(10)

上述例子中,pbar 是 tpdm 的“进度”,每一次对 pbar 进行 update 10 都相当于进度加10。

Total 的值即是总进度,这里 total 的值是100,那么pbar加到100的时候进度也就结束了。

你也可以选择不使用上下文的形式调用,但要记得结束后对对象进行关闭操作:

pbar = tqdm(total=100)
for i in range(10):
    sleep(0.1)
    pbar.update(10)
pbar.close()

3.模块结合

Tqdm 最妙的地方在于能在命令行中结合使用:

$ find . -name '*.py' -type f -exec cat \{} \; |
    tqdm --unit loc --unit_scale --total 857366 >> /dev/null
100%|█████████████████████████████████| 857K/857K [00:04<00:00, 246Kloc/s]

只需在管道之间插入tqdm(或python -m tqdm),即可将进度条显示到终端上。

备份大目录:

$ tar -zcf - docs/ | tqdm --bytes --total `du -sb docs/ | cut -f1` \
  > backup.tgz
 44%|██████████████▊                   | 153M/352M [00:14<00:18, 11.0MB/s]

这可以进一步美化:

$ BYTES="$(du -sb docs/ | cut -f1)"
$ tar -cf - docs/ \
  | tqdm --bytes --total "$BYTES" --desc Processing | gzip \
  | tqdm --bytes --total "$BYTES" --desc Compressed --position 1 \
  > ~/backup.tgz
Processing: 100%|██████████████████████| 352M/352M [00:14<00:00, 30.2MB/s]
Compressed:  42%|█████████▎            | 148M/352M [00:14<00:19, 10.9MB/s]

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

Redis适合做队列吗?详解三种Redis队列的使用方式

图片

作者:Magic Kaito

来源:水滴与银弹

我经常听到很多人讨论,关于「把 Redis 当作队列来用是否合适」的问题。

有些人表示赞成,他们认为 Redis 很轻量,用作队列很方便。

也些人则反对,认为 Redis 会「丢」数据,最好还是用「专业」的队列中间件更稳妥。

究竟哪种方案更好呢?

这篇文章,我就和你聊一聊把 Redis 当作队列,究竟是否合适这个问题。

我会从简单到复杂,一步步带你梳理其中的细节,把这个问题真正的讲清楚。

看完这篇文章后,我希望你对这个问题你会有全新的认识。

在文章的最后,我还会告诉你关于「技术选型」的思路,文章有点长,希望你可以耐心读完。

图片

从最简单的开始:List 队列

首先,我们先从最简单的场景开始讲起。

如果你的业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就是使用 List 这个数据类型。

因为 List 底层的实现就是一个「链表」,在头部和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。

如果把 List 当作队列,你可以这么来用。

生产者使用 LPUSH 发布消息:

127.0.0.1:6379> LPUSH queue msg1
(integer) 1
127.0.0.1:6379> LPUSH queue msg2
(integer) 2

消费者这一侧,使用 RPOP 拉取消息:

127.0.0.1:6379> RPOP queue
"msg1"
127.0.0.1:6379> RPOP queue
"msg2"

这个模型非常简单,也很容易理解。

图片

但这里有个小问题,当队列中已经没有消息了,消费者在执行 RPOP 时,会返回 NULL。

127.0.0.1:6379> RPOP queue
(nil)   // 没消息了

而我们在编写消费者逻辑时,一般是一个「死循环」,这个逻辑需要不断地从队列中拉取消息进行处理,伪代码一般会这么写:

while true:
    msg = redis.rpop("queue")
    // 没有消息,继续循环
    if msg == null:
        continue
    // 处理消息
    handle(msg)

如果此时队列为空,那消费者依旧会频繁拉取消息,这会造成「CPU 空转」,不仅浪费 CPU 资源,还会对 Redis 造成压力。

怎么解决这个问题呢?

也很简单,当队列为空时,我们可以「休眠」一会,再去尝试拉取消息。代码可以修改成这样:

while true:
    msg = redis.rpop("queue")
    // 没有消息,休眠2s
    if msg == null:
        sleep(2)
        continue
    // 处理消息        
    handle(msg)

这就解决了 CPU 空转问题。

这个问题虽然解决了,但又带来另外一个问题:当消费者在休眠等待时,有新消息来了,那消费者处理新消息就会存在「延迟」。

假设设置的休眠时间是 2s,那新消息最多存在 2s 的延迟。

要想缩短这个延迟,只能减小休眠的时间。但休眠时间越小,又有可能引发 CPU 空转问题。

鱼和熊掌不可兼得。

那如何做,既能及时处理新消息,还能避免 CPU 空转呢?

Redis 是否存在这样一种机制:如果队列为空,消费者在拉取消息时就「阻塞等待」,一旦有新消息过来,就通知我的消费者立即处理新消息呢?

幸运的是,Redis 确实提供了「阻塞式」拉取消息的命令:BRPOP / BLPOP,这里的 B 指的是阻塞(Block)。

图片

现在,你可以这样来拉取消息了:

while true:
    // 没消息阻塞等待,0表示不设置超时时间
    msg = redis.brpop("queue"0)
    if msg == null:
        continue
    // 处理消息
    handle(msg)

使用 BRPOP 这种阻塞式方式拉取消息时,还支持传入一个「超时时间」,如果设置为 0,则表示不设置超时,直到有新消息才返回,否则会在指定的超时时间后返回 NULL。

这个方案不错,既兼顾了效率,还避免了 CPU 空转问题,一举两得。

注意:如果设置的超时时间太长,这个连接太久没有活跃过,可能会被 Redis Server 判定为无效连接,之后 Redis Server 会强制把这个客户端踢下线。所以,采用这种方案,客户端要有重连机制。

解决了消息处理不及时的问题,你可以再思考一下,这种队列模型,有什么缺点?

我们一起来分析一下:

  1. 不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据
  2. 消息丢失:消费者拉取到消息后,如果发生异常宕机,那这条消息就丢失了

第一个问题是功能上的,使用 List 做消息队列,它仅仅支持最简单的,一组生产者对应一组消费者,不能满足多组生产者和消费者的业务场景。

第二个问题就比较棘手了,因为从 List 中 POP 一条消息出来后,这条消息就会立即从链表中删除了。也就是说,无论消费者是否处理成功,这条消息都没办法再次消费了。

这也意味着,如果消费者在处理消息时异常宕机,那这条消息就相当于丢失了。

针对这 2 个问题怎么解决呢?我们一个个来看。

发布/订阅模型:Pub/Sub

从名字就能看出来,这个模块是 Redis 专门是针对「发布/订阅」这种队列模型设计的。

它正好可以解决前面提到的第一个问题:重复消费。

即多组生产者、消费者的场景,我们来看它是如何做的。

Redis 提供了 PUBLISH / SUBSCRIBE 命令,来完成发布、订阅的操作。

图片

假设你想开启 2 个消费者,同时消费同一批数据,就可以按照以下方式来实现。

首先,使用 SUBSCRIBE 命令,启动 2 个消费者,并「订阅」同一个队列。

// 2个消费者 都订阅一个队列
127.0.0.1:6379> SUBSCRIBE queue
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "queue"
3) (integer) 1

此时,2 个消费者都会被阻塞住,等待新消息的到来。

之后,再启动一个生产者,发布一条消息。

127.0.0.1:6379> PUBLISH queue msg1
(integer) 1

这时,2 个消费者就会解除阻塞,收到生产者发来的新消息。

127.0.0.1:6379> SUBSCRIBE queue
// 收到新消息
1) "message"
2) "queue"
3) "msg1"

看到了么,使用 Pub/Sub 这种方案,既支持阻塞式拉取消息,还很好地满足了多组消费者,消费同一批数据的业务需求。

除此之外,Pub/Sub 还提供了「匹配订阅」模式,允许消费者根据一定规则,订阅「多个」自己感兴趣的队列。

// 订阅符合规则的队列
127.0.0.1:6379> PSUBSCRIBE queue.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "queue.*"
3) (integer) 1

这里的消费者,订阅了 queue.* 相关的队列消息。

之后,生产者分别向 queue.p1 和 queue.p2 发布消息。

127.0.0.1:6379> PUBLISH queue.p1 msg1
(integer) 1
127.0.0.1:6379> PUBLISH queue.p2 msg2
(integer) 1

这时再看消费者,它就可以接收到这 2 个生产者的消息了。

127.0.0.1:6379> PSUBSCRIBE queue.*
Reading messages... (press Ctrl-C to quit)
...
// 来自queue.p1的消息
1) "pmessage"
2) "queue.*"
3) "queue.p1"
4) "msg1"

// 来自queue.p2的消息
1) "pmessage"
2) "queue.*"
3) "queue.p2"
4) "msg2"
图片

我们可以看到,Pub/Sub 最大的优势就是,支持多组生产者、消费者处理消息。

讲完了它的优点,那它有什么缺点呢?

其实,Pub/Sub 最大问题是:丢数据

如果发生以下场景,就有可能导致数据丢失:

  1. 消费者下线
  2. Redis 宕机
  3. 消息堆积

究竟是怎么回事?

这其实与 Pub/Sub 的实现方式有很大关系。

Pub/Sub 在实现时非常简单,它没有基于任何数据类型,也没有做任何的数据存储,它只是单纯地为生产者、消费者建立「数据转发通道」,把符合规则的数据,从一端转发到另一端。

一个完整的发布、订阅消息处理流程是这样的:

  1. 消费者订阅指定队列,Redis 就会记录一个映射关系:队列->消费者
  2. 生产者向这个队列发布消息,那 Redis 就从映射关系中找出对应的消费者,把消息转发给它
图片

看到了么,整个过程中,没有任何的数据存储,一切都是实时转发的。

这种设计方案,就导致了上面提到的那些问题。

例如,如果一个消费者异常挂掉了,它再重新上线后,只能接收新的消息,在下线期间生产者发布的消息,因为找不到消费者,都会被丢弃掉。

如果所有消费者都下线了,那生产者发布的消息,因为找不到任何一个消费者,也会全部「丢弃」。

所以,当你在使用 Pub/Sub 时,一定要注意:消费者必须先订阅队列,生产者才能发布消息,否则消息会丢失。

这也是前面讲例子时,我们让消费者先订阅队列,之后才让生产者发布消息的原因。

另外,因为 Pub/Sub 没有基于任何数据类型实现,所以它也不具备「数据持久化」的能力。

也就是说,Pub/Sub 的相关操作,不会写入到 RDB 和 AOF 中,当 Redis 宕机重启,Pub/Sub 的数据也会全部丢失。

最后,我们来看 Pub/Sub 在处理「消息积压」时,为什么也会丢数据?

当消费者的速度,跟不上生产者时,就会导致数据积压的情况发生。

如果采用 List 当作队列,消息积压时,会导致这个链表很长,最直接的影响就是,Redis 内存会持续增长,直到消费者把所有数据都从链表中取出。

但 Pub/Sub 的处理方式却不一样,当消息积压时,有可能会导致消费失败和消息丢失

这是怎么回事?

还是回到 Pub/Sub 的实现细节上来说。

每个消费者订阅一个队列时,Redis 都会在 Server 上给这个消费者在分配一个「缓冲区」,这个缓冲区其实就是一块内存。

当生产者发布消息时,Redis 先把消息写到对应消费者的缓冲区中。

之后,消费者不断地从缓冲区读取消息,处理消息。

图片

但是,问题就出在这个缓冲区上。

因为这个缓冲区其实是有「上限」的(可配置),如果消费者拉取消息很慢,就会造成生产者发布到缓冲区的消息开始积压,缓冲区内存持续增长。

如果超过了缓冲区配置的上限,此时,Redis 就会「强制」把这个消费者踢下线。

这时消费者就会消费失败,也会丢失数据。

如果你有看过 Redis 的配置文件,可以看到这个缓冲区的默认配置:client-output-buffer-limit pubsub 32mb 8mb 60。

它的参数含义如下:

  • 32mb:缓冲区一旦超过 32MB,Redis 直接强制把消费者踢下线
  • 8mb + 60:缓冲区超过 8MB,并且持续 60 秒,Redis 也会把消费者踢下线

Pub/Sub 的这一点特点,是与 List 作队列差异比较大的。

从这里你应该可以看出,List 其实是属于「拉」模型,而 Pub/Sub 其实属于「推」模型

List 中的数据可以一直积压在内存中,消费者什么时候来「拉」都可以。

但 Pub/Sub 是把消息先「推」到消费者在 Redis Server 上的缓冲区中,然后等消费者再来取。

当生产、消费速度不匹配时,就会导致缓冲区的内存开始膨胀,Redis 为了控制缓冲区的上限,所以就有了上面讲到的,强制把消费者踢下线的机制。

好了,现在我们总结一下 Pub/Sub 的优缺点:

  1. 支持发布 / 订阅,支持多组生产者、消费者处理消息
  2. 消费者下线,数据会丢失
  3. 不支持数据持久化,Redis 宕机,数据也会丢失
  4. 消息堆积,缓冲区溢出,消费者会被强制踢下线,数据也会丢失

有没有发现,除了第一个是优点之外,剩下的都是缺点。

所以,很多人看到 Pub/Sub 的特点后,觉得这个功能很「鸡肋」。

也正是以上原因,Pub/Sub 在实际的应用场景中用得并不多。

目前只有哨兵集群和 Redis 实例通信时,采用了 Pub/Sub 的方案,因为哨兵正好符合即时通讯的业务场景。

我们再来看一下,Pub/Sub 有没有解决,消息处理时异常宕机,无法再次消费的问题呢?

其实也不行,Pub/Sub 从缓冲区取走数据之后,数据就从 Redis 缓冲区删除了,消费者发生异常,自然也无法再次重新消费。

好,现在我们重新梳理一下,我们在使用消息队列时的需求。

当我们在使用一个消息队列时,希望它的功能如下:

  • 支持阻塞等待拉取消息
  • 支持发布 / 订阅模式
  • 消费失败,可重新消费,消息不丢失
  • 实例宕机,消息不丢失,数据可持久化
  • 消息可堆积

Redis 除了 List 和 Pub/Sub 之外,还有符合这些要求的数据类型吗?

其实,Redis 的作者也看到了以上这些问题,也一直在朝着这些方向努力着。

Redis 作者在开发 Redis 期间,还另外开发了一个开源项目 disque。

这个项目的定位,就是一个基于内存的分布式消息队列中间件。

但由于种种原因,这个项目一直不温不火。

终于,在 Redis 5.0 版本,作者把 disque 功能移植到了 Redis 中,并给它定义了一个新的数据类型:Stream

下面我们就来看看,它能符合上面提到的这些要求吗?

趋于成熟的队列:Stream

我们来看 Stream 是如何解决上面这些问题的。

我们依旧从简单到复杂,依次来看 Stream 在做消息队列时,是如何处理的?

首先,Stream 通过 XADD 和 XREAD 完成最简单的生产、消费模型:

  • XADD:发布消息
  • XREAD:读取消息

生产者发布 2 条消息:

// *表示让Redis自动生成消息ID
127.0.0.1:6379> XADD queue * name zhangsan
"1618469123380-0"
127.0.0.1:6379> XADD queue * name lisi
"1618469127777-0"

使用 XADD 命令发布消息,其中的「*」表示让 Redis 自动生成唯一的消息 ID。

这个消息 ID 的格式是「时间戳-自增序号」。

消费者拉取消息:

// 从开头读取5条消息,0-0表示从开头读取
127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 0-0
1) 1) "queue"
   2) 1) 1) "1618469123380-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618469127777-0"
         2) 1) "name"
            2) "lisi"

如果想继续拉取消息,需要传入上一条消息的 ID:

127.0.0.1:6379> XREAD COUNT 5 STREAMS queue 1618469127777-0
(nil)

没有消息,Redis 会返回 NULL。

图片

以上就是 Stream 最简单的生产、消费。

这里不再重点介绍 Stream 命令的各种参数,我在例子中演示时,凡是大写的单词都是「固定」参数,凡是小写的单词,都是可以自己定义的,例如队列名、消息长度等等,下面的例子规则也是一样,为了方便你理解,这里有必要提醒一下。

下面我们来看,针对前面提到的消息队列要求,Stream 都是如何解决的?

1) Stream 是否支持「阻塞式」拉取消息?

可以的,在读取消息时,只需要增加 BLOCK 参数即可。

// BLOCK 0 表示阻塞等待,不设置超时时间
127.0.0.1:6379> XREAD COUNT 5 BLOCK 0 STREAMS queue 1618469127777-0

这时,消费者就会阻塞等待,直到生产者发布新的消息才会返回。

2) Stream 是否支持发布 / 订阅模式?

也没问题,Stream 通过以下命令完成发布订阅:

  • XGROUP:创建消费者组
  • XREADGROUP:在指定消费组下,开启消费者拉取消息

下面我们来看具体如何做?

首先,生产者依旧发布 2 条消息:

127.0.0.1:6379> XADD queue * name zhangsan
"1618470740565-0"
127.0.0.1:6379> XADD queue * name lisi
"1618470743793-0"

之后,我们想要开启 2 组消费者处理同一批数据,就需要创建 2 个消费者组:

// 创建消费者组1,0-0表示从头拉取消息
127.0.0.1:6379> XGROUP CREATE queue group1 0-0
OK
// 创建消费者组2,0-0表示从头拉取消息
127.0.0.1:6379> XGROUP CREATE queue group2 0-0
OK

消费者组创建好之后,我们可以给每个「消费者组」下面挂一个「消费者」,让它们分别处理同一批数据。

第一个消费组开始消费:

// group1的consumer开始消费,>表示拉取最新数据
127.0.0.1:6379> XREADGROUP GROUP group1 consumer COUNT 5 STREAMS queue >
1) 1) "queue"
   2) 1) 1) "1618470740565-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618470743793-0"
         2) 1) "name"
            2) "lisi"

同样地,第二个消费组开始消费:

// group2的consumer开始消费,>表示拉取最新数据
127.0.0.1:6379> XREADGROUP GROUP group2 consumer COUNT 5 STREAMS queue >
1) 1) "queue"
   2) 1) 1) "1618470740565-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618470743793-0"
         2) 1) "name"
            2) "lisi"

我们可以看到,这 2 组消费者,都可以获取同一批数据进行处理了。

这样一来,就达到了多组消费者「订阅」消费的目的。

图片

3) 消息处理时异常,Stream 能否保证消息不丢失,重新消费?

除了上面拉取消息时用到了消息 ID,这里为了保证重新消费,也要用到这个消息 ID。

当一组消费者处理完消息后,需要执行 XACK 命令告知 Redis,这时 Redis 就会把这条消息标记为「处理完成」。

// group1下的 1618472043089-0 消息已处理完成
127.0.0.1:6379> XACK queue group1 1618472043089-0
图片

如果消费者异常宕机,肯定不会发送 XACK,那么 Redis 就会依旧保留这条消息。

待这组消费者重新上线后,Redis 就会把之前没有处理成功的数据,重新发给这个消费者。这样一来,即使消费者异常,也不会丢失数据了。

// 消费者重新上线,0-0表示重新拉取未ACK的消息
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 COUNT 5 STREAMS queue 0-0
// 之前没消费成功的数据,依旧可以重新消费
1) 1) "queue"
   2) 1) 1) "1618472043089-0"
         2) 1) "name"
            2) "zhangsan"
      2) 1) "1618472045158-0"
         2) 1) "name"
            2) "lisi"

4) Stream 数据会写入到 RDB 和 AOF 做持久化吗?

Stream 是新增加的数据类型,它与其它数据类型一样,每个写操作,也都会写入到 RDB 和 AOF 中。

我们只需要配置好持久化策略,这样的话,就算 Redis 宕机重启,Stream 中的数据也可以从 RDB 或 AOF 中恢复回来。

5) 消息堆积时,Stream 是怎么处理的?

其实,当消息队列发生消息堆积时,一般只有 2 个解决方案:

  1. 生产者限流:避免消费者处理不及时,导致持续积压
  2. 丢弃消息:中间件丢弃旧消息,只保留固定长度的新消息

而 Redis 在实现 Stream 时,采用了第 2 个方案。

在发布消息时,你可以指定队列的最大长度,防止队列积压导致内存爆炸。

// 队列长度最大10000
127.0.0.1:6379> XADD queue MAXLEN 10000 * name zhangsan
"1618473015018-0"

当队列长度超过上限后,旧消息会被删除,只保留固定长度的新消息。

这么来看,Stream 在消息积压时,如果指定了最大长度,还是有可能丢失消息的。

除了以上介绍到的命令,Stream 还支持查看消息长度(XLEN)、查看消费者状态(XINFO)等命令,使用也比较简单,你可以查询官方文档了解一下,这里就不过多介绍了。

好了,通过以上介绍,我们可以看到,Redis 的 Stream 几乎覆盖到了消息队列的各种场景,是不是觉得很完美?

既然它的功能这么强大,这是不是意味着,Redis 真的可以作为专业的消息队列中间件来使用呢?

但是还「差一点」,就算 Redis 能做到以上这些,也只是「趋近于」专业的消息队列。

原因在于 Redis 本身的一些问题,如果把其定位成消息队列,还是有些欠缺的。

到这里,就不得不把 Redis 与专业的队列中间件做对比了。

下面我们就来看一下,Redis 在作队列时,到底还有哪些欠缺?

与专业的消息队列对比

其实,一个专业的消息队列,必须要做到两大块:

  1. 消息不丢
  2. 消息可堆积

前面我们讨论的重点,很大篇幅围绕的是第一点展开的。

这里我们换个角度,从一个消息队列的「使用模型」来分析一下,怎么做,才能保证数据不丢?

使用一个消息队列,其实就分为三大块:生产者、队列中间件、消费者

图片

消息是否会发生丢失,其重点也就在于以下 3 个环节:

  1. 生产者会不会丢消息?
  2. 消费者会不会丢消息?
  3. 队列中间件会不会丢消息?

1) 生产者会不会丢消息?

当生产者在发布消息时,可能发生以下异常情况:

  1. 消息没发出去:网络故障或其它问题导致发布失败,中间件直接返回失败
  2. 不确定是否发布成功:网络问题导致发布超时,可能数据已发送成功,但读取响应结果超时了

如果是情况 1,消息根本没发出去,那么重新发一次就好了。

如果是情况 2,生产者没办法知道消息到底有没有发成功?所以,为了避免消息丢失,它也只能继续重试,直到发布成功为止。

生产者一般会设定一个最大重试次数,超过上限依旧失败,需要记录日志报警处理。

也就是说,生产者为了避免消息丢失,只能采用失败重试的方式来处理。

但发现没有?这也意味着消息可能会重复发送。

是的,在使用消息队列时,要保证消息不丢,宁可重发,也不能丢弃。

那消费者这边,就需要多做一些逻辑了。

对于敏感业务,当消费者收到重复数据数据时,要设计幂等逻辑,保证业务的正确性。

从这个角度来看,生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。

所以,无论是 Redis 还是专业的队列中间件,生产者在这一点上都是可以保证消息不丢的。

2) 消费者会不会丢消息?

这种情况就是我们前面提到的,消费者拿到消息后,还没处理完成,就异常宕机了,那消费者还能否重新消费失败的消息?

要解决这个问题,消费者在处理完消息后,必须「告知」队列中间件,队列中间件才会把标记已处理,否则仍旧把这些数据发给消费者。

这种方案需要消费者和中间件互相配合,才能保证消费者这一侧的消息不丢。

无论是 Redis 的 Stream,还是专业的队列中间件,例如 RabbitMQ、Kafka,其实都是这么做的。

所以,从这个角度来看,Redis 也是合格的。

3) 队列中间件会不会丢消息?

前面 2 个问题都比较好处理,只要客户端和服务端配合好,就能保证生产端、消费端都不丢消息。

但是,如果队列中间件本身就不可靠呢?

毕竟生产者和消费这都依赖它,如果它不可靠,那么生产者和消费者无论怎么做,都无法保证数据不丢。

在这个方面,Redis 其实没有达到要求。

Redis 在以下 2 个场景下,都会导致数据丢失。

  1. AOF 持久化配置为每秒写盘,但这个写盘过程是异步的,Redis 宕机时会存在数据丢失的可能
  2. 主从复制也是异步的,主从切换时,也存在丢失数据的可能(从库还未同步完成主库发来的数据,就被提成主库)

基于以上原因我们可以看到,Redis 本身的无法保证严格的数据完整性

所以,如果把 Redis 当做消息队列,在这方面是有可能导致数据丢失的。

再来看那些专业的消息队列中间件是如何解决这个问题的?

像 RabbitMQ 或 Kafka 这类专业的队列中间件,在使用时,一般是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,以此保证消息的完整性。这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。

也正因为如此,RabbitMQ、Kafka在设计时也更复杂。毕竟,它们是专门针对队列场景设计的。

但 Redis 的定位则不同,它的定位更多是当作缓存来用,它们两者在这个方面肯定是存在差异的。

最后,我们来看消息积压怎么办?

4) 消息积压怎么办?

因为 Redis 的数据都存储在内存中,这就意味着一旦发生消息积压,则会导致 Redis 的内存持续增长,如果超过机器内存上限,就会面临被 OOM 的风险。

所以,Redis 的 Stream 提供了可以指定队列最大长度的功能,就是为了避免这种情况发生。

但 Kafka、RabbitMQ 这类消息队列就不一样了,它们的数据都会存储在磁盘上,磁盘的成本要比内存小得多,当消息积压时,无非就是多占用一些磁盘空间,相比于内存,在面对积压时也会更加「坦然」。

综上,我们可以看到,把 Redis 当作队列来使用时,始终面临的 2 个问题:

  1. Redis 本身可能会丢数据
  2. 面对消息积压,Redis 内存资源紧张

到这里,Redis 是否可以用作队列,我想这个答案你应该会比较清晰了。

如果你的业务场景足够简单,对于数据丢失不敏感,而且消息积压概率比较小的情况下,把 Redis 当作队列是完全可以的。

而且,Redis 相比于 Kafka、RabbitMQ,部署和运维也更加轻量。

如果你的业务场景对于数据丢失非常敏感,而且写入量非常大,消息积压时会占用很多的机器资源,那么我建议你使用专业的消息队列中间件。

总结

好了,总结一下。这篇文章我们从「Redis 能否用作队列」这个角度出发,介绍了 List、Pub/Sub、Stream 在做队列的使用方式,以及它们各自的优劣。

之后又把 Redis 和专业的消息队列中间件做对比,发现 Redis 的不足之处。

最后,我们得出 Redis 做队列的合适场景。

这里我也列了一个表格,总结了它们各自的优缺点。

图片

后记

最后,我想和你再聊一聊关于「技术方案选型」的问题。

你应该也看到了,这篇文章虽然始于 Redis,但并不止于 Redis。

我们在分析 Redis 细节时,一直在提出问题,然后寻找更好的解决方案,在文章最后,又聊到一个专业的消息队列应该怎么做。

其实,我们在讨论技术选型时,就是一个关于如何取舍的问题。

而这里我想传达给你的信息是,在面对技术选型时,不要不经过思考就觉得哪个方案好,哪个方案不好

你需要根据具体场景具体分析,这里我把这个分析过程分为 2 个层面:

  1. 业务功能角度
  2. 技术资源角度

这篇文章所讲到的内容,都是以业务功能角度出发做决策的。

但这里的第二点,从技术资源角度出发,其实也很重要。

技术资源的角度是说,你所处的公司环境、技术资源能否匹配这些技术方案

这个怎么解释呢?

简单来讲,就是你所在的公司、团队,是否有匹配的资源能 hold 住这些技术方案。

我们都知道 Kafka、RabbitMQ 是非常专业的消息中间件,但它们的部署和运维,相比于 Redis 来说,也会更复杂一些。

如果你在一个大公司,公司本身就有优秀的运维团队,那么使用这些中间件肯定没问题,因为有足够优秀的人能 hold 住这些中间件,公司也会投入人力和时间在这个方向上。

但如果你是在一个初创公司,业务正处在快速发展期,暂时没有能 hold 住这些中间件的团队和人,如果贸然使用这些组件,当发生故障时,排查问题也会变得很困难,甚至会阻碍业务的发展。

而这种情形下,如果公司的技术人员对于 Redis 都很熟,综合评估来看,Redis 也基本可以满足业务 90% 的需求,那当下选择 Redis 未必不是一个好的决策。

所以,做技术选型不只是技术问题,还与人、团队、管理、组织结构有关

也正是因为这些原因,当你在和别人讨论技术选型问题时,你会发现每个公司的做法都不相同。

毕竟每个公司所处的环境和文化不一样,做出的决策当然就会各有差异。

如果你不了解这其中的逻辑,那在做技术选型时,只会趋于表面现象,无法深入到问题根源。

而一旦你理解了这个逻辑,那么你在看待这个问题时,不仅对于技术会有更加深刻认识,对技术资源和人的把握,也会更加清晰。

希望你以后在做技术选型时,能够把这些因素也考虑在内,这对你的技术成长之路也是非常有帮助的。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

阿里Python 自动化工具 tidevice 使用指南

图片

1. 前言

最近,阿里内部开源了一个 iOS 端由 Python 编写的自动化工具,即:tidevice

它是一款跨平台的自动化开源工具,不依赖 Xcode 就可以启动 WebDriverAgent( WDA ),这也就意味着 Windows 可以直接运行 iOS 自动化脚本

项目地址:

https://github.com/alibaba/taobao-iphone-device

2. 准备

进入到虚拟环境,先安装 tidevice 的依赖包

# 安装依赖包
pip3 install -U "tidevice[openssl]" 

# 查看tidevice版本,检查是否安装成功
tidevice version

3. 常见功能

将 iPhone 设备连接到电脑上,就可以使用 tidevice 提供的功能了

3-1  查看设备列表

# 二选一
tidevice list

# json数据显示
tidevice list --json

3-2  安装、卸载应用

安装对应的关键字为:「 install

tidevice 同时支持安装本地 ipa 包和远链下的 ipa 应用包

另外,tidevice 可以使用「 –udid 」参数将应用安装到某一台设备中

# 安装本地应用
tidevice install example.ipa

# 根据udid参数,指定设备安装应用
tidevice --udid $UDID install https://example.org/example.ipa
tidevice install https://.....ipa

卸载对应的关键字为:「 uninstall

通过应用包名可以卸载设备上的某一个应用

# 卸载应用(通过包名)
# 比如:知识星球App包名为:com.unnoo.quan
tidevice uninstall com.unnoo.quan

3-3  启动、关闭应用

启动应用对应的关键字为:「 launch

关闭应用对应的关键字为:「 kill

# 打开应用
tidevice launch com.unnoo.quan

# 停止杀死应用
tidevice kill com.unnoo.quan

3-4  已安装应用及名称、版本

使用「 tidevice applist 」命令,可以查看设备上已经安装的应用、应用名称、应用版本号

# 查看设备应用信息列表
localhost:tidevice_demo xingag
 
nbsp;tidevice applist
com.tencent.xin WeChat 8.0.2
com.ss.iphone.ugc.Aweme 抖音 13.4.0
com.tencent.mttlite QQ浏览器 10.8.3
com.panasonic.jp.imageapp Image App 1.10.17
com.unnoo.quan 知识星球 4.20.2
co.visualsupply.cam VSCO 189
com.ucweb.iphone.lowversion UC浏览器 13.1.5
com.niksoftware.snapseedforipad Snapseed 2.19.5
com.autonavi.amap 高德地图 10.76.0
com.lagou.education 拉勾教育 1.4.9
com.xunmeng.pinduoduo 拼多多 5.37.0
com.tencent.QQMusic QQ音樂 10.5.5
io.ideamp.feelcab Feelca B 1.6.0
com.chinaunicom.mobilebusiness 手机营业厅 8.00.01
com.feiyu-tech.vico Vicool 1.2.26
com.ss.iphone.article.News 今日头条 7.9.3
com.sogou.sogouinput 搜狗输入法 10.18.1
org.geekbang.GeekTime 极客时间 2.9.2
com.burbn.hyperlapse Hyperlapse 1.3.4
com.apple.itunesu iTunes U 3.8

3-5  设备信息

利用「 tidevice info 」可以查看设备信息,比如:设备序列号、系统版本、CPU、本机号码、时间戳、蓝牙地址、MAC 地址等信息

# 查看设备信息
localhost:~ xingag
 
nbsp;tidevice info
MarketName:       iPhone SE (1st generation)
DeviceName:       xingag
ProductVersion:   14.4
ProductType:      iPhone8,4
ModelNumber:      MP892
SerialNumber:     ...
CPUArchitecture:  arm64
ProductName:      iPhone OS
ProtocolVersion:  2
RegionInfo:       ZP/A
TimeIntervalSince1970: 1616979424.235399
TimeZone:         Asia/Shanghai
WiFiAddress:      58:e2:8f:68:4c:b0
BluetoothAddress: 58:e2:8f:68:4c:b1
BasebandVersion:  10.40.01
...

如果要查看设备的电池信息,可以使用关键字 「 –domain + 应用包名 」来获取

# 查看设备电池信息
localhost:~ xingag
 
nbsp;tidevice info --domain com.apple.mobile.battery --json
{
    "BatteryCurrentCapacity"54,
    "BatteryIsCharging"true,
    "ExternalChargeCapable"true,
    "ExternalConnected"true,
    "FullyCharged"false,
    "GasGaugeCapability"true,
    "HasBattery"true
}

3-6  重启及截图

重启对应的关键字为:「 reboot

截图对应的关键字为:「 screenshot

# 重启设备
tidevice reboot

# 截图并保存到文件 screenshot.jpg 中
tidevice screenshot screenshot.jpg

3-7  系统日志及更多帮助

查看系统日志的命令为:「 tidevice syslog 」,当然也可以配置 grep 参数进行过滤 

# 查看系统日志
tidevice syslog

# 更多帮助
tidevice -h

4. 自动化步骤

要完成 iOS 端的自动化,我们还需要做一些准备工作

4-1 安装 WDA

我们需要将 WebDriverAgent 安装到 iOS 设备上,通过它驱动手机进行一系列自动化操作

PS:由于 Facebook WDA 很长时间没有更新了,这里建议直接使用 Appium WDA

https://github.com/appium/WebDriverAgent

下载项目后,使用 Xcode 打开,Scheme 选择 WebDriverAgentRunner,设备选择真机

然后配置开发者证书,可以参考下面链接进行配置

https://testerhome.com/topics/7220

最后,Xcode 选择 Project 下的 Test 将项目运行到真机设备上

注意:第一次运行 WDA 可能运行失败,需要手动在设置中对证书添加信任

4-2  运行 WDA

真机设备安装完 WDA 后,就可以脱离 Mac,使用 Windows 进行自动化操作了

使用「 tidevice wdaproxy 」命令可以运行 WDA 和端口转发

 # 运行 WDA
# 端口转发到8100
# 应用包名:com.facebook.WebDriverAgentRunner.xingag23.xctrunner
idevice wdaproxy -B com.facebook.WebDriverAgentRunner.xingag23.xctrunner --port 8100

PS:由于 Xcode 设置证书的时候,更改过 bundle_id,所以这里的包名是一个变量

4-3  编写自动化脚本

接下来,就可以使用 Appium 或 facebook-wda 编写脚本并运行到 iOS 设备上了

以 facebook-wda 为例

首先,我们安装对应的依赖

# 安装facebook-wda依赖
pip3 install -U facebook-wda

接着,编写自动化脚本并运行测试

import wda
import time

# 连接设备
c1 = wda.USBClient()

# c1 = wda.Client("http://10.3.209.175:8100")
print(c.info)

# 极客时间
bundle_id = 'org.geekbang.GeekTime'

# 通过app的bundleId启动App
s = c1.session(bundle_id)

# 休眠5s
s.sleep(5)

# 截图保存到本地
c1.screenshot('sc.png')

# 回到桌面
s.home()

5. 最后

阿里这款 iOS 端自动化工具的主要优点是启动速度快,内存占用低;更重要的是,它是跨平台,稳定性相对较高

如果你觉得文章还不错,请大家 点赞、分享、留言 下,因为这将是我持续输出更多优质文章的最强动力!

本文转自Airpython.

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!


​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pandas 性能优化

有趣好用的Python教程