0%

Python进阶

《流畅的Python》学习笔记
学习Python之前,最好熟悉一下Python的语言和风格规范
https://google-styleguide.readthedocs.io/zh_CN/latest/google-python-styleguide/contents.html
原项目:https://github.com/google/styleguide

Ch 0 Python的语言和风格规范

语言规范

Lint

  • 定义
    pylint是一个在Python源代码中查找bug的工具,对于C和C++这样的不那么动态的(原文是less dynamic)语言,这些bug通常由编译器来捕获,由于Python的动态特性,有些警告可能不对,不过伪警告应该很少。
  • 优点
    可以捕获容易忽视的错误,例如输入错误,使用未赋值的变量等。
  • 缺点
    pylint isn't perfect. To take advantage of it, sometimes we'll need to write around it, suppress its warnings or fix it.
  • 结论
    确保对你的代码运行pylint,抑制不准确的警告,以便能够将其他警告暴露出来。可以通过设置一个行注释来抑制警告,例如:
    dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

导入包、模块

  • 定义
    模块间共享代码的重用机制。
  • 优点
    命名空间管理约定十分简单,每个标识符的源都用一种一致的方式指示,x.Obj表示Obj对象定义在模块x中。
  • 缺点
    模块名仍可能冲突,有些模块名太长,不太方便。
  • 结论
    导入时不要使用相对名称,即使模块在同一个包中,也要使用完整包名,这能帮助你避免无意间导入一个包两次。
    所有的新代码都应该用完整包名来导入每个模块。

异常

  • 结论
    异常必须遵守特定条件:
    • 优先合理的使用内置异常类。比如 ValueError 指示了一个程序错误,比如在方法需要正数的情况下传递了一个负数错误。不要使用 assert 语句来验证公共API的参数值,assert 是用来保证内部正确性的,而不是用来强制纠正参数使用,若需要使用异常来指示某些意外情况,不要用 assert,用 raise 语句,例如:
      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.
      raise ValueError(f'Min. port must be at least 1024, not {minimum}.')
      port = self._find_next_open_port(minimum)
      if not port:
      raise ConnectionError(
      f'Could not connect to service on port {minimum} or higher.')
      assert port >= minimum, (
      f'Unexpected port {port} when minimum was {minimum}.')
      return port
    • 模块或包应该定义自己的特定域的异常基类,这个基类应该从内建的Exception类继承,模块的异常基类后缀应该叫做 Error
    • 永远不要使用 except: 语句来捕获所有异常,也不要捕获 Exception 或者 StandardError,除非你打算重新触发该异常,或者你已经在当前线程的最外层(记得还是要打印一条错误消息),在异常这方面,Python非常宽容,except: 真的会捕获包括Python语法错误在内的任何错误,使用 except: 很容易隐藏真正的bug。
    • 尽量减少try/except块中的代码量. try块的体积越大,期望之外的异常就越容易被触发,这种情况下,try/except块将隐藏真正的错误。
    • 使用finally子句来执行那些无论try块中有没有异常都应该被执行的代码,这对于清理资源常常很有用,例如关闭文件。

全局变量

  • 结论
    避免使用全局变量,鼓励使用模块级的常量。因为导入时会对模块级变量赋值,可能改变模块行为。例如 MAX_HOLY_HANDGRENADE_COUNT = 3,注意常量命名必须全部大写,用_分隔。若必须要使用全局变量,应在模块内声明全局变量,并在名称前_使之成为模块内部变量,外部访问必须通过模块级的公共函数。

嵌套/局部/内部类或函数

  • 定义
    类可以定义在方法,函数或者类中,函数可以定义在方法或函数中,封闭区间中定义的变量对嵌套函数是只读的,(译者注:即内嵌函数可以读外部函数中定义的变量,但是无法改写,除非使用 nonlocal)
  • 优点
    允许定义仅用于有效范围的工具类和函数,在装饰器中比较常用。
  • 缺点
    嵌套类或局部类的实例不能序列化(pickled),内嵌的函数和类无法直接测试,同时内嵌函数和类会使外部函数的可读性变差。
  • 结论
    使用内部类或者内嵌函数可以忽视一些警告.但是应该避免使用内嵌函数或类,除非是想覆盖某些值.若想对模块的用户隐藏某个函数,不要采用嵌套它来隐藏,应该在需要被隐藏的方法的模块级名称加 _ 前缀,这样它依然是可以被测试的.

  • 定义
    pylint是一个在Python源代码中查找bug的工具,对于C和C++这样的不那么动态的(原文是less dynamic)语言,这些bug通常由编译器来捕获,由于Python的动态特性,有些警告可能不对,不过伪警告应该很少。
  • 优点
    可以捕获容易忽视的错误,例如输入错误,使用未赋值的变量等。
  • 缺点
    pylint isn't perfect. To take advantage of it, sometimes we'll need to write around it, suppress its warnings or fix it.
  • 结论
    确保对你的代码运行pylint,抑制不准确的警告,以便能够将其他警告暴露出来。可以通过设置一个行注释来抑制警告,例如:
    dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

  • 定义
    pylint是一个在Python源代码中查找bug的工具,对于C和C++这样的不那么动态的(原文是less dynamic)语言,这些bug通常由编译器来捕获,由于Python的动态特性,有些警告可能不对,不过伪警告应该很少。
  • 优点
    可以捕获容易忽视的错误,例如输入错误,使用未赋值的变量等。
  • 缺点
    pylint isn't perfect. To take advantage of it, sometimes we'll need to write around it, suppress its warnings or fix it.
  • 结论
    确保对你的代码运行pylint,抑制不准确的警告,以便能够将其他警告暴露出来。可以通过设置一个行注释来抑制警告,例如:
    dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

  • 定义
    pylint是一个在Python源代码中查找bug的工具,对于C和C++这样的不那么动态的(原文是less dynamic)语言,这些bug通常由编译器来捕获,由于Python的动态特性,有些警告可能不对,不过伪警告应该很少。
  • 优点
    可以捕获容易忽视的错误,例如输入错误,使用未赋值的变量等。
  • 缺点
    pylint isn't perfect. To take advantage of it, sometimes we'll need to write around it, suppress its warnings or fix it.
  • 结论
    确保对你的代码运行pylint,抑制不准确的警告,以便能够将其他警告暴露出来。可以通过设置一个行注释来抑制警告,例如:
    dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

  • 定义
    pylint是一个在Python源代码中查找bug的工具,对于C和C++这样的不那么动态的(原文是less dynamic)语言,这些bug通常由编译器来捕获,由于Python的动态特性,有些警告可能不对,不过伪警告应该很少。
  • 优点
    可以捕获容易忽视的错误,例如输入错误,使用未赋值的变量等。
  • 缺点
    pylint isn't perfect. To take advantage of it, sometimes we'll need to write around it, suppress its warnings or fix it.
  • 结论
    确保对你的代码运行pylint,抑制不准确的警告,以便能够将其他警告暴露出来。可以通过设置一个行注释来抑制警告,例如:
    dict = 'something awful'  # Bad Idea... pylint: disable=redefined-builtin

风格规范

序幕

Ch 1 Python数据模型

数据(对象)模型是对Python框架的描述,规范了这门语言自身构建模块的接口,这些模块包括序列、迭代器、函数、类和上下文管理器等。
而无论在哪种框架下编程,都会大量实现会被框架本身调用的方法,Python里会使用特殊方法去激活一些基本的对象操作,这类方法称为魔术方法(magic method)或双下方法(dunder method),形如__getitem__

一摞Python风格的纸牌

如下代码段:

import collections
from random import choice

#collections.namedtuple用于构建一个简单的类,
#该类用来构建只有少数属性但没有方法的对象,如数据库条目。
Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck:
ranks = [str(n) for n in range(2,11)] + list('JQKA')
suits = '方块 梅花 红桃 黑桃'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
"""提供了元素定位deck[0]
将[]操作交给self._cards列表,所以deck类自动支持切片操作且可迭代(支持反向迭代reversed(deck))
迭代通常是隐式的,若一个集合类型未实现__contains__方法,那么in运算就会按顺序做一次迭代搜索
"""
return self._cards[position]

if __name__ == "__main__":
card1 = Cards(3, '红桃')
print(card1)
deck = FrenchDeck()
print(len(deck))
print(deck[-1])
print(choice(deck))
print(Card('7', '红桃'))
# TODO(xyr): 排序和洗牌

output:
Card(rank=3, suit='红桃')
52
Card(rank='A', suit='黑桃')
Card(rank='3', suit='黑桃')
True

如何使用特殊方法

  • 首先,特殊方法是为了被Python解释器调用,而非自己调用,除非有大量元编程存在,或者在自己的子类的__init__方法中调用超累的构造器。
  • 特殊方法的调用一般是隐式的,比如for i in x:调用的其实是iter(x),而这个函数背后则是x.__iter__()方法(前提是这个方法已经在x中被实现)。
  • 通过内置的函数(len、str等)使用特殊方法是最好的选择。
  • 如果是Python内置的类型(list、str等),CPython的__len__实际上会直接返回PyVarObject(表示内存中长度可变的内置对象的C语言结构体)里的ob_size属性。
  • 不要随意添加特殊方法,以防在后面的更新中被Python内部使用而产生冲突。

模拟数值类型

利用特殊方法,可以让自定义对象通过“+”(或别的运算符)进行运算,例如下面的一个自定义的二维向量类,其中的+、*和abs(取绝对值或取模)运算即是由这些特殊方法实现。

from math import hypot

class Vector:

def __init__(self, x=0, y=0):
self.x = x
self.y = y

def __repr__(self):
return 'Vector(%r, %r)' % (self.x, self.y)

def __abs__(self):
return hypot(self.x, self.y)

def __bool__(self):
return bool(abs(self))
# return bool(self.x or self.y) #更加高效

def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector(x, y)

def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar) #未考虑交换律

if __name__ == "__main__":
v1 = Vector(3, 4)
v2 = Vector(1,2)
print(v1+v2)
print(abs(v1))
print(v1*3)

output:
Vector(4, 6)
5.0
Vector(9, 12)

字符串表示形式

  • Python通过内置函数repr将一个对象用字符串的形式表达出来,repr就是通过__repr__这个特殊方法得到一个对象的字符串的形式,若未实现__repr__,打印一个向量实例时得到的会是<Vector object at 0x10e100070>
  • 格式化字符串的两种方法,使用%和使用str.format均利用了repr。
  • __repr__中使用%r来获取对象各个属性的标准字符串表示形式。
  • __repr__所返回的字符串应准确无歧义,且尽可能表达出如何用代码创建出这个被打印的对象。
  • __repr____str__的区别在于前者方便记录和调试日志,后者在str()函数被使用,或print()打印对象时被调用,且返回的字符串对终端用户更友好。两者优先实现__repr__,因为一个函数没有__str__而去调用它会用__repr__来代替。(详见http://stackoverflow.com/questions/1436703/differencebetween-str-and-repr-in-python)

算术运算符

通过__add____mul__实现了+和*,这里不改变操作对象,而是返回一个新创建的向量对象。

自定义的布尔值

  • bool(x)背后调用x.__bool__(),若未实现__bool__方法,那么bool(x)会尝试调用x.__len__(),0为False,否则返回True。
  • 默认情况下自定义类的实例总为真,除非这个类对__bool__或者__len__函数有自己的实现。
  • 更高效的__bool__方法,
    def __bool__(self):
    return bool(self.x or self.y)

特殊方法一览

Python语言参考手册中的“DataModel”,(https://docs.python.org/3/reference/datamodel.html)一章列出了83个特殊方法的名字,其中47个用于实现算术运算、位运算和比较操作。

为什么len不是普通方法

“实用胜于纯粹,不能让特例特殊到破坏既定规则。”
len之所以不是一个普通方法,是为了让Python自带的数据结构可以走后门,abs也是同理。但是多亏了它是特殊方法,我们也可以把len用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点。

数据结构

Ch2 序列构成的数组

无论那种数据结构,都共用一套操作:迭代切片排序拼接

内置序列类型概览

列表推导和生成器表达式

元组不仅仅是不可变的列表

切片

对序列使用+和*

序列的增量赋值

list.sort方法和内置函数sorted

用bisect来管理已排序的序列

当列表不是首选时