python进阶内容,帮助理解让你摸不着头脑的一些问题

python之前

术语

  • KISS 原则:“Keep It Simple, Stupid”的首字母缩写。这个原则要求尽量寻找最简单的方案,尽量减少可变部分。这个警句是 Kelly Johnson 首创的。
  • 统一访问原则:不管服务是由存储还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用。

objects, types, classes, instances的区别

object 对象

对象 (object) 是 python 对数据的抽象总称。在 python 编程中,所有的数据都是以 对象 或者 对象 之间的关系表示的,每个对象都有对应的 ID、类型(type)和值(value)。

[data model 原文] Objects are Python’s abstraction for data. All data in a Python program is represented by objects or by relations between objects. (In a sense, and in conformance to Von Neumann’s model of a “stored program computer,” code is also represented by objects.)

Every object has an identity, a type and a value.

python 中一切都是对象,list、42、函数、模块都是 object

def foo(): 
    pass

type(foo), id(foo)
# (<class 'function'>, 38110760)

type(foo.__code__), id(foo.__code__)
# (<class 'code'>, 38111680)

type 类型

一个对象的类型决定了该对象所能支持的操作(比如,能不能计算长度),也定义了 该类型的对象 的 值。

An object’s type determines the operations that the object supports (e.g., “does it have a length?”) and also defines the possible values for objects of that type.

因此每个对象都属于某个类型 (type),通过type内置函数可以确定对象的类型。类型 type 也是一个对象,因此它也有对应的类型,而这个类型是 type (The type is an object too, so it has a type of its own, which is called type.)

type(42)
# <class 'int'>

type(type(42))
# <class 'type'>

type(type(type(42)))
# <class 'type'>

是的,就这样无限循环下去了 ,Yep, it’s turtles all the way down.

Classes 类

A class defines the blueprint, which can be instantiated to create Object(s)

class 是一种机制,让我们通过 python 代码 创建新的用户自定义的类型 (type)

class Joe:
    pass

j = Joe()
type(j)
# <class '__main__.Joe'>

使用 class 机制,我们创建了 Joe——用户定义的新类型,j是类Joe的一个实例,换句话说:j是一个对象,它的类型是Joe

Class 和 type 是指代相同概念的两个名称的示例。为了避免产生误解,当表示类型时,我尽量说”type” ,当指的是通过 class 新创建的类型时,我尽量说 “user-defined class” (or “user-defined type”)

I will always try to say “type” when I mean a type, and “user-defined class” (or “user-defined type”) when referring to a new type created using the class construct.

Instances 实例

不像 class 和 type,instance 和 object 是同义词,可以这样理解:对象(object)是某种类型(type)的实例 (instance),因此 “42 is an instance of the type int“ 等价于 “42 is an int object”.

python 版本

import platform 
print(platform.python_version())
# 2.7.15

import sys 
print(sys.version)
# 2.7.15 |Anaconda, Inc.| (default, Oct 23 2018, 13:35:16) 
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)]

print(sys.version_info)
# sys.version_info(major=2, minor=7, micro=15, releaselevel='final', serial=0)

python 反编译

dis模块为反汇编 Python 函数字节码提供了简单的方式

from dis import dis
b = 6
def f(a):
    global b
    print(a)
    print(b)
    b=9

>>> dis(f)
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP

  4           8 LOAD_GLOBAL              0 (print)
             10 LOAD_GLOBAL              1 (b)
             12 CALL_FUNCTION            1
             14 POP_TOP

  5          16 LOAD_CONST               1 (9)
             18 STORE_GLOBAL             1 (b)
             20 LOAD_CONST               0 (None)
             22 RETURN_VALUE

数据结构

序列基础部分

常见序列

序列的种类

  • 容器序列存放的是它们所包含的任意类型的对象的引用。
  • 扁平序列里存放的是元素的值本身,而不是其他对象的引用。换句话说,扁平序列其实是一段连续的内存空间。由此可见扁平序列其实更加紧凑,但是它里面只能存放诸如字符、字节和数值这种基础类型。

推导式

推导式 包括:列表推导、生成器表达式,以及同它们很相似的集合(set)推导和字典(dict)推导。
通常的原则是,只用列表推导来创建新的列表,并且尽量保持简短。 如果列表推导的代码超过了两行,你可能就要考虑是不是得用 for 循环重写了。

Python 2.x 中,在列表推导中 for 关键词之后的赋值操作可能会影响列表推导上下文 中的同名变量。像下面这个 Python 2.7 控制台对话,x 原本的值被取代了:

x = 'my precious'
dummy = [x for x in 'ABC]
print(x)
# 'C'

但是这种情况在 Python 3 中是不会出现的。

推导式在Python3中都有了自己的局部作用域,就像函数似的。表达式内部的变量和赋值只在局部起作用,表达式的上下文里的同名变量不会被覆盖,还可以被正常引用。

生成器

生成器表达式的语法跟列表推导差不多,只不过把方括号换成圆括号而已。

如果生成器表达式是一个函数调用过程中的唯一参数,那么不需要额外再用括号把它围起来。

symbols = '$¢£¥€¤'
t = tuple( ord(symbol) for symbol in symbols )
# (36, 162, 163, 165, 8364, 164)

元组

元组的不可变性 其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
t1 == t2
# True
id(t1[-1])
# 4302515784
t1[-1].append(99)
t1 
# (1, 2, [30, 40, 99])
id(t1[-1]) 
# 4302515784
拆包

在进行拆包的时候,我们不总是对元组里所有的数据都感兴趣,_占位符能作为占位符。

如果做的是国际化软件,那么_可能就不是一个理想的占位符,因为它也是 gettext.gettext 函数的常用别名,gettext 模块的文档 (https://docs.python.org/3/library/gettext.html)里提到了这一点。在其他情况下,`_` 会 是一个很好的占位符。

除此之外,在元组拆包中使用 *也可以帮助我们把注意力集中在元组的部分元素上,来处理剩下的元素

namedtuple

元组已经设计得很好用了,但作为记录来用的话,还是少了一个功能:我们时常会需要给记录中的字段命名。namedtuple函数的出现帮我们解决了这个问题。

  1. 创建一个具名元组需要两个参数,一个是类名,另一个是类的各个字段的名字。后者 可以是由数个字符串组成的可迭代对象,或者是由空格分隔开的字段名组成的字符串

    City = namedtuple('City', 'name country population coordinates')
    City = namedtuple('City', [name,country,population,coordinates])
    
  2. 类属性_fields、类方法_make(iterable) _replace(key=new_value)和实例方法_asdict()

    • _fields属性是一个包含这个类所有字段名称的元组。
    • _make()通过接受一个可迭代对象来生成这个类的一个实例,它的作用跟City(*delhi_data) 是一样的。
    • _replace(key=new_value),可以创建一个元组的浅副本,并修改其中的一些字段key等于new_value
    • _asdict() 把具名元组以collections.OrderedDict的形式返回,我们可以利用它 来把元组里的信息友好地呈现出来。

对序列的操作

切片

a:b:c 这种用法只能作为索引或者下标用在 [] 中来返回一个切片对象:slice(a, b, c)

命名切片

可以给切片对象进行命名,方便管理

invoice = """
0.....6................................40........52...55........
1909 Pimoroni PiBrella                  $17.50 3 $52.50
1489 6mm Tactile Switch x20             $4.95 2 $9.90
1510 Panavise Jr. - PV-201              $28.00 1 $28.00
1601 PiTFT Mini Kit 320x240             $34.95 1 $34.95
"""

SKU = slice(0, 6)                        # 第1列
DESCRIPTION = slice(6, 40)    # 第2列
UNIT_PRICE = slice(40, 52)    # 第3列
QUANTITY = slice(52, 55)        # 第4列
ITEM_TOTAL = slice(55, None)# 第5列

line_items = invoice.split('\n')[2:]
for item in line_items:
    print(item[UNIT_PRICE], item[DESCRIPTION]) # 打印第3列、第2列
切片赋值
  • 如果赋值的对象是一个切片,那么赋值语句的右侧必须是个可迭代对象。即便只有单独一个值,也要把它转换成可迭代的序列。
  • 切片的长度,可以不等于 赋值的迭代对象的长度,可以认为是把一个片段剪切为另一个片段
  • 但是如果是[a:b:c]中跨度c不为1的话,长度就必须相同。
l = list(range(10))
# l [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

l[2:5] = [20, 30]    # 长度大于的情况
# l [0, 1, 20, 30, 5, 6, 7, 8, 9]

del l[5:7]
# l [0, 1, 20, 30, 5, 8, 9]

l[3::2] = [11, 22]
# l [0, 1, 20, 11, 5, 22, 9]

l[2:5] = 100
# 报错

l[2:5] = [100]
# l [0, 1, 100, 22, 9]

对序列使用+*

c = a + b:将a、b两个 list 中的元素复制到一个新的 list c 中

c = a * n:将 list a[a,b,c]中的元素,复制n次,得到新的 list c[a,b,c, a,b,c, a,b,c]

a,b,c不能是引用类型的对象,不然复制后,改变其中一个就会改变所有

案例:生成一个3x3的方格

正确例子,使用列表生成式

board = [['_'] * 3 for i in range(3)]
print(board)
# [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]
board[1][2] = 'X'
# board [['_', '_', '_'], ['_', '_', 'X'], ['_', '_', '_']]

等价于

board = []
for i in range(3): 
    row=['_'] * 3 
    board.append(row)

错误例子,相当于weird_board = [['_','_','_']] * 3, 将内部元素 list 复制了3份,但其实是同一个引用

weird_board = [['_'] * 3] * 3

print(weird_board)
# [['_', '_', '_'], ['_', '_', '_'], ['_', '_', '_']]

weird_board[1][2] = 'O'
print(weird_board)
# weird_board [['_', '_', 'O'], ['_', '_', 'O'], ['_', '_', 'O']]

等价于

row=['_'] * 3 
board = [] 
for i in range(3):
    board.append(row)

增量赋值+=

+= 背后的特殊方法是 __iadd__ (用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用 __add__

在这个表达式中,变量名会不会被关联到新的对象,完全取决于这个类型有没有实现 `iadd` 这 个方法。

  • 如果 a 实现了 __iadd__ 方法,就会调用这个方法。同时对可变序列(例如 list、bytearray 和 array.array)来说,a 会就地改动,就像调用了 a.extend(b) 一样。
  • 如果 a 没有实现 __iadd__ 的话,a += b 这个表达式的效果就变得跟 a = a + b 一样了:首先计算 a + b,得到一个新的对象,然后赋值给 a。

一个诡异的现象

t = (1, 2, [30, 40])
t[2] += [50, 60]    # t[2].extend([50, 60]) 就能避免这个异常
Traceback (most recent call last):

    File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment

print(t)
# (1, 2, [30, 40, 50, 60])

t[2].extend([50, 60])就能避免这个异常

背后原理

需要理解s[a] += b 生成的字节码:

  1. 将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)。

  2. 计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对象(也就是示例 2-15 里的列表)。

  3. s[a] = TOS 赋值。这一步失败,是因为 s 是不可变的元组(示例 2-15 中的元组 t)。\

原因就是:第1步中将引用存在了临时变量中,最后又重新进行赋值,tuple 又不可变,因此报错,得到的经验:

  • 不要把可变对象放在元组里面
  • 增量赋值不是一个原子操作。我们刚才也看到了,它虽然抛出了异常,但还是完成了 操作。

排序

  • list.sort 方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的 返回值是 None 的原因
  • 内置函数 sorted,它会新建一个列表作为返回值

参数:

  1. key:传入一个只有一个参数的函数,列表中的每个元素都会作为参数传入,得到的返回值将作为排序算法依赖的对比关键字。默认是一个恒等函数——参数是自己,返回值也是自己的函数
  2. reverse: False(默认):升序,True:降序

其他列表类型

array

与C语言的数组很像,只能存放同一种类型的数组,因此比较节省空间。

from array import array
from random import random

floats = array('d', (random() for i in range(10**7))) # 从生成器中获取数据
print(floats[-1])
# 0.07802343889111107

'''保存数据到文件中'''
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()

'''从文件中读取数据'''
floats2 = array('d')
fp = open('floats.bin', 'rb')
floats2.fromfile(fp, 10**7)
fp.close()
print(floats2[-1])
# 0.07802343889111107

从 Python 3.4 开始,数组类型不再支持诸如 list.sort() 这种就地排序方法。要给 数组排序的话,得用 sorted 函数新建一个数组:a = array.array(a.typecode, sorted(a))

memoryview

不太了解,主要与numpy 相关

collections.deque

我们可以把列表list当作栈或者队列来用(比如,把 .append 和 .pop(0) 合起来用,就能模拟栈的“先进先出”的特点)。但是删除列表的第一个元素 (抑或是在第一个元素之前添加一个元素)之类的操作是很耗时的,因为这些操作会牵扯到移动列表里的所有元素

heapq

Python中使用列表就可实现堆,主要的用法如下:

  • heappop,heappush:入堆\出堆
  • heapify:将列表转为堆
  • nlargest, nsmallest:获取top n的堆中的元素
from heapq import heappop, heappush, heapify, nlargest, nsmallest

heap_list = list()
arr = [-5, 51, 2, 1, 15, 5, 6, 7]
for x in arr:
    heappush(heap_list, x)  # 入堆 

# 获取最大\最小n的元素
print(nsmallest(3, heap_list))
print(nlargest(3, heap_list))

heapify(arr)  # 将arr列表heap化
print(arr==heap_list)

for i in range(len(heap_list)):
    print(heappop(heap_list))  # 出堆

Queue TODO

队列

序列高级部分

可迭代对象

可迭代对象的定义

在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义。例如, Python 的序列协议只需要 __len____getitem__ 两个方法。

import re

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __getitem__(self, item):
        return self.words[item]

    def __iter__(self):
        for word in self.words:
            yield word


sen = Sentence("abc def 123 456")
for i in sen:
    print(i)

我们发现 __iter____getitem__中实现任意一个,对象就是可迭代的,怎么解释这种情况呢,Python 解释器中有这样的描述:

解释器需要迭代对象 x 时, 会自动调用 iter(x)
内置的 iter 函数有以下作用:

  1. 优先检查对象是否实现了 __iter__ 方法, 如果实现了就调用它, 获取一个迭代器。
  2. 如果没有实现 __iter__ 方法, 但是实现了 __getitem__ 方法,Python 会创建一个迭代器, 尝试按顺序(从索引 0 开始) 获取元素。之所以对 __getitem__ 方法做特殊处理,是为了向后兼容,而未来可能不会再这么做。
  3. 如果尝试失败, Python 抛出 TypeError 异常, 通常会提示“C object is not iterable”(C 对象不可迭代)

因此我们自定义可迭代对象需要实现 __iter__ 函数;严格来讲,我们还需要实现 __getitem__ 函数,这个函数的主要作用是使当前对象可通过下标取值。

判断是否可迭代

什么是可迭代对象:使用 iter 内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的 __iter__ 方法,那么对象就是可迭代的。

如果有一个未知的对象,我们怎么判断该对象是否可迭代呢,两种方式(推荐第一种):

  • iter(x)
    这是最准确的判断方式,传入要判断的对象,只要对象实现了__getitem____iter__中的其中一个,该对象都是可迭代的,否则抛出TypeError: ‘C’ object is not iterable 异常;这儿有个区别(有待考证),如果仅仅实现了 __getitem__ ,调用 iter(x) 返回的是迭代器对象,如果实现的是 __iter__ ,那么调用iter(x)返回的是生成器对象;
  • isinstance(x, abc.Iterable)
    该方式会自动忽略__getitem__函数,仅当对象实现了 __iter__ 函数才返回 True,其他一律返回 False

从 Python 3.4 开始,检查对象 x 能否迭代,最准确的方法是:调用 iter(x) 函数,如果不可迭代,再处理 TypeError 异常。这比使用 isinstance(x, abc.Iterable) 更准确,因为 iter(x) 函数会考虑到遗留的 __getitem__ 方法, 而 abc.Iterable 类则不考虑。

https://www.jianshu.com/p/048d40d86759

迭代器

迭代器的定义

迭代器是这样的对象:实现了无参数的 __next__ 方法,返回序列中的下一个元素; 如果没有元素了,那么抛出 StopIteration 异常。Python 中的迭代器还实现了 __iter__ 方法,因此迭代器也可以迭代。

>>> a='abc'
>>> it=iter(a)
>>> it
<str_iterator object at 0x1042e4940>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> list(it)
[]
>>> list(iter(a))
['a', 'b', 'c']

注意事项:

  • 没有办法检查是否还有遗留的元素
  • 如果想再次迭代,那就要调用 iter(…),传入之前构建迭代器的可迭代对象

判断是否为迭代器

检查对象 x 是否为迭代器最好的方式是调用 isinstance(x, abc.Iterator)。得益于 Iterator.__subclasshook__ 方法,即使对象 x 所属的类不是 Iterator 类的真实子类或虚拟子类,也能这样检查。

在 Python 3.4 中,Lib/types.py,

  # Iterators in Python aren't a matter of type but of protocol. A large 
  # and changing number of builtin types implement *some* flavor of 
  # iterator. Don't check the type! Use hasattr to check for both 
  # "__iter__" and "__next__" attributes instead.

这就是 abc.Iterator 抽象基类中 subclasshook 方法的作用

  class Iterator(Iterable):
   __slots__ = ()

   @abstractmethod
   def __next__(self):
       'Return the next item from the iterator. When exhausted, raise StopIteration'
       raise StopIteration

   def __iter__(self):
       return self

   @classmethod
   def __subclasshook__(cls, C):
       if cls is Iterator:

           if (any("__next__" in B.__dict__ for B in C.__mro__) and
                   any("__iter__" in B.__dict__ for B in C.__mro__)):
               return True
       return NotImplemented

自定义迭代器

既是可迭代对象又是迭代器(bad idea)

构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。

除了__iter__ 方法之外,你可能还想在某个类(可迭代对象)中实现 __next__ 方法,让实例既是可迭代的对象,也是自身的迭代器。可是,这种想法非常糟糕。

迭代器可以迭代,但是可迭代的对象不是迭代器

可迭代的对象一定不能是自身的迭代器。

也就是说,可迭代的对象必须实现 __iter__ 方法,但不能实现 __next__ 方法。 迭代器应该一直可以迭代。迭代器的 __iter__ 方法应该返回自身。

迭代器模式可用来:

  • 访问一个聚合对象的内容而无需暴露它的内部表示
  • 支持对聚合对象的多种遍历
  • 为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)

为了“支持多种遍历”,必须能从同一个可迭代的实例中获取多个独立的迭代器,而且各个迭代器要能维护自身的内部状态,因此这一模式正确的实现方式是,每次调用 iter(my_iterable) 都新建一个独立的迭代器。这就是为什么这个示例需要定义SentenceIterator 类。

# 正确做法

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        return SentenceIterator(self.words)

class SentenceIterator:
    def __init__(self, words):
        self.words = words
        self.index = 0

    def __next__(self):
        try:
            word = self.words[self.index]
        except IndexError:
            raise StopIteration()
        self.index += 1
        return word

    def __iter__(self):
        return self
python 风格的迭代器

符合 Python 习惯的方式是,用生成器函数代替 SentenceIterator 类。

惰性实现

惰性实现是指尽可能延后生成值。这样做能节省内存,而且或许还可以避免做无用的处理。因此修改如下:

惰性实现:生成器函数1

import re
import reprlib

RE_WORD = re.compile('\w+')
class Sentence:
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    def __iter__(self):
        for word in self.words:
            yield word
        return

生成器函数定义体中的 return 语句会触发生成器对象抛出 StopIteration 异常。

惰性实现:生成器函数2

re.finditer 函数是 re.findall 函数的惰性版本,返回的不是列表,而是一个生成器,按需生成 re.MatchObject 实例

import re
import reprlib

RE_WORD = re.compile('\w+')

class Sentence:
    def __init__(self, text):
        self.text = text
        # 取消了 self.words

    def __repr__(self):
        return 'Sentence(%s)' % reprlib.repr(self.text)

    # 生成器函数2
    def __iter__(self):
        for match in RE_WORD.finditer(self.text):
            yield match.group()

惰性实现:生成器表达式

生成器表达式会产出生成器,因此可以使用生成器表达式进一步减少 Sentence 类的代码

生成器表达式是语法糖:完全可以替换成生成器函数,不过有时使用生成器表达式更便利

# 生成器表达式
    def __iter__(self):
        return (match.group() for match in RE_WORD.finditer(self.text))

如果函数或构造方法只有一个参数,传入生成器表达式时只写一对括号就行了。不用写一对调用函数的括号,再写一对括号围住生成器表达式。

def func(var):
    pass

func(n for n in range(10)) # (n for n in range(10))是生成器表达式,传入单参数的函数时不用外层的括号

iter 函数鲜为人知的用法

iter(calllable, stop_flag)
  1. 第一个参数必须是可调用的对象,用于不断调用(没有参数),产出各个值
  2. 第二个参数是个标记值(哨符),当可调用的对象返回这个值时,触发迭代器抛出 StopIteration 异常,而不产出哨符

内置函数iter的文档中有个实用的例子。这段代码逐行读取文件,直到遇到空行或者到达文件末尾为止

with open('mydata.txt') as fp:
    for line in iter(fp.readline, ''):
        process_line(line)

可迭代对象与迭代器的区别

可迭代的对象和迭代器之间的关系:Python 从可迭代的对象中获取迭代器,并且迭代器本身也是可迭代对象,但是可迭代的对象不是迭代器

标准的迭代器接口有两个方法:

  1. __next__:返回下一个可用的元素,如果没有元素了,抛出 StopIteration 异常。
  2. __iter__:返回 self,以便在需要使用可迭代对象的地方可以用迭代器代替,例如在 for 循环中。

image-20190410153352063

迭代器在 collections.abc.Iterator 抽象基类中制定。这个类定义了 __next__ 抽象方法,而且继承自 Iterable 类

for 循环的原理

当进行 for 循环时:

  1. 会调用 iter(iterable)__getitem____iter__中,获取迭代器iterator
  2. 然后使用 next(iterator),执行迭代器iterator__next__()函数,获取元素

因此:

  • 可迭代对象至少实现__getitem____iter__中的1个。
  • 迭代器必须实现__next__函数

代码演示

s = [1,2,3]

for n in s: 
    print(n)
#1
#2
#3

等价于

s = [1,2,3]

it = iter(s)
while True:
    try:
        print(next(it))
    except StopIteration:
        del it
        break

为了更好的理解迭代器的内部结构,我们先来定义一个生成斐波拉契数的迭代器:

# fib 既是可迭代对象,又是迭代器
class fib:
    def __init__(self):
       self.prev = 0
       self.curr = 1

    def __iter__(self):
       return self

    def __next__(self):
       value = self.curr
       self.curr += self.prev
       self.prev = value
       return value

from itertools import islice
f = fib()
print(list(islice(f, 0, 10)))

注意这个类既是可迭代的 (因为具有__iter__()方法),也是它自身的迭代器(因为具有__next__()方法)。

迭代器内部状态保存在当前实例对象的prev以及cur属性中,在下一次调用中将使用这两个属性。每次调用next()方法都会执行以下两步操作:

  1. 修改状态,以便下次调用next()方法
  2. 计算当前调用的结果

https://www.jianshu.com/p/4c8e4fb4ef37#

切片

切片的真相

iterable[index]会调用 __getitem__,index 会转为 slice 对象,TOOD getitem 详细

class MySeq:
    def __getitem__(self, index):
        return index

s = MySeq()

s[1] # 1
s[1:4] # slice(1, 4, None)
s[1:4:2] # slice(1, 4, 2)
s[1:4:2, 9] # (slice(1, 4, 2), 9)
s[1:4:2, 7:9] # (slice(1, 4, 2), slice(7, 9, None)) 元组中甚至可以有多个切片对象
  1. slice 是内置的类型(2.4.2 节首次出现)。
  2. 通过审查 slice,发现它有 start、stop 和 step 数据属性,以及 indices 方法。TODO indices 方法内容

indices用于优雅地处理缺失索引和负数索引,以及长度超过目标序列的切片,该方法会“整顿”元组,把 start、stop 和 stride 都变成非负数,而且都落在指定长度序列的边界内

>>> slice(None, 10, 2).indices(5)
# ➊ (0, 5, 2) 'ABCDE'[:10:2] 等同于 'ABCDE'[0:5:2]

>>> slice(-3, None, None).indices(5) 
# ➋ (2, 5, 1) 'ABCDE'[-3:] 等同于 'ABCDE'[2:5:1]

能正确处理切片的 getitem 方法

import numbers
class Vector():
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)

    def __getitem__(self, index):
        cls = type(self) # 获取类的类型,用于类型转换
        if isinstance(index, slice):
            return cls(self._components[index])#转为同类型对象
        elif isinstance(index, numbers.Integral):
            return self._components[index]     #返回单个元素
        else:
            msg = '{cls.__name__} indices must be integers'
            raise TypeError(msg.format(cls=cls))

生成器

生成器其实就是一种特殊的迭代器。它是一种更为高级、更为优雅的迭代器。

在Python中有两种类型的生成器:生成器函数以及生成器表达式

生成器表达式(括号)

简单的逻辑可以使用表达式

生成器表达式与列表解析式类似(一个是[],另一个是())。

### 创建 list
numbers = [1, 2, 3, 4, 5, 6]
l = [x * x for x in numbers]  
print(l)
# [1, 4, 9, 16, 25, 36]
### 创建生成器
numbers = [1, 2, 3, 4, 5, 6]
lazy_squares = (x * x for x in numbers) 
next(lazy_squares)
# 1
list(lazy_squares)
# [4, 9, 16, 25, 36]
# 注意我们第一次调用next()之后,lazy_squares对象的状态已经发生改变,所以后面后面地调用list()方法只会返回部分元素组成的列表。

注意我们第一次调用next()之后,lazy_squares对象的状态已经发生改变,所以后面地调用list()方法只会返回部分元素组成的列表。

生成器函数(yield)

复杂的逻辑使用函数

def fib():
    prev, curr = 0, 1
    while True:
        yield curr
        prev, curr = curr, prev + curr

f = fib() 
list(islice(f, 0, 10))

当调用f = fib()时,生成器被实例化并返回,这时并不会执行任何代码,生成器处于空闲状态

https://www.jianshu.com/p/4c8e4fb4ef37#

yield注意事项

yield 关键字只能把最近的外层函数变成生成器函数

调用f(),会得到一个无限循环,而不是生成器。

def f():
    def do_yield(n):
        yield n

    x = 0
    while True:
        x += 1
        gen = do_yield(x)
        print(gen)
                # <generator object f.<locals>.do_yield at 0x12c76fa98>

print(f())

可以使用yield from把工作委托给第三方完成。

def f():
    def do_yield(n):
        yield n
    x = 0
    while True:
        x += 1
        yield from do_yield(x)

yield from

yield from iterable

# 等价于
for i in iterable:
... yield i

会从迭代器中依次产生值出来,因此可以把这句话看做是一个生成器,yield from 不仅仅是语法糖而已,还会创建通道,把生成器当成协程使用时,这个通道特别重要,更多请参考协程部分TODO 协程

生成器函数库

主要是在 itertools 和 functools 模块 TODO

参考:流畅的 python14.9节

总结:关于可迭代对象、迭代器、生成器等之间的关系

以下几种对象之间的关系:

  • 容器(container)
  • 可迭代对象(Iterable)
  • 迭代器(Iterator)
  • 生成器(generator)
  • 生成器表达式
  • {list, set, dict} 解析式

相关的内建模块itertools,该模块提供的全部是处理迭代功能的函数,它们的返回值不是list,而是迭代对象,只有用for循环迭代的时候才真正计算。

字典

字典推导式

{} 包起来的就是字典推导式,[]包起来的是列表推导式,()包起来的是生成器

from pprint import pprint

DIAL_CODES = [
    (86, 'China'),
    (91, 'India'),
    (1, 'United States'),
    (62, 'Indonesia'),
    (55, 'Brazil'),
    (92, 'Pakistan'),
    (880, 'Bangladesh'),
    (234, 'Nigeria'),
    (7, 'Russia'),
    (81, 'Japan'),
]

country_code = {country: code for code, country in DIAL_CODES}
pprint(country_code)

"""
{'Bangladesh': 880,
 'Brazil': 55,
 'China': 86,
 'India': 91,
 'Indonesia': 62,
 'Japan': 81,
 'Nigeria': 234,
 'Pakistan': 92,
 'Russia': 7,
 'United States': 1}
"""

让字典提供默认值

get

d.get(k, [default])

返回键 k 对应的值,如果字典里没有键 k,则返回 None 或者 default

setdefault

d.setdefault(k, [default])

若字典⾥有键 k,则返回对应的值v; 若⽆则让d[k]=default,然后返回default
如果v是引用类型的话,可以进行如下操作:

my_dict.setdefault(key, []).append(new_value)

跟这样写:

if key not in my_dict:
    my_dict[key] = [] 
my_dict[key].append(new_value)

二者的效果是一样的,只不过后者至少要进行两次键查询——如果键不存在的话,就是三次,用 setdefault 只需要一次就可以完成整个操作。

使用带默认值的字典 defaultdict

在单纯地查找取值(而不是通过查找来插入新值)的时候,该怎么处理找不到的键呢?这就需要 defaultdict 来帮忙

d = defaultdict(default_factory)
# default_factory 必须是一个函数,可以返回默认值
  1. 默认值由 list 提供空列表[]
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
d = defaultdict(list)
for k, v in s:
    d[k].append(v)
sorted(d.items())
# [('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
  1. 默认值由 int 提供 0
s = 'mississippi'
d = defaultdict(int)
for k in s:
    d[k] += 1
print(sorted(d.items()))
# [('i', 4), ('m', 1), ('p', 2), ('s', 4)]
  1. 默认值由 lambda 函数提供
def constant_factory(value):
    return lambda: value
d = defaultdict(constant_factory('<missing>'))
d.update(name='John', action='ran')
print('%(name)s %(action)s to %(object)s' % d)
# 'John ran to <missing>'

总结

  • 这种技术比起等价的操作dict.setdefault()来得更加简单快捷
  • defaultdict 里的 default_factory 只会在 __getitem__ (TODO 这个方法)里被调用,在其他的方法里完全不会发挥作用。比如,dd 是个 defaultdict,k 是个找不到的键,dd[k] 这个表达式会调用 default_factory 创造某个默认值,而 dd.get(k)则会返回 None。

特殊方法__miss__

基类 dict 并没有定义这个方法,但是 dict 是知道有这么个东西存在的。也就是说,如果有一个类继承了 dict,然后这个继承类提供了 __missing__ 方法,那么在 __getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个 KeyError 异常。

注意:__missing__ 方法只会被 __getitem__ 调用

例子:数字1与字符串数字'1'映射到同一个值v

class StrKeyDict0(dict):

    def __missing__(self, key):
        if isinstance(key, str):  # 如果键本身就是字符串,那就抛出 KeyError 异常。
            raise KeyError(key)
        return self[str(key)]    # 如果找不到的键不是字符串,那么把它转换成字符串再进行查找。

    def get(self, key, default=None):
        try:
            return self[key]    # get 方法把查找工作用 self[key] 的形式委托给 __getitem__,这样在宣布查找失败之前,还能通过 __missing__ 再给某个键一个机会。
        except KeyError:
            return default

    def __contains__(self, key):
        return key in self.keys() or str(key) in self.keys()

视图

dict.keys()dict.values()dict.items()返回得到的对象,都是视图对象,视图提供了字典条目的动态视图,意味着当字典被改动时,视图也跟着变化,反映出字典的变化

Keys views are set-like since their entries are unique and hashable. If all values are hashable, so that (key, value) pairs are unique and hashable, then the items view is also set-like. (Values views are not treated as set-like since the entries are generally not unique.) For set-like views, all of the operations defined for the abstract base class collections.abc.Set are available (for example, ==, <, or ^).

dishes = {'eggs': 2, 'sausage': 1, 'bacon': 1, 'spam': 500}
keys = dishes.keys()
values = dishes.values()

# 迭代
n = 0
for val in values:
    n += val
print(n)
# 504

# keys、values 以一样的顺序进行迭代输出(insertion order)
list(keys)
# ['eggs', 'sausage', 'bacon', 'spam']
list(values)
# [2, 1, 1, 500]

# view 对象能够动态反映 dict 的改变
del dishes['eggs']
del dishes['sausage']
list(keys)
# ['bacon', 'spam']

# 集合操作
print(keys & {'eggs', 'bacon', 'salad'})
# {'bacon'}
print (keys ^ {'sausage', 'juice'})
# {'juice', 'sausage', 'bacon', 'spam'}

集合

  • 集合中的元素必须是可散列的,set 类型本身是不可散列的,但是 frozenset 可以。
  • 如果是空集,那么必须写成 set()的形式。如果写成{} 的形式,你创建的其实是个空字典。

文本和字节序列

从 Python 3 的 str 对象中获取的元素是 Unicode 字符,这相当于从 Python 2 的 unicode 对象中获取的元素

常见字符集

  • latin1(即 iso8859_1):一种重要的编码,是其他编码的基础,例如 cp1252 和 Unicode(注意,latin1 与 cp1252 的字节值是一样的,甚至连码位也相同)。
  • cp1252 :Microsoft 制定的 latin1 超集,添加了有用的符号,例如弯引号和€(欧元);有些 Windows 应用把它称为“ANSI”,但它并不是 ANSI 标准。
  • cp437:IBM PC 最初的字符集,包含框图符号。与后来出现的 latin1 不兼容。
  • gb2312 :用于编码简体中文的陈旧标准;这是亚洲语言中使用较广泛的多字节编码之一。
  • utf-8:目前 Web 中最常见的 8 位编码; 与 ASCII 兼容(纯 ASCII 文本是有效的 UTF-8 文 本)。
  • utf-16le:UTF-16 的 16 位编码方案的一种形式;所有 UTF-16 支持通过转义序列(称为“代理 对”,surrogate pair)表示超过 U+FFFF 的码位。

编码的二进制显示

二进制序列其实是整数序列,各个字节的值可能会使用下列三种不同的方式显示:

  1. 可打印的 ASCII 范围内的字节(从空格到 ~),使用 ASCII 字符本身
  2. 制表符、换行符、回车符和 \对应的字节,使用转义序列 \t\n\r\\
  3. 其他字节的值,使用十六进制转义序列(例如,\x00 是空字节)

编码与解码

编码:把码位转换成字节序列的过程

把文本转换成字节序列 时,如果目标编码中没有定义某个字符,那就会抛出 UnicodeEncodeError 异常

  1. 编码时指定 error='ignore' 处理方式悄无声息地跳过无法编码的字符;这样做通常很是不妥。
  2. error='replace',把无法编码的字符替换成 ?;数据损坏了,但是用户知道出了问题。
  3. error='xmlcharrefreplace' 把无法编码的字符替换成 XML 实体。
city = 'São Paulo'
city.encode('cp437', errors='ignore')
city.encode('cp437', errors='replace')
city.encode('cp437', errors='xmlcharrefreplace')

解码:把字节序列转换成码位的过程

不是每一个字节都包含有效的 ASCII 字符,也不是每一个字符序列都是有效的 UTF-8 或 UTF-16。因此,把二进制序列转换成文本时,遇到无法转换的字节序列时会抛出 UnicodeDecodeError

octets = b'Montr\xe9al'

octets.decode('cp1252') 
# 'Montréal'
octets.decode('iso8859_7') 
# 'Montrιal'
octets.decode('koi8_r')
# 'MontrИal'

octets.decode('utf_8') 
Traceback (most recent call last):
    File "<stdin>", line 1, in <module> UnicodeDecodeError: 
    'utf-8' codec can't decode byte 0xe9 in position 5: invalid continuation byte

octets.decode('utf_8', errors='replace')
# 'Montr�al'

使用 ‘replace’ 错误处理方式,\xe9 替换成了“� ”(码位是 U+FFFD),这是官方指定的 REPLACEMENT CHARACTER(替换字符),表示未知字符。

SyntaxError

Python 3 默认使用 UTF-8 编码源码,Python 2(从 2.5 开始)则默认使用 ASCII。

如果加载的 .py 模块中包含 UTF-8 之外的数据,而且没有声明编码,会得到类似下面的消息

SyntaxError: Non-UTF-8 code starting with '\xe1' in file ola.py on line

为了修正这个问题,可以在文件顶部添加一个神奇的 coding 注释

#  -*- coding: UTF-8 -*-
print('可以打印中文了。')

# coding: cp1252
print('Olá, Mundo!')

如何知道字节序列的编码

一般情况是不能。必须有人告诉你。有些通信协议和文件格式,如 HTTP 和 XML,包含明确指明内容编码的首部。

但是如果假定字节流是人类可读的纯文本,就可以通过试探和分析找出编码。

例如,如果 b’\x00’ 字节经常出现,那么可能是 16 位或 32 位编码,而不是 8 位编码方案,因为纯文本中不能包含空字符;如果字节序列 b’\x20\x00’ 经常出现,那么可能是 UTF-16LE 编码中的空格字符(U+0020),而不是 鲜为人知的 U+2000 EN QUAD 字符——谁知道这是什么呢!

统一字符编码侦测包 Chardet就是这样工作的,它能识别所支持的 30 种编码。Chardet是一个 Python 库,可以在程序中使用,也提供了命令行工具 chardetect。

BOM(byte-order mark)

字节序只影响一个字(word)占多个字节的编码(如 UTF-16 和 UTF-32),以及像 long这种多字节的数据类型,也受字节序影响。

在UCS 编码中有一个叫做”ZERO WIDTH NO-BREAK SPACE“的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。
UCS规范建议我们在传输字节流前,先传输 字符”ZERO WIDTH NO-BREAK SPACE“。
如果接收者收到FEFF,就表明这个字节流是大字节序的;如果收到FFFE,就表明这个字节流是小字节序的。因此字符”ZERO WIDTH NO-BREAK SPACE“又被称作BOM。

u16 = 'El Niño'.encode('utf_16')
print(u16)
b'\xff\xfeE\x00l\x00 \x00N\x00i\x00\xf1\x00o\x00'

在小字节序系统中,这个字符编码为b'\xff\xfe'(十进制数 255, 254)。

在大字节序系统中,这个字符编码为b'\xfe\xff'(十进制数 254, 255)。

所以编解码器知道该用哪个字节序。

UTF-16 有两个变种:

  • UTF-16LE,显式指明使用小字节序;
  • UTF-16BE,显式指明使用大 字节序。

如果使用这两个变种,不会生成 BOM:

u16le = 'El Niño'.encode('utf_16le')
list(u16le) 
# [69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111, 0]
u16be = 'El Niño'.encode('utf_16be')
list(u16be) 
# [0, 69, 0, 108, 0, 32, 0, 78, 0, 105, 0, 241, 0, 111]

Intel x86 架构用的是小字节序,因此有很多文件用的是不带 BOM 的小字节序 UTF-16 编码。

UTF-8 的一大优势是,不管设备使用哪种字节序,生成的字节序列始终一致,因此不需要 BOM

UTF-8以字节为编码单元,根据当前字节中开头几个1,就可以知道后面还有几个字节是共同组成一个字的。

比如下面的第2行,读取第1个字节后发现有2个1开头,就可以知道后面还有1个字节,共同组成一个字

0xxxxxxx //文字符号0~127(ASCII)
110xxxxx 10xxxxxx //128~2047
1110xxxx 10xxxxxx 10xxxxxx //2048~65535
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx //65535~0x10ffff

尽管如此,某些 Windows 应用(尤其是 Notepad)依然会在 UTF-8 编码的 文件中添加 BOM;

UTF-8 编码的 U+FEFF 字符是一个三字节序列:b'\xef\xbb\xbf'。因此,如果文件以这三个字节开头,有可能是带有 BOM 的 UTF-8 文件。

默认编码(最好不要使用)

import sys, locale

expressions = """
    locale.getpreferredencoding() 
    type(my_file) 
    my_file.encoding sys.stdout.isatty() 
    sys.stdout.encoding 
    sys.stdin.isatty() 
    sys.stdin.encoding 
    sys.stderr.isatty() 
    sys.stderr.encoding 
    sys.getdefaultencoding()
    sys.getfilesystemencoding()
    """

my_file = open('dummy', 'w')

for expression in expressions.split():
    value = eval(expression)
    print(expression.rjust(30), '->', repr(value))

locale.getpreferredencoding() 返回的编码是最重要的:这是打开文件的默认编码,也是重定向到文件的 sys.stdout/stdin/stderr 的默认编码。

因此,关于编码默认值的最佳建议是:别依赖默认值

规范化 Unicode 字符串

Unicode 有组合字符(变音符号附加到前一个字符上的记号,打印时作为一个整体),所以字符串比较起来很复杂。

“café”这个词可以使用两种方式构成,分别有 4 个和 5 个码位,但是结果完全一样

s1 = 'café'
s2 = 'cafe\u0301'
print(s1, s2)
# ('café', 'café')
print(len(s1), len(s2))
# (4, 5)
s1 == s2 
# False

在 Unicode 标准中,ée\u0301 这样的序列叫“标准等价物”(canonical equivalent),应用程序应该把它们视作相同的字符。但是,Python 看到的是不同的码位序列,因此判定二者不相等。

解决方案是使用 unicodedata.normalize 函数提供的 Unicode 规范化。这个 函数的第一个参数可以是:’NFC’、’NFD’、’NFKC’ 和 ‘NFKD’。

NFC与NFD

  • NFC(Normalization Form C)使用最少的码位构成等价的字符串,
  • NFD 把组合字符分解成基字符和单独的组合字符。
from unicodedata import normalize

s1 = 'café'             # 把"e"和重音符组合在一起
s2 = 'cafe\u0301' # 分解成"e"和重音符

len(s1), len(s2) 
# (4, 5)
len(normalize('NFC', s1)), len(normalize('NFC', s2)) 
# (4, 4)
len(normalize('NFD', s1)), len(normalize('NFD', s2)) 
# (5, 5)
normalize('NFC', s1) == normalize('NFC', s2) 
# True
normalize('NFD', s1) == normalize('NFD', s2) 
# True

NFKC 与 NFKD

字母 K 表 示“compatibility”(兼容性)

虽然 Unicode 的目标是为各个字符提供“规范的”码位,但是为了兼容现有的标准,有些字符会出现多次

虽然希腊字母表中有“μ”这个字母(码位是 U+03BC,GREEK SMALL LETTER MU),但是 Unicode 还是加入了微符号 ‘µ’(U+00B5),以便与 latin1 相互转 换。因此,微符号是一个“兼容字符”。

兼容字符:个人理解就是比较复杂的字符,出现在其他字符集中的字符,为了兼容其他字符集而出现在 unicode 中,因此称作兼容字符,这些字符可以被替换成一个或者多个字符。

在 NFKC 和 NFKD 形式中,各个兼容字符会被替换成一个或多个“兼容分解”字符,即便这样有些格式损失,但仍是“首选”表述。如下:

  1. 二分之一 ‘½’(U+00BD)经过兼容分解后得到的是 三个字符序列 ‘1/2’;

  2. 微符号 ‘µ’(U+00B5)经过兼容分解后得到的是小写字母 ‘μ’(U+03BC)。

大小写折叠(TODO)

流畅的 python 一书中

# Utility functions for normalized Unicode string comparison.

from unicodedata import normalize

def nfc_equal(str1, str2):
    return normalize('NFC', str1) == normalize('NFC', str2)

def fold_equal(str1, str2):
    return (normalize('NFC', str1).casefold() == normalize('NFC', str2).casefold())


# Using Normal Form C, case sensitive:

s1 = 'café'
s2 = 'cafe\u0301'

print(s1 == s2)             # False
print(nfc_equal(s1, s2))    # True
print(nfc_equal('A', 'a'))  # False

# Using Normal Form C with case folding:

s3 = 'Straße'
s4 = 'strasse'

print(s3 == s4)             # False
print(nfc_equal(s3, s4))    # False
print(fold_equal(s3, s4))   # True
print(fold_equal(s1, s2))   # True
print(fold_equal('A', 'a')) # True

去掉变音符号

维基百科中介绍圣保罗市São Paulo——> Sao_Paulo ,这就是去除变音符号。

版本1(过滤掉所有组合记号):

import unicodedata 
import string

def shave_marks(txt):

"""去掉全部变音符号""" 
    # 把所有字符分解成基字符和组合记号。
    norm_txt = unicodedata.normalize('NFD', txt)
    # 过滤掉所有组合记号
    shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c)) 
    # 重组所有字符
    return unicodedata.normalize('NFC', shaved)

版本2(只去除拉丁文字的变音符号):

def shave_marks_latin(txt):
    """把拉丁基字符中所有的变音符号删除"""
    norm_txt = unicodedata.normalize('NFD', txt)
    is_latin_base = False
    keepers = []
    for c in norm_txt:
        # 忽略拉丁基字符上的变音符号
        if unicodedata.combining(c) and is_latin_base:
            continue
        keepers.append(c)

        # 如果不是组合字符,那就是新的基字符,判断是否为拉丁字符
        if not unicodedata.combining(c):
            is_latin_base = c in string.ascii_letters

    shaved = ''.join(keepers)
    return unicodedata.normalize('NFC', shaved)

总结

对大多数应 用来说,NFC 是最好的规范化形式。不区分大小写的比较应该使用 str.casefold()

文本排序

(比较复杂,而且不一定成功,只适合 Unix)

在Python中,非ASCII文本的标准排序方式是使用locale.strxfrm函数,根据locale模块的文档,这个函数会“把字符串转换成适合所在区域进行比较的形式”。

使用 locale.strxfrm 函数之前,必须先为应用设定合适的区域设置:

import locale

locale.setlocale(locale.LC_COLLATE, 'pt_BR.UTF-8')
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted(fruits, key=locale.strxfrm)
# ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

PyUCA 库(推荐)

import pyuca

coll = pyuca.Collator()
fruits = ['caju', 'atemoia', 'cajá', 'açaí', 'acerola']
sorted_fruits = sorted(fruits, key=coll.sort_key)
print(sorted_fruits)
# ['açaí', 'acerola', 'atemoia', 'cajá', 'caju']

PyUCA 没有考虑区域设置。如果想定制排序方式,可以把自定义的排序表路径传给 Collator() 构造方法。PyUCA 默认使用项目自带的 allkeys.txt(https://github.com/jtauber/pyuca),这就是 Unicode 6.3.0 的“Default Unicode Collation Element Table”的副本。

Unicode数据库

Unicode标准提供了一个完整的数据库(许多格式化的文本文件),不仅包括码位与字符名称之间的映射,还有各个字符的元数据,以及字符之间的关系。例如,Unicode数据库记录了字符是否可以打印、是不是字母、是不是数字,或者是不是其他数值符号。字符串的isidentifier、isprintable、isdecimal和isnumeric等方法就是靠这些信息作判断的。

import unicodedata
import re

re_digit = re.compile(r'\d')

sample = '1\xbc\xb2\u0969\u136b\u216b\u2466\u2480\u3285'

for char in sample:
    print('U+%04x' % ord(char),char.center(6),
            're_dig' if re_digit.match(char) else '-',
            'isdig' if char.isdigit() else '-',
            'isnum' if char.isnumeric() else '-',
            format(unicodedata.numeric(char), '5.2f'),
            unicodedata.name(char),
            sep='\t')

unicodedata模块更多函数的说明(https://docs.python.org/3/library/unicodedata.html

支持字符串和字节序列的双模式API

正则表达式中的双模式

如果使用字节序列构建正则表达式,\d和\w等模式只能匹配ASCII字符

如果是字符串模式,就能匹配ASCII之外的Unicode数字或字母。

import re

re_numbers_str = re.compile(r'\d+')
re_words_str = re.compile(r'\w+')
re_numbers_bytes = re.compile(rb'\d+')
re_words_bytes = re.compile(rb'\w+')

text_str = ("Ramanujan saw \u0be7\u0bed\u0be8\u0bef" " as 1729 = 1³ + 12³ = 9³ + 10³.")
text_bytes = text_str.encode('utf_8')

print('Text', repr(text_str), sep='\n ')
print('Numbers')
print(' str :', re_numbers_str.findall(text_str))
print(' bytes:', re_numbers_bytes.findall(text_bytes))
print('Words')
print(' str :', re_words_str.findall(text_str))
print(' bytes:', re_words_bytes.findall(text_bytes))

# Text
#  'Ramanujan saw ௧௭௨௯ as 1729 = 1³ + 12³ = 9³ + 10³.'
# Numbers
#  str : ['௧௭௨௯', '1729', '1', '12', '9', '10']
#  bytes: [b'1729', b'1', b'12', b'9', b'10']
# Words
#  str : ['Ramanujan', 'saw', '௧௭௨௯', 'as', '1729', '1³', '12³', '9³', '10³']
#  bytes: [b'Ramanujan', b'saw', b'as', b'1729', b'1', b'12', b'9', b'10']

os 中的双模式(TODO)

不是很明白

GNU/Linux内核不理解Unicode,因此你可能发现了,对任何合理的编码方案来说,在文件名中使用字节序列都是无效的,无法解码成字符串。

os模块中的所有函数、文件名或路径名参数既能使用字符串,也能使用字节序列。如果这样的函数使用字符串参数调用,该参数会使用sys.getfilesystemencoding()得到的编解码器自动编码,然后操作系统会使用相同的编解码器解码。这几乎就是我们想要的行为,与Unicode三明治最佳实践一致。

函数

编程语言理论家把“一等对象”定义为满足下述条件的程序实体:

  • 在运行时创建
  • 能赋值给变量或数据结构中的元素
  • 能作为参数传给函数
  • 能作为函数的返回结果

在 Python 中,整数、字符串和字典都是一等对象——没什么特别的。有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数。Python 不支持重载方法或函数。

术语

非绑定方法(unbound method):直接通过类访问的实例方法没有绑定到特定的实例上,因此把这种方法称为“非绑定 方法”。若想成功调用非绑定方法,必须显式传入类的实例作为第一个参数。那个实例会 赋值给方法的 self 参数。

形参与实参

形参(parameter):声明函数时指定的零个或多个“形式参数”,这些是未绑定的局部变量。调用函数时, 传入的实参(“实际参数”)会绑定给这些变量。有5种:

  1. 位置或关键字参数(positional-or-keyword):def func(foo, bar=None): ...

  2. 任意数量的位置参数(var-positional):在名字前面加*的参数,如下面的args,除了已被其他参数(arg1,arg2)接受的任何位置参数,剩下的位置参数都提供给 args,如:def func(arg1, arg2, *args, **kwargs): ...

  3. 任意数量的关键字参数(var-keyword):在名字前面加**的参数,如上例的kwargs,除了被其他形参接收的实参,剩下的关键字参数都提供给 kwargs

  4. 仅位置参数(positional-only):只能通过位置提供,不能以关键词提供的参数,新版的python可以通过/来划分了。def func(posonly1, posonly2, /, positional_or_keyword): ...

  5. 仅关键字参数(keyword-only):只能以关键字形式提供的参数,在*arg这种任意数量的位置参数或 单单一个* 后面的形参,就是仅关键字参数,如下面的kw_only1,kw_only2def func(arg, *, kw_only1, kw_only2): ...

    注意:上例只接收3个参数,这个*不接收任何参数,这只是定义仅关键字参数的语法,调用形式:func(1, kw_only1=2, kw_only2=3)

综合例子:

def tag(name, *content, cls=None, **attrs):
    """生成一个或多个HTML标签"""
    if cls is not None:
        attrs['class'] = cls
    if attrs:
        attr_str = ''.join(' %s="%s"' % (attr, value) for attr, value in sorted(attrs.items()))
    else:
        attr_str = ''
    if content:
        return '\n'.join('<%s%s>%s</%s>' % (name, attr_str, c, name) for c in content)
    else:
        return '<%s%s />' % (name, attr_str)


print(tag('p', 'hello', 'world', id=33))
print(tag(name="img", content='testing',))
my_tag = {'name': 'img', 'title': 'Sunset Boulevard', 'src': 'sunset.jpg', 'cls': 'framed'}
print(tag(**my_tag))

# <p id="33">hello</p>  <p id="33">world</p>
# <img content="testing" />
# <img class="framed" src="sunset.jpg" title="Sunset Boulevard" />

在示例中,举例了各种形参,同时也给出了他们定义时的位置要求,顺序如下:

  1. name是位置或关键字参数。
  2. content是任意数量的位置参数,name参数后面的任意个参数会被 *content 捕获,存入一个元组。
  3. cls是仅关键字参数。
  4. attrs是任意数量的关键字参数,没有明确指定名称的关键字参数会被 **attrs 捕获,存入一个字典。

定义函数时若想指定仅限关键字参数,要把它们放到前面有 * 的参数后面。如果不想支持任意数量的位置参数,但是想支持仅限关键字参数,在签名中放一个*,如下所示:

def f(a, *, b): 
    return a, b

f(1, b=2) 
#(1, 2)

注意,仅限关键字参数不一定要有默认值,可以像下例中 b 那样,强制必须传入实参。

参考:parameter

实参(argument):调用函数(或方法)时传给函数(或方法)的值,有2种:

  1. 位置参数:出现在参数列表的前面位置 或者 通过*解压的可迭代对。
complex(3, 5)
complex(*(3, 5))
  1. 关键字参数:name=val或者通过**解压的字典
complex(real=3, imag=5)
complex(**{'real': 3, 'imag': 5})

参考:argument

高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数

列表推导或生成器表达式具有map和filter两个函数的功能,而且更易于阅读

list(map(fact, range(6))) 
# [1, 1, 2, 6, 24, 120]
[fact(n) for n in range(6)]  # 列表推导式代替 map
# [1, 1, 2, 6, 24, 120]

list(map(factorial, filter(lambda n: n % 2, range(6))))
# [1, 6, 120]
[factorial(n) for n in range(6) if n % 2]    # if 代替 filter,列表推导式代替 map
# [1, 6, 120]

reduce在 Python 2 中是内置函数,在Python3中放到functools模块里了。这个函数最常用于求和,自2003年发布的Python2.3开始,最好使用内置的sum函数

from functools import reduce
from operator import add 

reduce(add, range(100)) 
# 4950
sum(range(100))
# 4950

匿名函数

为了使用高阶函数,有时创建一次性的小型函数更便利。这便是匿名函数存在的原因。Python 简单的句法限制了 lambda 函数的主体只能是一个表达式,而不是一个代码块,不能赋值,也不能使用 while 和 try 等。

输入是传入到参数列表argument_list的值,输出是根据表达式expression计算得到的值。

  • lambda x, y: x*y;函数输入是x和y,输出是它们的积x*y
  • lambda:None;函数没有输入参数,输出是None

函数专有属性

重点说明函数专有而用户定义的一般对象没有的属性

class C: pass
obj = C() 
def func(): pass 
sorted(set(dir(func)) - set(dir(obj))) 
# ['__annotations__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']

获取函数参数的信息(不太明白TODO)

使用 inspect 模块

函数注解

def clip(text:str, max_len:'int > 0'=80) -> str:
    """在max_len前面或后面的第一个空格处截断文本 """
    pass
  • 各个参数可以在: 之后增加注解表达式。如果参数有默认值,注解放在参数名和 = 号之间。
  • 如果想注解返回值,在 )和函数声明末尾的:之间添加-> 和一个表达 式。

注解表达式可以是任何类型,注解中最常用的类型是类(如 str 或 int)和字符串 (如 ‘int > 0’)

Python 对注解所做的唯一的事情是,只是存储在函数的 __annotations__ 属性(字典)中。换句话说,注解对 Python 解释器没有任何意义。注解只是元数据,可以供 IDE、框架和装饰器等工具使用。

支持函数式编程的包

operator模块

  • _ 开头的名称,因为它们基本上是实现细节

  • i开头、后面是另一个运算符的那些名称(如 iadd、iand 等),对应的是增量赋值运算符(如 +=、&= 等)

一、operator 模块为多个算术运算符提供了对应的函数

from functools import reduce 
from operator import mul   # mul对应乘法函数

def fact(n):
    return reduce(mul, range(1, n+1))

二、operator 模块中还有一类函数,能替代从序列中取出元素或读取对象属性的 lambda 表达式

itemgetter 的常见用途:根据元组的某个字段给元组列表排序,如果把多个参数传给 itemgetter,它构建的函数会返回提取的值构成的元组

metro_data = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
]
from operator import itemgetter

for city in sorted(metro_data, key=itemgetter(1)):
    print(city)
cc_name = itemgetter(1, 0)

# 如果把多个参数传给 itemgetter,会返回提取的值构成的元组
for city in metro_data:
    print(cc_name(city))

attrgetter与itemgetter作用类似,它创建的函数根据名称提取对象的属性。如果把多个属性名传给attrgetter,它也会返回提取的值构成的元组。

此外,如果参数名中包含 .(点号),attrgetter 会深入嵌套对象,获取指定的属性。

metro_data = [
    ('Tokyo', 'JP', 36.933, (35.689722, 139.691667)),
    ('Delhi NCR', 'IN', 21.935, (28.613889, 77.208889)),
    ('Mexico City', 'MX', 20.142, (19.433333, -99.133333)),
    ('New York-Newark', 'US', 20.104, (40.808611, -74.020386)),
    ('Sao Paulo', 'BR', 19.649, (-23.547778, -46.635833))
]

from collections import namedtuple

LatLong = namedtuple('LatLong', 'lat long')
Metropolis = namedtuple('Metropolis', 'name cc pop coord')

metro_areas = [Metropolis(name, cc, pop, LatLong(lat, long)) for name, cc, pop, (lat, long) in metro_data ]


# .的例子
from operator import attrgetter
name_lat = attrgetter('name', 'coord.lat')

for city in sorted(metro_areas, key=attrgetter('coord.lat')):
    print(name_lat(city))
# ('Sao Paulo', -23.547778)
# ('Mexico City', 19.433333)
# ('Delhi NCR', 28.613889)
# ('Tokyo', 35.689722)
# ('New York-Newark', 40.808611)

functools

functools.partial 这个高阶函数用于部分应用一个函数。

部分应用:基于一个函数创建一个新的可调用对象,把原函数的某些参数固定。

partial 的第一个参数是一个可调用对象(callable),后面跟着任意个要绑定的定位参数和关键字参数

import unicodedata, functools
nfc = functools.partial(unicodedata.normalize, 'NFC')

s1 = 'café'
s2 = 'cafe\u0301'

print(  s1, s2)
#('café', 'café')

print(s1 == s2,nfc(s1) == nfc(s2) )
# False True

# partial 函数的一些属性
print(nfc)
# functools.partial(<built-in function normalize>, 'NFC')
print(nfc.func)
# <built-in function normalize>
print(nfc.args)
# ('NFC',)
print(nfc.keywords)
# {}

变量作用域

对于理解装饰器、闭包非常重要。

闭包(自由变量)

闭包 是指 延伸了作用域的函数,其中包含函数定义体中引用、但是不在函数定义体中定义 的非全局变量__closure__。函数是不是匿名的没有关系,关键是它能访问函数定义体之外定义的非全局变量

类对象编程

# 可调用对象方式
class Averager():
    def __init__(self):
        self.series = []

    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)

函数式编程

# 函数式编程
def make_averager():
    series = []
    def averager(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)

    return averager

这两个示例有共通之处:调用 Averager() 或 make_averager() 得到一个可调用 对象 avg,它会更新历史值,然后计算当前均值。

在 averager 函数中,series 是自由变量(free variable),指未在本地作用域中绑定的变量(包括外层函数的参数,比如 make_averager(arg),arg也算是)

image-20190402210650537

avg = make_averager
avg.__code__.co_varnames    # 局部变量
# ('new_value', 'total')

avg.__code__.co_freevars # 自由变量
# ('series',)

avg.__closure__ 
# (<cell at 0x107a44f78: list object at 0x107a91a48>,)

avg.__closure__[0].cell_contents 
# [10, 11, 12]

审查返回的 averager 对象,我们发现 Python 在__code__属性(表示编译后的函数定义 体)中保存局部变量自由变量的名称。

__closure__是每个被返回的函数的属性,如:avg,并非是make_averager的属性

avg.__closure__中的各个元素对应于avg.__code__.co_freevars中的一个名称。这些元素是 cell 对象,有个 cell_contents 属性,保存着真正的值。

# 遍历 闭包中的 自由变量
for cl in avg.__closure__:
    print(cl.cell_contents)

综上:闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时, 虽然定义作用域不可用了,但是仍能使用那些绑定。 注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

如果对自由变量赋值呢?特别是不可变类型,会发生什么问题?,请看 nolocal 的作用

nolocal声明

def make_averager():
    count = 0
    total = 0

    def averager(new_value):
        count += 1
        total += new_value
        return total / count

    return averager

avg = make_averager()
avg(10)
# Traceback (most recent call last):
# UnboundLocalError: local variable 'count' referenced before assignment

count += 1 语句的作用其实与count = count + 1一样。因此,我们在 averager 的定义体中为 count 赋值了,这会把自由变量 count 变成局部变量

闭包一节中没有这个问题,是利用了列表是可变的对象这一事实。但是对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑 定,例如 count = count + 1,其实会隐式创建局部变量 count。

为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。

Python 2 没有 nonlocal,因此需要变通方法,基本上,这种处理方式是把内部函数需要修改 的变量(如 count 和 total)存储为可变对象(如字典或简单的实例)的元素或属性,并且把那个对象绑定给一个自由变量。

装饰器

什么是装饰器

装饰器是一个返回值为函数inner的函数,其第一个参数是另一个函数func,二者接受相同的参数,装饰器的典型行为:把被装饰的函数func替换成新函数inner,使得在执行func前后执行一些额外的操作,类似于装饰func,因此称为装饰器。

函数装饰器 用于在源码中“标记”函数,以某种方式增强函数的行为。

# 定义装饰器函数,增强 func 的功能(日志功能)
def decorate(func):
    def inner(*args):
        print("begin %s" % func.__name__)
        func(*args)
        print("end %s" % func.__name__)
    return inner

然后强化 target 函数

def target():
    print('running target()')

target = decorate(target)

@语法糖,等价于

@decorate
def target():
    print('running target')
target()
  • @装饰器函数对象(函数名)@装饰器工厂函数(参数)@后面必须跟着装饰器函数对象,调用装饰器工厂函数也一样可以得到装饰器对象。
  • 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
  • 第二个特性是,装饰器 在加载模块时立即执行。而被装饰的函数只在明确调用时运行。

自定义装饰器的最佳实践

使用functools.wrap实现装饰器的完整定义。

# deco 是装饰器,第一个参数是“可调用对象”
def deco(func):
    def inner(num):
        '''内部函数,打印 num'''
        print('running inner(%s)' %num)
    return inner

# target 被作为参数传入到 deco 中,并且 target 变成了 inner
@deco
def target(n):
    print('running target()')

target(10)
# running inner()  其实变成了 inner 函数

print(target) # <function deco.<locals>.inner at 0x10063b598> 其实变成了 inner 函数
print(target.__doc__) # 内部函数,打印 num
print(target.__name__) # inner

实现的 target 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name____doc__属性。 使用 functools.wraps装饰器可以把相关的属性从 func 复制到inner 中。此外,这个新版还能正确处理关键字参数。

import functools
# functools的使用
def deco(func):
    @functools.wraps(func)
    def inner(num):
        '''内部函数,打印 num'''
        print('running inner(%s)' %num)
    return inner

装饰器在真实代码中的常用方式:

  1. 装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
  2. 大多数装饰器会在内部定义一个新的函数,然后将其返回。

叠加装饰器

@d1@d2 两个装饰器按顺序应用到 f 函数上,作用相当于f = d1(d2(f))

也就是说,下述代码:

@d1 
@d2 
def f():
    print('f')

等同于

def f():
    print('f')

f = d1(d2(f))

带参数的装饰器

Python 把被装饰的函数作为第一个参数传给装饰器函数。那怎么让装饰器接受其他参数呢?答案是:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。TODO 网上其他例子?

registry = set()

def register(active=True):# 装饰器工厂,利用参数定制【装饰器】
    def decorate(func):   # 真正的装饰器
        print('running register(active=%s)->decorate(%s)' % (active, func))
        if active:
            registry.add(func)
        else:
            registry.discard(func)
        return func

    return decorate      # 装饰器工厂 返回定制后的[装饰器]

@register(active=False)
def f1():
    print('running f1()')

# 即使不传入参数,register 也必须作为函数调用,返回真正的 装饰器 decorate。
@register()       
def f2():
    print('running f2()')

def f3():
    print('running f3()')

这里的关键是,register() 要返回 decorate,然后把它应用到被装饰的函数上。

因此如果不使用 @ 句法,那就要像常规函数那样使用 register;装饰 f 函数的句法是 register()(f);不想添加(或把它删除)的话,句法是 register(active=False)(f)

register()(f)=decorate(f),因为decorate是register()运行结果

标准库中的装饰器

Python 内置了三个用于装饰方法的函数:property、classmethod 和 staticmethod;

常见的装饰器是 functools.wraps,它的作用是协助构建行为良好的装饰器;

标准库中最值得关注的两个装饰器是 lru_cache 和全新的 singledispatch(Python 3.4 新增);

functools.lru_cache 实现缓存

functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。

typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。

lru_cache 使用字典存储结果,而且键根据调用时传入的定位 参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列 的。

它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。

import functools
import time

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
            pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
            arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
        return result
    return clocked

@functools.lru_cache()
@clock
def fibonacci(n):
    if n<2:
        return n
    return fibonacci(n-1)+fibonacci(n-2)

if __name__=='__main__':
    print(fibonacci(6))

functools.singledispatch 实现函数/方法重载

是 Python 3.4 增加的,PyPI 中的 singledispatch 包(https://pypi.python.org/pypi/singledispatch)可以向后兼容 Python 2.6 到 Python 3.3。

import numbers
import html

def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

这个函数适用于任何 Python 类型,但是现在我们想做个扩展,让它针对不同的参数类型使用不同的方式来显示。

  • str:把内部的换行符替换为 <br>\n;不使用 <pre>,而是使用 <p>
  • int:以十进制和十六进制显示数字。
  • list:输出一个 HTML 列表,根据各个元素的类型进行格式化。

由于Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的变体,也无法使用不同的方式处理不同的数据类型。

singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。 这是单分派。如果根据多个参数选择专门的函数,那就是多分派了。

from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return '<pre>{}</pre>'.format(content)

@htmlize.register(str)
def _(text):
    content = html.escape(text).replace('\n', '<br>\n')
    return '<p>{0}</p>'.format(content)

@htmlize.register(numbers.Integral)
def _(n):
    return '<pre>{0} (0x{0:x})</pre>'.format(n)

# 可以叠放多个 register 装饰器,让同一个函数支持不同类型
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
    inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
    return '<ul>\n<li>' + inner + '</li>\n</ul>'

print(htmlize(12))
print(htmlize("hello"))
print(htmlize([1,2,3,4,5]))

#<pre>12 (0xc)</pre>
#<p>hello</p>
#<ul>
#<li><pre>1 (0x1)</pre></li>
#<li><pre>2 (0x2)</pre></li>
#<li><pre>3 (0x3)</pre></li>
#<li><pre>4 (0x4)</pre></li>
#<li><pre>5 (0x5)</pre></li>
#</ul>

注册的函数应该尽量处理抽象基类(如 numbers.Integralabc.MutableSequence),而不要处理具体实现(如 intlist),这样代码支持的兼容类型更广泛。

面向对象基础

引用

首先,我们要抛弃变量是存储数据的盒子这一错误观念,应该理解为附加在对象上的标注

为了理解 Python 中的赋值语句,应该始终先读右边。对象在右边创建或获取, 在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注

a = [1, 2, 3]
b = a
a.append(4)
b [1, 2, 3, 4]

image-20190403153226415

is和==

  • is 运算符比较两个对象的标识;id() 函数返回对象标识的 整数表示。
  • == 运算符比较两个对象的值(对象中保存的数据)

通常,我们关注的是值,而不是标识,因此 Python 代码中 ==出现的频率比is 高。

变量和单例值之间比较时,应该使用 is。最常使用 is 检查变量绑定的值是不是 None。推荐的写法:x is None

is 运算符比== 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是 直接比较两个整数 ID

浅复制与深复制

《Python特性》-4.4克隆对象

浅复制

构造方法list()[:] 做的是浅复制:即复制了最外层容器,副本中的元素是源容器中元素的引用。

如果所有元素都是不可变的,那么这样没有问题,还能节省内存。

image-20190404095010842

l2 = list(l1) 赋值后的程序状态。 l1l2 指代不同的列表, 但是二者引用同一个列表 [66, 55, 44] 和元组 (7, 8, 9)(图表由Python Tutor网站生成)

深复制

copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。

class Bus:

    def __init__(self, passengers=None):
        if passengers is None:
            self.passengers = []
        else:
            self.passengers = list(passengers)

    def pick(self, name):
        self.passengers.append(name)

    def drop(self, name):
        self.passengers.remove(name)

import copy

bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)
bus3 = copy.deepcopy(bus1)

id(bus1), id(bus2), id(bus3)
# (4301498296, 4301499416, 4301499752) ➊ bus id均不同

id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
# (4302658568, 4302658568, 4302657800) ➌ 只有第3个深拷贝的乘客id不同

bus1.drop('Bill')
bus2.passengers
# ['Alice', 'Claire', 'David'] ➋ bus1 中的 'Bill' 下车后,bus2 中也没有他了

bus3.passengers
# ['Alice', 'Bill', 'Claire', 'David'] ➍

此外,深复制有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法 __copy__()__deepcopy__(),控制 copy 和 deepcopy 的行 为,详情参见 copy 模块的文档(http://docs.python.org/3/library/copy.html)。

函数传参:参数共享

Python 唯一支持的参数传递模式是共享传参

  1. 共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参 是实参的别名。

  2. 不要使用可变类型作为参数的默认值

class HauntedBus:
    """备受幽灵乘客折磨的校车"""
    def __init__(self, passengers=[]):
        self.passengers = passengers

bus1 = HauntedBus()
bus2 = HauntedBus()
HauntedBus.__init__.__defaults__[0] is bus1.passengers # True
bus2.passengers is bus1.passengers # True

问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表

del和垃圾回收

del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。

import weakref

s1 = {1, 2, 3}
s2 = s1
def bye():
    print('Gone with the wind...')

ender = weakref.finalize(s1, bye)

ender.alive
# True

del s1 # del 不删除对象,而是删除对象的引用 
ender.alive 
# True

s2 = 'spam' # 重新绑定最后一个引用 s2,让 {1, 2, 3} 无法获取。对象被销毁了,调用了 bye 回调,ender.alive 的值变成了 False。
# Gone with the wind...

ender.alive
# False

你可能觉得奇怪,为什么示例中的 {1, 2, 3} 对象被销毁了?毕竟,我们把 s1 的引用传给finalize函数了呀,而为了监控对象和调用回调,必须要有引用。其实这是因为finalize 持有的是{1, 2, 3}弱引用

弱引用TODO

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。

weakref 模块的文档指出,weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用weakref集合和 finalize。TODO,这是什么意思。

注意 for 循环的变量的作用范围而导致的意外;for 循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失,如下例:

import weakref

class Cheese:
    def __init__(self, kind):
        self.kind = kind
    def __repr__(self):
        return 'Cheese(%r)' % self.kind    

stock = weakref.WeakValueDictionary()
catalog = [Cheese('Red Leicester'), 
           Cheese('Tilsit'),
           Cheese('Brie'), 
           Cheese('Parmesan')]

# 添加到弱引用 stock 中
for cheese in catalog:
    stock[cheese.kind] = cheese

sorted(stock.keys())
# ['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']

del catalog
sorted(stock.keys()) # 因为变量cheese还有对Cheese('Parmesan')的强引用
# ['Parmesan']

del cheese
sorted(stock.keys())
# []

弱引用的局限

不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。

  • 基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以。
  • set 实例可以作为所指对象
  • 用户定义的类型也没问题,这就解释了示例中为什么使用那个简单的 Cheese 类
  • int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行

面向对象:类

类(class)

  定义新类型的程序结构,里面有数据属性,以及用于操作数据属性的方法。

类型(type)

  程序中的各种数据,限定可取的值和可对数据做的操作。有些 Python 类型近似于机器数据类型(例如 float 和 bytes),而另一些则是机器数据类型的扩展(例如,int 不受 CPU 字长的限制,str 包含多字节 Unicode 数据码位)和特别高层的抽象(例如 dict、 deque,等等)。类型分为两类:用户定义的类型和解释器内置的类型。在 Python 2.2 统一类型和类之前,类型和类是不同的实体,用户定义的类不能扩展内置的类型。而在那之后,内置的类型和新式类兼容了,类是 type 的实例。在 Python 3 中,所有类都是新式类。

类型判断

Python判断变量的类型有两种方法:type(obj)isinstance(obj,C)

对于基本的数据类型两个的效果都一样

ip_port = ['219.135.164.245', 3128]
### type()
if type(ip_port) is list:
    print('list数组')
### isinstance()
if isinstance(ip_port, list):
    print('list数组')

区别之处

isinstance()type() 的区别在于:

  • isinstance(obj,C) 会判断 C 是不是父类类型,可判断继承
  • type(obj) == C不会认为子类是一种父类类型
class A:
    pass

class B(A):
    pass

isinstance(A(), A)  # returns True
type(A()) == A      # returns True
isinstance(B(), A)    # returns True
type(B()) == A        # returns False

属性

访问权限

一、_var“单下划线” 开始的成员变量叫做保护变量(protected),(不过《流畅的 python》9.7章不这样认为),比如_name,按照约定俗成的规定,只有类对象和子类对象能访问到这些变量;外部访问需通过类提供的接口(方法)进行访问。“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

Python 解释器不会对使用单个下划线的属性名做特殊处理,不过这是很多 Python 程序员 严格遵守的约定,他们不会在类外部访问这种属性

二、 __var“双下划线” 开始的是私有成员(private),只有类对象自己能访问,连子类对象也不能访问到这个数据。

Python解释器对外把Student类的__name变量改成了_Student__name(所以通过这个名字还是可以访问到的)

注意下面的这种错误写法

bart = Student('Bart Simpson', 59)
bart.get_name()
# 'Bart Simpson'
bart.__name = 'New Name' # 设置__name变量!,并没有修改成功!
print(bart.__name)
# 'New Name'
bart.get_name() # get_name()内部返回self.__name
# 'Bart Simpson'

三、变量名类似__xxx__的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量。

导入时:

在一个模块中以单下划线开头的变量和函数被默认当作内部函数,如果使用 from a_module import * 导入时,这部分变量和函数不会被导入。如果使用 import a_module 这样导入模块,仍然可以用 a_module._some_var 这样的形式访问到这样的对象,或者from mymod import _privatefunc

参考:https://www.cnblogs.com/work115/p/5606981.html

类属性与实例属性

  • 实例属性

给实例绑定属性的方法是通过实例变量.,或者通过self变量:

class Student(object):
    def __init__(self, name):
        self.name = name      # 实例属性绑定方法1

s = Student('Bob')
s.score = 90                # 实例属性绑定方法2
  • 类属性

直接在class中定义属性,这种属性是类属性,归Student类所有,这个属性虽然归类所有,但类的所有实例都可以访问到。

class Student(object):
    name = 'Student'
属性覆盖

Python 有个很独特的特性:类属性可用于为实例属性提供默认值。

使用self.var 读取它的值。因为实例本身没有 var 属性,所以 self.var 默认获 取的是Class.var类属性的值。

如果为不存在的实例属性赋值,会新建实例属性。同名类属性不受影响,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的属性定制不同的值。

但是如果使用的好,就会出现下面的问题

查找顺序(大坑)

实例属性->类属性

class Student(object):
    name = 'Student'

s = Student()

''' 访问 '''
print(s.name) # 实例并没有name属性,所以会继续查找class的name属性
# Student
print(Student.name) # 打印类的name属性
# Student

''' 修改 '''
s.name = 'Michael' # 给实例绑定name属性
print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
# Michael
print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
# Student
Student.name='new Student' # 这才是修改类属性
print(Student.name) 
# new Student

''' 删除 '''
del s.name # 如果删除实例的name属性
print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
# new Student

从上面的例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

属性获取

Python下一切皆对象,每个对象都有多个属性(attribute),Python对属性有一套统一的管理方案。

一共有4种:

  1. 使用属性运算符

    print(xmr.name)

  2. 通过属性字典__dict__
    print(xmr.__dict__['name'])

  3. 通过getattr函数
    print(getattr(xmr, 'name'))

  4. operator.attrgetter (可用于排序)

    import operator
    op = operator.attrgetter('name')
    print(op(xmr)
    
    import operator
    
    people = [Person('xiemanR', 18), Person('zhangshan', 17), Person('lisi', 20), Person('wangwu', 25)]
    
    r = sorted(people, key=operator.attrgetter('age'))
    
    for i in r:
        print(i)
    

__dict__与dir()的区别:

  1. dir()是一个内建函数,返回的是list;dir()用来寻找一个对象的所有属性,包括__dict__中的属性,包括从父类中继承的属性)。

  2. __dict__是一个字典,键为属性名,值为属性值; 并不是所有对象都拥有dict属性。许多内建类型就没有__dict__属性,如list,此时就需要用dir()来列出对象的所有属性。

    • 实例的__dict__仅存储与该实例相关的实例属性(self.xx,但不包括@property),正是因为实例的__dict__属性,每个实例的实例属性才会互不影响。

    • 类的__dict__存储所有实例共享的变量和函数(类属性,方法等),类的__dict__并不包含其父类的属性。

属性操作:xxxattr函数

仅仅把属性和方法列出来是不够的,配合内建函数getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态:

  • getattr(obj, 属性名/方法名, 默认值)
  • setattr(obj, 属性名/方法名, 值)
  • hasattr(obj, 属性名/方法名)
class MyObject(object):
     def __init__(self):
       self.x = 9
     def power(self):
         return self.x * self.x

obj = MyObject()


### 访问、修改属性
if hasattr(obj, 'x'):
    print(getattr(obj, 'x'))

if not hasattr(obj, 'y'):
    setattr(obj, 'y', 23)
    print(obj.y)

print(getattr(obj, 'z', 0))  # 属性不存在时,提供默认值

### 获取方法
if hasattr(obj,'power'):
    fn = getattr(obj,'power')
    print(fn())

属性查找失败后,解释器会调用__getattr__方法。

多数时候,如果实现了__getattr__ 方法,那么也要定义 __setattr__ 方法,以防对象的行为不一致。默认情况:在超类上调用 __setattr__ 方法,提供标准行为。

为了实现只读的功能,可以通过__setattr__方法进行控制

def __setattr__(self, name, value):
    cls = type(self)
    if len(name) == 1:
        if name in cls.shortcut_names:
            error = 'readonly attribute {attr_name!r}'
        elif name.islower():
            error = "can't set attributes 'a' to 'z' in {cls_name!r}"
        else:
            error = ''
        if error:
            msg = error.format(cls_name=cls.__name__, attr_name=name)
            raise AttributeError(msg)
    super().__setattr__(name, value)

特性 @property

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把成绩随便改

装饰器(decorator)可以给函数动态加上功能。对于类的方法,装饰器一样起作用。Python内置的@property装饰器就是负责把一个方法变成属性调用的。

内置的 property 经常用作装饰器,但它其实是一个类

特性是用于 管理实例属性类属性,真正的值存储在私有属性 __var

使用方式

方式1:

class LineItem:

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight # self.weight 会找到类属性(特性)weight,然后就会进入set_weight方法(访问顺序请看下面介绍)
        self.price = price

    def subtotal(self):
        return self.weight * self.price

    def get_weight(self):
        return self.__weight

    def set_weight(self, value):
        if value > 0:
            self.__weight = value
        else:
            raise ValueError('value must be > 0')

    weight = property(get_weight, set_weight)

类中的特性能影响实例属性的寻找方式,而一开始这种方式可能会让人觉得意外(访问顺序请看下面介绍)

方式2:

class Student(object):
    def __init__(self):
        self._score = 0

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value


s = Student()
print(s.score)  # OK,实际转化为s.get_score()
s.score = 60  # OK,实际转化为s.set_score(60)
print(s.score)  # OK,实际转化为s.get_score()
s.score = 9999 # 报错

只定义getter方法,不定义setter方法就是一个只读属性

删除操作

可以使用 @my_propety.deleter 装饰器包装一个方法,负责删除特性管理 的属性

# 来自电影《巨蟒与圣杯》,感觉这个例子不太好,看不出效果
class BlackKnight:
    def __init__(self):
        self.members = ['an arm', 'another arm', 'a leg', 'another leg']
        self.phrases = ["'Tis but a scratch.", "It's just a flesh wound.", "I'm invincible!", "All right, we'll call it a draw."]

    @property
    def member(self):
        print('next member is:')
        return self.members[0]

    @member.deleter
    def member(self):
        text = 'BLACK KNIGHT (loses {})\n-- {}'
        print(text.format(self.members.pop(0), self.phrases.pop(0)))

测试

knight = BlackKnight()
knight.member
# next member is: 'an arm'

del knight.member
# BLACK KNIGHT (loses an arm)
# -- 'Tis but a scratch.

del knight.member
# BLACK KNIGHT (loses another arm)
# -- It's just a flesh wound.

del knight.member
# BLACK KNIGHT (loses a leg)
# -- I'm invincible!

del knight.member
# BLACK KNIGHT (loses another leg)
# -- All right, we'll call it a draw.

在不使用装饰器的经典调用句法中,fdel 参数用于设置删值函数。

member = property(member_getter, fdel=member_deleter)
特性的文档

如果使用经典调用句法,为 property 对象设置文档字符串的方法是传入 doc 参数:

weight = property(get_weight, set_weight, doc='weight in kilograms')

使用装饰器创建 property 对象时,读值@property 装饰的方法的文档变成特性的文档。

class Foo:
    @property
    def bar(self):
        '''The bar attribute'''
        return self.__dict__['bar']

    @bar.setter
    def bar(self, value):
        self.__dict__['bar'] = value
Property源码

其实Property是数据描述符,参考:Properties

class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError("unreadable attribute")
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError("can't delete attribute")
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

特性工厂

工厂函数构建的特性利用,weight 特性覆盖了 weight 实例属性,对 self.weight 或 nutmeg.weight 的每个引用都由特性函数处理,只有 直接存取 __dict__ 属性才能跳过特性的处理逻辑

在真实的系统中,分散在多个类中的多个字段可能要做同样的验证,此时最好把 quantity 工厂函数放在实用工具模块中,以便重复使用。

# 特性工厂
def quantity(storage_name):
    # 运行这两个函数时,它们会从闭包中读取 storage_name,确定从哪里获取属性 的值,或者在哪里存储属性的值
    def qty_getter(instance):
        return instance.__dict__[storage_name]

    def qty_setter(instance, value):
        if value > 0:
            instance.__dict__[storage_name] = value
        else:
            raise ValueError('value must be > 0')

    return property(qty_getter, qty_setter)

特性是类属性。构建各个 quantity 特性对象时,要传入 LineItem 实例属性 的名称,让特性管理。

class LineItem:
    weight = quantity('weight') # 特性 weight 定义为类属性
    price = quantity('price')

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

__slots__

参考:Customizing attribute access

类可以定义这个这属性,限制实例能有哪些属性

原因:默认情况下,Python 在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存

原理:如果要处理数百万个属性不多的实例,通过 __slots__ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。

注意:

  • 父类中声明的 __slots__ 会被子类继承,但是子类默认拥有__dict____weakref__,因此子类默认是可以动态赋值,除非子类也定义了 __slots__ ,只需定义额外的、父类中没有的 slots 即可.

    class A:
       __slots__ = ('a','b')
    
    class B(A):
       __slots__ = ('c','d') # 如果没有这一行,B类可以任意添加属性,因为有 __dict__属性
    
    a = B()
    a.a, a.b, a.c a.d= 10, 20, 30, 40
    
  • 在类中定义 __slots__ 属性之后, 不再拥有__dict____weakref__属性,除非显式在 slots 中声明,或者父类中可用

  • 如果不把 ‘__dict__‘ 加入 __slots__,实例不能再有 __slots__ 中所列名称之外的其他属性,失去动态添加属性的功能

  • 如果不把 ‘__weakref__‘ 加入 __slots__,实例就不能作为弱引用的目标

  • __slots__实现方式是在类的层面上为每个变量创建描述符,因此不能定义与__slots__中的重名的类属性,比如想用于为实例提供默认值的类属性

在类中定义告诉解释器:“这个类中的所有实例属性都在这儿了!

__slots__ 属性的值是一个字符 串组成的元组,指明允许有的属性。

class Vector2d:
    __slots__ = ('x', 'y')

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

    def __repr__(self):
        return '<{},{}>'.format(self.x, self.y)

v=Vector2d(1,2)
>>> v.x
2
# 不允许赋值了给未知的属性
>>> v.o=10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Vector2d' object has no attribute 'o'

属性访问优先顺序

特性都是类属性,但是特性管理的其实是实例属性的存取

  • 如果实例和所属的类有同名数据属性,那么实例属性会覆盖(或称遮盖)类属性
  • 但是实例属性不会遮盖类特性
  • 直接从 Class 中读取 prop 特性,获取的是特性对象本身,不会运行特性的读值方法

obj.attr 这样的表达式不会从 obj 开始寻找 attr,而是从 obj.__class__ 开始,而且,仅当类中没有名为 attr 的特性时,Python 才会在 obj 实例中寻找。这条规则不仅适用于特性,还适用于一整类描述符——覆盖型描述符 (overriding descriptor) 具体看描述符的 【属性访问的优先级规则】 一章

class Class:
    data = 'the class data attr'
    @property
    def prop(self):
        return 'the prop value'

Class.prop # ➊ 直接从 Class 中读取 prop 特性,获取的是特性对象本身,不会运行特性的读值方法
# <property object at 0x1072b7408>

obj.prop # ➋ 执行特性的读值方法
# 'the prop value'

obj.prop = 'foo' # ➌ 执行特性的赋值方法
# Traceback (most recent call last): AttributeError: can't set attribute

obj.__dict__['prop'] = 'foo' # ➍ 绕过特性,给实例添加 prop 属性
vars(obj) # ➎ 
{ 'data': 'bar','prop': 'foo'}

obj.prop # ➏ 仍会运行特性的读值方法。特性没被实例属性遮盖。
# 'the prop value'

Class.prop = 'baz' # ➐覆盖 Class.prop 特性,销毁特性对象
obj.prop # ➑
# 'foo'

特殊属性的访问

参考:Special method lookup

结论:特殊方法的查找与普通属性一样,只有隐式的调用才会绕过一些限制

隐式调用特殊方法(如使用len(),repr(),hash()就会隐式调用__len__等方法),只有当方法是定义在object 的类中 而不是在 object 实例的字典中时才能正确执行,(if defined on an object’s type, not in the object’s instance dictionary. )这也是为什么下面的代码会出现异常:

class C:
    pass

c = C()
c.__len__ = lambda: 5
len(c)
# TypeError: object of type 'C' has no len()

但是下面这样就没有问题:

class C:
        def __len__(self):
                return 10

c = C()
len(c) # 10

这种现象在很多特殊方法上都有:__hash__,__repr__

隐式调用特殊方法时,它会绕过 必须传入实例参数 的限制

1 .__hash__() == hash(1)                    # 实例调用特殊方法,会将本身传入第一个参数
# True
int.__hash__() == hash(int)                # 类调用特殊方法,必须显式传入实例参数,否则报错
# TypeError: descriptor '__hash__' of 'int' object needs an argument

type(1).__hash__(1) == hash(1)        # 等价于上面,显式传入实例参数,就不会出错
# True
type(int).__hash__(int) == hash(int)    # 调用元类的特殊方法,也需要显式传入实例参数,只不过这个实例是一个类对象
# True

同时,它也会 绕过类、甚至元类的__getattribute__()方法

class Meta(type):
    def __getattribute__(*args):
        print("Metaclass getattribute invoked")
        return type.__getattribute__(*args)

class C(object, metaclass=Meta):
    def __len__(self):
        return 10
    def __getattribute__(*args):
        print("Class getattribute invoked")
        return object.__getattribute__(*args)

c = C()
c.__len__()        # ❶ 通过实例显式调用特殊方法,会调用"类"的 __getattribute__
# Class getattribute invoked
# 10
type(c).__len__(c) # ❷通过类显式调用特殊方法,会调用“元类"的 __getattribute__
# Metaclass getattribute invoked
# 10
len(c)                      # 隐式调用,绕过 __getattribute__
# 10

❷ 请参考:元类编程中的【类属性访问优先级】,就可以明白为什么会调用元类的方法

总结:处理属性的重要属性与函数

重要属性
  1. __class__ :对象所属类的引用,即 obj.__class__type(obj) 的作用相同
  2. __dict__ :一个映射,存储对象或类的可写属性。有 __dict__ 属性的对象,任何时候都能随意设置新属性。如果类有 __slots__ 属性,它的实例可能没有 __dict__属性。
  3. __slots__ :类可以定义这个这属性,限制实例能有哪些属性。__slots__ 属性的值是一个元组,每个元素是字符串,指明允许有的属性。 如果 __slots__ 中没有 ‘__dict__‘,那么该类的实例没有 __dict__ 属性,实例只允许有指定名称的属性。
内置函数
  1. dir([object]):列出对象的大多数属性。官方文档dir函数的目的是交互式使用, 因此没有提供完整的属性列表,只列出一组【重要的】属性名。

    • dir 函数能审查有或没有 dict 属性的对象。

    • dir 函数不会列出 dict 属性本身,但会列出其中的键。

    • dir 函数也不会列出类的几个特殊属性,例如 mrobasesname

    • 如果没有指定可选的 object 参数,dir 函数会列出当前作用域中的名称。

  2. vars([object]) :返回 object 对象的 __dict__ 属性;

    如果实例所属的类定义了 __slots__ 属性, 实例没有 __dict__ 属性,那么 vars 函数不能处理那个实例(dir 函数能处理这样的实例)。如果没有指定参数,那么 vars() 函数的作用与 locals() 函数一样:返回表示本地作用域的字典。

  3. getattr(object, name[, default]) :从 object 对象中获取 name 字符串对应的属性。获取的属性可能来自对象所属的类或超类。如果没有找到指定的属性,getattr 函数抛出 AttributeError 异常,或者返回 default 参数的值。

  4. hasattr(object, name):如果object 对象中存在指定的属性,或者能以某种方式(例如继承)通过 object 对象获取指定的属性,返回 True。文档说:“这个函数的实现方法是调用 getattr(object, name) 函数,看看是否抛出 AttributeError 异常。”

  5. setattr(object, name, value) :把 object 对象指定属性的值设为 value,前提是 object 对象能接受那个值。

    这个函数可能会创建一个新属性,或者覆盖现有的属性

特殊方法

​ 使用点号或内置的 getattr、hasattr 和 setattr 函数存取属性都会触发下述列表中相应的特殊方法。直接通过实例的 __dict__ 属性读写属性不会触发这些特殊方法

​ 仅当特殊方法在对象所属的类型上定义,而不是在对象的实例字典中定义时,才能确保隐式调用特殊方法调用成功(obj.attrgetattr(obj, 'attr', 42) 都会触发 Class.__getattribute__(obj, 'attr') 方法)

​ 特殊方法 不会被同名实例属性遮盖

  1. __dir__(self):把对象传给 dir 函数时调用,列出属性。例如,dir(obj) 触发 Class.__dir__(obj) 方法。

  2. __getattr__(self, name):仅当搜索过 obj、Class 和超类之后,获取指定的属性失败时调用。也就是只有当试图访问不存在的属性时它才会被调用。表达式 obj.no_such_attrgetattr(obj, 'no_such_attr')hasattr(obj, 'no_such_attr') 可能会触发 Class.__getattr__(obj, 'no_such_attr') 方法,但是,仅当在 obj、Class 和超类中找不到指定的属性时才会触发。__getattr__只在对象的类中寻找,而不在实例中寻找(这句话不是很明白)。

  3. __getattribute__(self, name):点号、 getattrhasattr 内置函数会触发这个方法。尝试获取指定的属性时总会调用这个方法,不过,寻找的属性是特殊属性或特殊方法时除外

    调用 __getattribute__ 方法且抛出 AttributeError 异常时,才会调用 __getattr__ 方法。为了在获取 obj 实例的属性时不导致无限递归,__getattribute__ 方法的实现要使用 super().__getattribute__(obj, name)

    class A:
        '''__getattr__ 与 __getattribute__的调用顺序'''
        def __getattribute__(self, item):
            if item == "attr":
                raise AttributeError # 会进入__getattr__方法
            return "__getattribute__"
    
        def __getattr__(self, item):
            return "__getattr__"
    
  1. __setattr__(self, name, value):尝试设置指定的属性时总会调用这个方法。

    点号和 setattr 内置函数会触发这个方法。例如,obj.attr = 42setattr(obj, 'attr', 42) 都会触发 Class.__setattr__(obj, ‘attr’, 42) 方法。

  2. __delattr__(self, name) :只要使用 del 语句删除属性,就会调用这个方法。例如,del obj.attr 语句触发 Class.__delattr__(obj, 'attr') 方法。

注意:特殊方法 __getattribute____setattr__不管怎样都会调用,几乎会影响每一次属性存取

实例方法、类方法、静态方法

首先形式上的区别:

  • 实例方法隐含的参数为类实例self。
  • 类方法隐含的参数为类本身cls。 如果存在类的继承,那类方法获取的类是类树上最底层(具体)的类(子类)。
  • 静态方法无隐含参数,主要为了类实例也可以直接调用静态方法。
class A:
    class_var = "class_var"

    def method(self, name):
        print(self)
        self.class_var = name # 绑定了实例属性class_var,并不会影响到外层真正的类属性
        print(self.class_var)

    @classmethod
    def classMethod(cls, name):
        print(cls)
        print(cls.class_var)

    @staticmethod
    def staticMethod(name):
        print(name)

a = A()
a.class_var = "this is new"

a.method("name")
# A.method("name") # 报错
A.method(a, "name") # 除非手动传入 实例对象
print("A的类属性不变:", A.class_var)

a.classMethod("name") # 会自动传入实例a的类A
A.classMethod("name")

a.staticMethod("name")
A.staticMethod("name")
实例方法 类方法 静态方法
实例调用 a.method(a,…) a.classMethod(type(a),…) a.staticMethod()
类调用 A.method(a,…)需要显式传入实例对象 a A.classMethod(A,…) A.staticMethod()

实例方法(普通方法):随着实例属性的改变而改变

类方法(无论是类调用还是实例调用):都是类属性的值,不随实例属性的变化而变化

静态方法:不可以访问类属性,故直接输出传入方法的值

  • classmethod 最常见的用途是当函数只需要类的引用而不关心实例是什么的情况,另一个功能是定义备选 构造方法

  • staticmethod 不是特别有用,如果想定义不需要与类交互的函数,那么在模块中定义就好了。有时,函数虽然从不处理类,但是函数的功能与类紧密相关,因此想把它放在近处。即便如此,在同一模块中的类前面或后面定义函数也就行了。

    技术审校之一 Leonardo Rochael 不同意我对 staticmethod 的见解,作为反驳,他推荐阅读 Julien Danjou 写的一篇博客文章,题为“The Definitive Guide on How to Use Static, Class or Abstract Methods in Python”。Danjou 的这篇文章写得很好,我推 荐阅读。

继承(TODO)

12章节

class A:
    def __init__(self):
        self.a=10
class B:
    def __init__(self):
        self.b=20
class C(A,B):
    pass
    # def __init__(self):
    #     super().__init__()

与 java 不同,C 并不会继承实例的属性,默认__init__会调用超类的构造方法,因此会得到 a 属性,但是如果覆盖了__init__方法,没有调用超类的构造方法,就会失去 a 属性

内置函数(特殊方法)

首先明确一点,特殊方法的存在是为了被 Python 解释器调用的,你自己并不需要调用它们。也就是说没有my_object.__len__()这种写法,而应该使用len(my_object)

特殊方法 不会被同名实例属性遮盖

class A:
    def __str__(self):
        return "class A"

a=A()
print(a)
# class A
a.__str__=None #并不会被覆盖
print(a)
# class A

在执行len(my_object)的时候:

  1. 如果my_object是一个自定义类的对象,Python会自己去调用其中由你实现的__len__方法
  2. 如果是 Python 内置的类型,比如列表(list)、字符串(str)、字节序列 (bytearray)等,那么 CPython 会抄个近路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体。直接读取这个值比调用一个方法要快很多。

很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在 x 中被实现了。

通常你的代码无需直接使用特殊方法。通过内置的函数(例如len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。

init TODO

new

用于新建 cls 类的实例, __new__() 是静态方法(通过特殊方法实现的,不用@staticmethod)第一个参数是 需要创建出来的实例对应的类, 剩余的参数args将被传到 对象构造表达式(object constructor expression:cls(args)),__new__() 的返回值必须是一个新的对象,通常就是cls的实例。

覆盖 new 方法的一种典型实现是调用超类的 __new__ 方法: super().__new__(cls[, ...]) 然后对 新创建的实例 进行需要的操作,最后返回实例。

  • 如果返回的是 cls类的实例,就会调用该实例的 init 方法: __init__(self[, ...]), self 就是这个实例本身,其他的参数与传递给 new 方法一样。

  • 如果返回的不是 cls类的实例,就不会调用该实例的 init 方法

__new__() 主要是用于允许不可变类的子类(如 int, str, or tuple) 自定义实例的创建。 也常常在自定义的元类中被覆盖,用于自定义类的创建

new方法接受的参数虽然也是和init一样,initnew 最主要的区别在于:

  • new创建这个类实例的方法,通常用于控制生成一个新实例的过程。它是类级别的方法。
  • init 在得到 new 方法返回的实例后,用于初始化这个实例,控制这个初始化的过程,比如添加一些属性,做一些额外的操作,发生在类实例被创建完以后。它是实例级别的方法。
class Person(object):
  '''Silly Person'''
  def __new__(cls, name, age):
      print('__new__ called.')
      return super(Person, cls).__new__(cls, name, age) 
      # 调用超类的 new 方法
      # super().__new__(cls) 就会调用 object.__new__(Person)创建一个 Person 实例

  def __init__(self, name, age):
      print('__init__ called.')
      self.name = name
      self.age = age

  def __str__(self):
      return '<Person: %s(%s)>' % (self.name, self.age)

if __name__ == '__main__:
    piglei = Person(‘piglei’, 24)
    print(piglei)

执行结果:

__new__ called.
__init__ called.
<Person: piglei(24)>
作用
实现单例

事实上,当我们理解了new方法后,我们还可以利用它来做一些其他有趣的事情,比如实现 设计模式中的 单例模式(singleton) 。
因为类每一次实例化后产生的过程都是通过new来控制的,所以通过重载new方法,我们 可以很简单的实现单例模式。

def __new__(cls):
        # 关键在于这,每一次实例化的时候,我们都只会返回这同一个instance对象
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance

obj1 = Singleton()
obj2 = Singleton()

obj1.attr1 = 'value1'
print obj1.attr1, obj2.attr1
print obj1 is obj2

输出结果:

True

TODO:有参数的单例模式怎么实现

创建对象
class FrozenJSON:
    """一个只读接口,使用属性表示法访问JSON类对象 """
    def __new__(cls, arg):
        if isinstance(arg, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(arg, abc.MutableSequence):
            return [cls(item) for item in arg]
        else:
            return arg

    def __init__(self, mapping):
        self.__data = {}
        for key, value in mapping.items():
            if iskeyword(key):
                key += '_'
            self.__data[key] = value

    def __getattr__(self, name):
        if hasattr(self.__data, name):
            return getattr(self.__data, name)
        else:
            return FrozenJSON(self.__data[name])

s={"key1":{"num":11},"key2":[1,2,3]}
ss=FrozenJSON(s)
print(ss.key1.num)
print(ss.key2)
# 11
# [1, 2, 3]

repr与 str

  • __repr__ 返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象。交互式控制台和调试程序(debugger)用 repr 函数来获取字符串表示形式;
  • __str__ 返回的字符串对终端用户更友好,在str()函数或是在用print函数打印一个对象的时候才被调用的

如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象 没有 __str__ 函数,而 Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。

使用 reprlib 模块可以生成长度有限的表示形式,reprlib.repr() 函数用于生成大型结构或递归结构的安全表示形式,它会限制输出字符串的长度,用 ‘…’ 表示截断的部分,如 array('d', [0.0, 1.0, 2.0, 3.0, 4.0, …])

Difference between str and repr

format格式化显示

内置的format()函数和 str.format()方法把各个类型的格式化方式委托给相应的 .__format__(format_spec) 方法。format_spec 是格式说明符,它是:

  • format(my_obj, format_spec) 的第二个参数,或者
  • str.format() 方法的str{} 代换字段中冒号后面的部分
n=1/3

format(n, '0.4f') # 格式说明符是 '0.4f'
# 0.3333

'n is {rate:0.2f}'.format(rate=n) #格式说明符是 '0.2f'
# n is 0.33

{0.mass:5.3e} 这样的格式字符串其实包含两部分,

  1. 冒号左边的 0.mass 在代换字段句法中是字段名

  2. 冒号后面的 5.3e格式说明符

    格式说明符使用的表示法叫格式规范微语言(Format Specification MiniLanguage)

自定义格式化代码

如果类没有定义__format__方法,从 object 继承的方法__format__会返回 str(my_object),然而,如果传入格式说明符,object.__format__ 方法会抛出 TypeError

格式规范微语言是可扩展的,因为各个类可以自行决定如何解释 format_spec 参数。

自定义的格式代码选择字母时,我会避免使用其他类型用过的字母。在格式 规范微语言(Format Specification MiniLanguage)中我们看到,整数使用的代码有 ‘bcdoxXn’,浮点数使用的代码有 ‘eEfFgGn%’,字符串使用的代码 有 ‘s’。因此,为极坐标选的代码是 ‘p’。各个类使用自己的方式解释格式代码, 在自定义的格式代码中重复使用代码字母是不会出错,但是可能会让用户困惑。

import math
class Vector():
    def angle(self):
        return math.atan2(self.y, self.x)

    def __format__(self, fmt_spec=''):
        if fmt_spec.endswith('p'):
            fmt_spec = fmt_spec[:-1] #删除 'p' 后缀
            coords = (abs(self), self.angle()) #极坐标:(magnitude, angle)
            outer_fmt = '<{}, {}>'
        else:
            coords = self
            outer_fmt = '({}, {})'
        components = (format(c, fmt_spec) for c in coords)
        return outer_fmt.format(*components)

hash

为了把实例变成可散列的:

  1. 必须使用 __hash__ 方法,还需要__eq__方法
  2. 此外,还要让实例不可变
  3. 根据特殊方法 hash 的文档 (https://docs.python.org/3/reference/datamodel.html),最好使用位运算符异或(^)**混合各分量的散列值**——我们会这么做。
class Vector2d:
    typecode = 'd'

    def __init__(self, x, y):
        self.__x = float(x)
        self.__y = float(y)

    @property
    def x(self):
        return self.__x

    @property
    def y(self):
        return self.__y

    def __hash__(self):
        return hash(self.x) ^ hash(self.y)

    def __eq__(self, other):
        return tuple(self) == tuple(other)

    # 是为了 tuple 方法能够使用
    def __iter__(self):
        return (i for i in (self.x, self.y))

iter(TODO)

byte

调用bytes() 函数时,会调用类中的__byte__(),返回字节序

dict

__dict__是一个字典或者 mapping 类型的对象,用于保存对象可读写的属性(保存着实例中的全部属性)

  • 属性引用[Attribute references]会被转换为在__dict__字典中查找, m.x 等价于 m.__dict__["x"].

  • 属性赋值[Attribute assignment] 会更新模块的命名空间字典(module’s namespace dictionary), m.x = 1 等价于 m.__dict__["x"] = 1.

更新实例的 __dict__ 属性,把值设为一个映射,能快速地在那个实例中创建一堆属性。

可以使用vars(obj)获取dict

class Record:
    def __init__(self, **kwargs):
        self.key = 0
        self.__dict__.update(kwargs)


dic = {'key1': 1, 'key2': 2}
r = Record(**dic)
print(r.__dict__)
print(vars(r))
# {'key': 0, 'key1': 1, 'key2': 2}
# {'key': 0, 'key1': 1, 'key2': 2}

Python 标准库中至少有两个与 Record 类似的类,其实例可以有任意个属性, 由传给构造方法的关键字参数构建——multiprocessing.Namespace 类和 argparse.Namespace 类

getattr(TODO)

个人结论:对找不到属性是,应该自定义 getattr,进行处理

getattribute(TODO)

尝试获取指定的属性时总会调用这个方法,不过,寻找的属性是特殊属性或特殊方法 时除外。点号与 getattr 和 hasattr 内置函数会触发这个方法。调用 getattribute 方法且抛出 AttributeError 异常时,才会调用 getattr 方 法。为了在获取 obj 实例的属性时不导致无限递归,getattribute 方法的实现要 使用 super().getattribute(obj, name)。

Called unconditionally to implement attribute accesses for instances of the class. If the class also defines __getattr__(), the latter will not be called unless __getattribute__() either calls it explicitly or raises an AttributeError. This method should return the (computed) attribute value or raise an AttributeError exception.

In order to avoid infinite recursion in this method, its implementation should always call the base class method with the same name to access any attributes it needs, for example, object.__getattribute__(self, name).

This method may still be bypassed when looking up special methods as the result of implicit invocation via language syntax or built-in functions. See Special method lookup.

__setattr__(TODO)

属性描述符

背景

在前面我们了解了对象属性访问和行为控制的一些特殊方法,例如__getattribute____getattr____setattr____delattr__。这些方法具有属性的”普适性”,可以用于属性查找、设置、删除的一般逻辑,也就是说所有的属性都可以使用这些方法实现属性的查找、设置、删除等操作。

但是,这并不能很好地实现对某个具体属性的访问控制行为。假如要实现控制dog.age属性的类型只能是整数,如果单单去修改__setattr__方法满足它,那这个方法便有可能不能支持其他的属性设置。对属性的控制行为就由属性对象来控制

这里单独抽离出来一个属性对象,在属性对象中定义这个属性的查找、设置、删除行为。这个属性对象就是描述符。描述符对象一般是作为其他类对象的属性而存在。在其内部定义了三个方法用来实现属性对象的查找、设置、删除行为,描述符是实现了特定协议的类:

  • __get__(self, instance, owner):定义当试图取出描述符的值时的行为, owner 参数是托管类的 class 引用,instance 是该 class 的实例,self是描述符实例(具体术语请看后面定义)
  • __set__(self, instance, value):定义当描述符的值改变时的行为。
  • __delete__(self, instance):定义当描述符的值被删除时的行为。

术语概念

image-20190417114136034

  • 描述符类:实现描述符协议的类。在图中,是 Quantity 类。

  • 托管类:使用描述符对象管理类中某个属性的类,把描述符实例声明为类属性——图中的 LineItem 类。

  • 描述符实例:描述符类的各个实例,声明为托管类的类属性。在图中,各个描述符实例使用箭头和带下划线的名称表示(在 UML 中,下划线表示类属性)。与黑色菱形接触的 LineItem 类包含描述符实例。

  • 托管实例:托管类的实例。在这个示例中,LineItem 实例是托管实例(没在类图中展示)。

  • 储存属性:托管【实例】中的属性instance.__dict__[storage_name],用于存储由描述符管理的属性的值。在图中,LineItem 实例的 weight 和 price 属性是储存属性。这种属性与描述符属性不同,描述符属性都是类属性。

  • 托管属性:托管类中由描述符实例处理的公开属性,值存储在储存属性中。虽然托管属性在托管类中定义,但是作用相当于实例属性(即各个实例通常有各自的值,存储在储存属性中)也就是说,描述符实例储存属性托管属性建立了基础。

用法

描述符的用法是,创建 一个实例,作为另一个类的类属性。

class Quantity:
    def __init__(self, storage_name):
        self.storage_name = storage_name

    def __set__(self, instance, value):
        if value > 0:
              # 必须直接处理托管实例的 __dict__ 属性;如果使用内置的 setattr 函数,因为[托管属性]和[储存属性]的名称不同,会再次触发 __set__ 方法,导致无限递归。
            instance.__dict__[self.storage_name] = value 
        else:
            raise ValueError('value must be > 0')

class LineItem:
    weight = Quantity('weight')
    price = Quantity('price')

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

不能试图使用下面这种错误的写法:

self.dict[self.storage_name] = value

错误的原因:想想 __set__ 方法里的selfinstance的意思。这里,self 是描述符实例,它其实是托管类的类属性。同一时刻,内存中可能有几千个 LineItem 实例,不过只会有2个描述符实例:LineItem.weight 和 LineItem.price。因此,存储在描述符实例中的数据,其实会变成 LineItem 类的类属性,从而由全部 LineItem 实例共享。

重复的属性名解决方案

想要实现下面代码,避免重复的属性名

class LineItem:
        weight = Quantity() 
        price = Quantity()

但是,赋值语句右手边的表达式先执行,而此时变量还不存在。 Quantity() 表达式计算的结果是创建描述符实例,而此时 Quantity 类中的代码无法猜出要把描述符绑定给哪个变量。

这里实现一个不太优雅但是可行的方案,更好的解决方案是使用类装饰器或元类:

想法:我们将为每个 Quantity 实例的 storage_name 属性自动生成一个唯一的字符串


class Quantity:
    __counter = 0 # ❶ __counter 统计 Quantity 实例的数量

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index) # ❷ 
        cls.__counter += 1

    def __get__(self, instance, owner): 
        if instance is None: # ❹
            return self
        else:
            return getattr(instance, self.storage_name) # ➎

    def __set__(self, instance, value): # ➏
        if value > 0:
            setattr(instance, self.storage_name, value)
        else:
            raise ValueError('value must be > 0')

class LineItem:
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

❷:定义[储存属性],也就是price = Quantity('price')中右边的’price’,自动生成类似_Quantity#0 的储存属性名

❹:考虑A.x这种类直接访问属性的情况,具体看下面的 【触发 get 的几种形式】

➎,➏:这里可以使用内置的高阶函数 getattr 和 setattr 存取值,无需使用 instance.__dict__,因为托管属性和储存属性的名称不同

触发 get 的几种形式

参考:Descriptor HowTo GuideInvoking Descriptors

描述符的方法调用起始于绑定a.x,从而触发__getattribute__方法,而参数如何装配,取决于a是实例还是类

  • 直接调用(少见):代码直接调用描述符的 __get__ 方法: x.__get__(a)
  • 实例绑定:a.xobject.__getattribute__()转变为type(a).__dict__['x'].__get__(a, type(a)).
  • 类绑定:A.xtype.__getattribute__()转变为A.__dict__['x'].__get__(None, A).
  • 超类绑定:If a is an instance of super, then the binding super(B,obj).m() searches obj.class.mro for the base class Aimmediately preceding B and then invokes the descriptor with the call: A.__dict__['m'].__get__(obj, obj.__class__).(不太明白TODO)

演示用的伪代码(TODO,不太明白)

def __getattribute__(self, key):
    "Emulate type_getattro() in Objects/typeobject.c"
    v = object.__getattribute__(self, key)
    if hasattr(v, '__get__'):
        return v.__get__(None, self)
    return v

特性工厂函数与描述符类比较

def quantity():
    try:
        quantity.counter += 1 # counter 定义为 quantity 函数自身的属性
    except AttributeError:
        quantity.counter = 0
        storage_name = '_{}:{}'.format('quantity', quantity.counter)

    def qty_getter(instance):
        return getattr(instance, storage_name)

    def qty_setter(instance, value):
        if value > 0:
            setattr(instance, storage_name, value)
        else:
            raise ValueError('value must be > 0')

    return property(qty_getter, qty_setter)

我喜欢描述符类那种方式,主要有下列两个原因。

  1. 描述符类可以使用子类扩展;若想重用工厂函数中的代码,除了复制粘贴,很难有其他方法。
  2. 与工厂函数中使用函数属性和闭包保持状态相比,在类属性和实例属性中保持状态更易于理解。

继承描述符

商品的描述信息不能为空,我们要再创建一个描述符:NonBlank。在设计 NonBlank 的过程中,我们发现,它与 Quantity 描述符很像,只是验证逻辑不同。

回想 Quantity 的功能,我们注意到它做了两件不同的事:管理托管实例中的储存属性, 以及验证用于设置那两个属性的值。由此可知,我们可以重构,并创建两个基类。

image-20190424092434540

模板方法设计模式:一个模板方法用一些抽象的操作定义一个算法,而子类将重定义这些操作以提供具体 的行为

import abc
class AutoStorage:

    __counter = 0

    def __init__(self):
        cls = self.__class__
        prefix = cls.__name__
        index = cls.__counter
        self.storage_name = '_{}#{}'.format(prefix, index)
        cls.__counter += 1

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            return getattr(instance, self.storage_name)

    def __set__(self, instance, value):
        setattr(instance, self.storage_name, value)

class Validated(abc.ABC, AutoStorage):
        '''抽象基类'''
    def __set__(self, instance, value):
        value = self.validate(instance, value) # set之前调用验证方法
        super().__set__(instance, value)

    @abc.abstractmethod
    def validate(self, instance, value):
        """return validated value or raise ValueError"""

class Quantity(Validated):
    """继承Validated,实现具体的验证逻辑"""
    def validate(self, instance, value):
        if value <= 0:
            raise ValueError('value must be > 0')
        return value

class NonBlank(Validated):
    """继承Validated,实现具体的验证逻辑:at least one non-space character"""
    def validate(self, instance, value):
        value = value.strip()
        if len(value) == 0:
            raise ValueError('value cannot be empty or blank')
        return value


class LineItem:
      description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

描述符的种类

参考:https://docs.python.org/3/howto/descriptor.html

  • 任意实现__get____set____delete__方法中的一个就可以认为是描述符;
  • 只实现__get__ 方法的对象是非数据描述符(non-data descriptor),意味着在初始化之后它们只能被读取,会被实例属性覆盖。常用于方法(方法其实是非属性描述符)
  • 实现了__get____set__ 的对象是数据描述符(data descriptor)
  • 个人结论:只有__set__的对象不是数据描述符
  • 实现__set____delete__中的任意一个的对象是数据描述符,意味着这种属性是可读写的。通常__get____set__ 都会定义,会覆盖普通实例属性;如果没有定义 __get__ ,在实例中没有同名的实例属性的情况,返回描述符本身,否则返回实例属性
  • 描述符的调用由__getattribute__() 方法触发,覆盖该方法可以阻止描述符的调用

http://python.jobbole.com/88582/

特性(property)是数据描述符,不论有没有使用`@attr.setter定义赋值函数,因为这个类实现了set`方法,默认抛出 AttributeError: can’t set attribute(具体看【特性】-property 源码 这一节)

推荐看属性的【访问优先级规则】,才能深刻理解为什么会有下面的现象

定义做实验的代码

### 辅助函数,仅用于显示 ###

def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]


def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))


def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))


### 对这个示例重要的类 ###

class Overriding:
    """也称数据描述符或强制描述符"""

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

    def __set__(self, instance, value):
        print_args('set', self, instance, value)


class OverridingNoGet:  # ➌
    """没有``__get__``方法的覆盖型描述符"""

    def __set__(self, instance, value):
        print_args('set', self, instance, value)


class NonOverriding:  # ➍
    """也称非数据描述符或遮盖型描述符"""

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)


class Managed:  # ➎ 托管类,使用各个描述符类的一个实例。

    over = Overriding()
    over_no_get = OverridingNoGet()
    non_over = NonOverriding()

    def spam(self):  # ➏ 方法也是描述符,下一节会介绍
        print('-> Managed.spam({})'.format(display(self)))

同时有 get和set的数据描述符

特性(property)是数据描述符,不论有没有使用`@attr.setter定义赋值函数,因为这个类实现了set`方法

obj = Managed()

obj.over # ➋ 触发描述符 __get__ 方法,第二个参数(instance)的值是托管实例 obj
# -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

Managed.over # ➌ 触发描述符 __get__ 方法,第二个参数(instance)的值是 None
# -> Overriding.__get__(<Overriding object>, None, <class Managed>)

obj.over = 7 # ➍ 触发描述符 __set__ 方法
# -> Overriding.__set__(<Overriding object>, <Managed object>, 7)

obj.over 
# -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

obj.__dict__['over'] = 8 # ➏ 跳过描述符
vars(obj)
# {'over': 8}

obj.over # ➑ 即使有名为 over 的实例属性,Managed.over 描述符仍会覆盖读取 obj.over 这个操作。
# -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)

只有 set 的数据描述符

覆盖型描述符既会实现 __set__ 方法,也会实现 __get__ 方法,不过也可以只实 现 __set__ 方法。

赋值操作还是由描述符的 __set__ 接管,而取值操作,分2种情况:

  • 有同名实例属性:返回实例属性
  • 没有同名实例属性:返回描述符对象本身

也就是说,实例属性的读操作 会遮盖描述符

obj=Managed()

obj.over_no_get # ➊ 没有同名实例属性,返回描述符本身
# <__main__.OverridingNoGet object at 0x665bcc>

Managed.over_no_get # ➋ 直接从类属性中获取描述符本身,不考虑实例属性
# <__main__.OverridingNoGet object at 0x665bcc>

obj.over_no_get = 7 # ➌
# -> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)

obj.over_no_get # ➍ 没有同名实例属性,仍然返回描述符本身
# <__main__.OverridingNoGet object at 0x665bcc>

obj.__dict__['over_no_get'] = 9 # 添加同名实例属性
obj.over_no_get # ➏ 同名实例属性会遮盖描述符
# 9

obj.over_no_get = 7 # ➐ 赋值经过描述符的 __set__ 方法处理
# -> OverridingNoGet.__set__(<OverridingNoGet object>, <Managed object>, 7)

obj.over_no_get # ➑ 读取时,只要有同名的实例属性,描述符就会被遮盖
# 9

只有 get 的非数据描述符

如果设置了同名的实例属性,描述符会被遮盖,致使描述符无法处理那个实例的那个属性。

作用:类中的方法就是非数据描述符,请看 【方法是非数据描述符 】一节

obj = Managed()

obj.non_over ➊
# -> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

obj.non_over = 7 ➋
obj.non_over # ➌ 现在,obj 有名为 non_over 的实例属性,把 Managed 类的同名描述符属性遮盖掉
# 7

Managed.non_over # ➍ 描述符依然存在,会通过类截获这次访问,触发描述符的__get__
# -> NonOverriding.__get__(<NonOverriding object>, None, <class Managed>)

del obj.non_over # ➎ 删除实例属性
obj.non_over # ➏ 恢复触发类中描述符的 __get__ 方法
# -> NonOverriding.__get__(<NonOverriding object>, <Managed object>, <class Managed>)

在类中覆盖描述符

从上面例子可以看出,依附在类上的描述符无法控制为类属性赋值的操作。其实,这意味着为类属性赋值能覆盖 描述符属性

不管描述符是不是覆盖型,为类属性赋值都能覆盖描述符。这是一种猴子补丁技术

obj = Managed()
Managed.over = 1
Managed.over_no_get = 2
Managed.non_over = 3
obj.over, obj.over_no_get, obj.non_over
# (1, 2, 3)

揭示了读写属性的另一种不对等读类属性的操作可以由依附在托管类上定义有 __get__ 方法的描述符处理,但是写类属性的操作不会由依附在托管类上定义有 __set__ 方法的描述符处理

想控制设置类属性的操作,要把描述符依附在类的类上,即依附在元类上。 默认情况下,对用户定义的类来说,其元类是 type

描述符用法建议

内置的 property 类创建的其实是覆盖型描述符,set 方法和 get 方法都 实现了,即便不定义设值方法也是如此。特性的 set 方法默认抛出 AttributeError: can’t set attribute,因此创建只读属性最简单的方式是使用特 性

只读描述符

并非只实现 get,__get____set__ 两个方法必须都定义,否则,实例的同名属性会遮盖描述符,只读属性__set__方法只需抛出 AttributeError 异常即可

用于验证的描述符

仅用于验证的描述符attr__set__ 方法应该检查 value 参数获得的值,然后使用描述符实例的名称为键,直接设置 instance.__dict__['attr']=value

可以没有 __get__ 方法,这样,从实例中读取同名属性的速度很快,因为不用经过 __get__ 方法处理

只有 __get__的描述符(高效缓存)

那么创建的是非覆盖型描述符。这种描述符可用于执行某些耗费资源的计算,得到 value,然后通过instance.__dict__['attr']=value中设置同名属性,缓存结果,实例属性就会覆盖描述符。

特殊方法不会被实例属性覆盖

my_obj.the_method = 7 这样简单赋值之后,后续通过该实例访问 the_method 得到的是数字 7,特殊方法不受这个问题的影响。解释器只会在类中寻找特殊的方法,也就是说 repr(x) 执行的其实是 x.__class__.__repr__(x)

属性访问的优先级规则

参考:Invoking Descriptors

获取属性时,必然调用默认 Class.__getattribute__,其内部逻辑如下:

def __getattribute__():
    cls_attr = None  # 类中找到的属性

    # 遍历父类链,查看每个类的 dict
    for class in obj.mro():
        if class.__dict__['x'] is not None:  # 类字典中查找
            cls_attr =class .__dict__['x']
            break

    if cls_attr is not None and is Data Descriptor:
        return cls_attr.__get__()  # 返回数据描述符的值(get 和 set 都有)

    if obj.__dict__['x'] is not None:
        return obj.__dict__['x']  # 实例字典中查找

    if cls_attr is not None
        if cls_attr is Non-Data Descriptor:#(只有 get)
            return cls_attr.__get__()
        return cls_attr # 普通数据、只有set的描述符

    raise AttributeError

if Class has the special method __getattr__:
    return Class.__getattr__()

如图所示:

img

图来自:http://python.jobbole.com/88582/

实验验证的代码:


### 辅助函数,仅用于显示 ###

def cls_name(obj_or_cls):
    cls = type(obj_or_cls)
    if cls is type:
        cls = obj_or_cls
    return cls.__name__.split('.')[-1]


def display(obj):
    cls = type(obj)
    if cls is type:
        return '<class {}>'.format(obj.__name__)
    elif cls in [type(None), int]:
        return repr(obj)
    else:
        return '<{} object>'.format(cls_name(obj))


def print_args(name, *args):
    pseudo_args = ', '.join(display(x) for x in args)
    print('-> {}.__{}__({})'.format(cls_name(args[0]), name, pseudo_args))


### 对这个示例重要的类 ###

class Overriding:
    """也称数据描述符或强制描述符"""

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)

    def __set__(self, instance, value):
        print_args('set', self, instance, value)


class OverridingNoGet:  # ➌
    """没有``__get__``方法的覆盖型描述符"""

    def __set__(self, instance, value):
        print_args('set', self, instance, value)


class NonOverriding:  # ➍
    """也称非数据描述符或遮盖型描述符"""

    def __get__(self, instance, owner):
        print_args('get', self, instance, owner)


class Parent:
    over = Overriding()
    def __getattr__(self, item):
        return "getattr"

class Managed(Parent):  # ➎ 托管类,使用各个描述符类的一个实例。

    def __init__(self):
        self.__dict__['over'] = "obj.over"
        self.__dict__['over_no_get'] = "obj.over_no_get"
        self.__dict__['non_over'] = "obj.non_over"

    over_no_get = OverridingNoGet()
    over_no_get_cls = OverridingNoGet()
    attr_cls = 'attr_cls'
    non_over = NonOverriding()

    def spam(self):  # ➏ 方法也是描述符,下一节会介绍
        print('-> Managed.spam({})'.format(display(self)))


dd = Managed()
print(dd.over)                        # -> Overriding.__get__(<Overriding object>, <Managed object>, <class Managed>)
print(dd.over_no_get)            # obj.over_no_get
print(dd.over_no_get_cls)    # <__main__.OverridingNoGet object at 0x10d0ca320>
print(dd.attr_cls)                # attr_cls
print(dd.non_over)                # obj.non_over
print(dd.none)                        # getattr

方法是非数据描述符

参考:Functions and Methods

在类中定义的函数属于绑定方法类型(bound method)一种可调用类型,因为用户定义的函数都有 __get__ 方法,但没有__set__方法,所以依附到类上时,就相当于非数据描述符

class Function(object):

    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return types.MethodType(self, obj)

s

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

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

    def __get__(self, obj, objtype=None):
        return self.f

s

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

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

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

z

class Managed:  # ➎ 托管类,使用各个描述符类的一个实例

    #over = Overriding()
    #over_no_get = OverridingNoGet()
    #non_over = NonOverriding()

    def spam(self):  # ➏ 方法也是描述符,下一节会介绍
        print('-> Managed.spam({})'.format(display(self)))

o = Managed()
print(o.spam) # ➊
# <bound method Managed.spam of <__main__.Managed object at 0x10909f160>>
print(Managed.spam) # ❷ 
# <function Managed.spam at 0x109097730>

➊ obj.spam 获取的是绑定方法对象。

❷ 但是 Managed.spam 获取的是函数。

深入理解这种机制

import collections

class Text(collections.UserString):
    def __repr__(self):
        return 'Text({!r})'.format(self.data)

    def reverse(self):
        return self[::-1]
word = Text('forward')

print(word) #➊
# Text('forward')

print(word.reverse()) #➋
# Text('drawrof')

print(Text.reverse(word)) # ➌等价于➋,显示地传入self参数:word对象
# Text('drawrof')

#与上面一样,显示地传入self参数:各种可迭代对象
list(map(Text.reverse, ['repaid', (10, 20, 30), Text('stressed')]))
# ['diaper', (30, 20, 10), Text('desserts')]

type(word.reverse),type(Text.reverse) # ➍
# (<class 'function'>, <class 'method'>)

Text.reverse.__get__(word) # ➏ 等价于 word.reverse
# <bound method Text.reverse of Text('forward')>

Text.reverse.__get__(None, Text) # ➐ 等价于 Text.reverse
# <function Text.reverse at 0x101244e18>

word.reverse.__self__ # ➑
# Text('forward')

word.reverse.__func__ is Text.reverse # ➒ 
# True

word.reverse.__call__ # ➓ 
# <method-wrapper '__call__' of method object at 0x100aae988>

❽ 绑定方法对象有个 __self__ 属性,其值是调用这个方法的实例引用

➒ 绑定方法的 __func__ 属性是依附在托管类上那个原始函数的引用

类中的方法第一个参数是 self 的秘密➓

与描述符一样,

通过托管类访问时,函数的 __get__ 方法会返回自身的引用。但是,

通过实例访问时,函数的 __get__ 方法返回的是绑定方法对象:一种可调用的对象,➓ 有个 __call__ 方法,用于处理真正的调用过程。这个方法会调用 __func__ 属性引用的原始函数,把函数的第一个参数设为绑定方法的 __self__ 属性。

接口

Python - 协议和鸭子类型

鸭子类型(duck typing)

​ 忽略对象的真正类型,转而关注对象有没有实现所需 的方法、签名和语义

​ 多态的一种形式,在这种形式中,不管对象属于哪个类,也不管声明的具体接口是什 么,只要对象实现了相应的方法,函数就可以在对象上执行操作。

​ 对 Python 来说,这基本上是指避免使用 isinstance 检查对象的类型(更别提 type(foo) is bar 这种更糟的检查方式了

由来

  1. When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.——James Whitcomb Riley
  2. 不关注对象的类型,而关注对象的行为(方法)。它的行为是鸭子的行为,那么可以认为它是鸭子。

协议

  1. 在面向对象编程中,协议是非正式的接口,是一组方法,但只是一种文档,语言不对施加特定的措施或者强制实现。
  2. 虽然协议是非正式的,在Python中,应该把协议当成正式的接口。
  3. Python中存在多种协议,用于实现鸭子类型(对象的类型无关紧要,只要实现了特定的协议(一组方法)即可)
  4. 需要成为相对应的鸭子类型,那就实现相关的协议,即相关的__method__。例如实现序列协议(__len____getitem__),这个类就表现得像序列。
  5. 协议是正式的,没有强制力,可以根据具体场景实现一个具体协议的一部分。例如,为了支持迭代,只需实现__getitem__,不需要实现__len__
  6. 在Python文档中,如果看到“文件类对象“(表现得像文件的对象),通常说的就是协议,这个对象就是鸭子类型。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。”

猴子补丁:在运行时修 改类或模块,而不改动源码。

抽象基类

Alex Martelli 的警告:不要自己定义抽象基类,除非你要构建允许 用户扩展的框架。日常使用中,我们与抽象基类的联系应 该是创建现有抽象基类的子类,或者使用现有的抽象基类注册。

  • 声明抽象基类最简单的方式是继承 abc.ABC 或其他抽象基类

abc.ABC 是 Python 3.4 新增的类,如果你使用的是旧版 Python,那么无法继承现有的抽象基类。此时,必须在 class 语句中使用 metaclass= 关键字,把值设为 abc.ABCMeta(不是 abc.ABC)

# 这是Python < 3.4
class Tombola(metaclass=abc.ABCMeta):
     ...

在 Python 2 中必须使用 __metaclass__ 类 属性:

# 这是Python 2 !!! 
class Tombola(object): 
    __metaclass__ = abc.ABCMeta
  • 抽象方法使用 @abstractmethod 装饰器标记,而且定义体中通常只有文档字符串

除了 @abstractmethod 之外,abc 模块还定义了 @abstractclassmethod、@abstractstaticmethod 和 @abstractproperty 三个装饰器。然而,后三个装饰器从 Python 3.3 起废弃了,因为装饰器可以在 @abstractmethod 上堆叠,abstractmethod() 应该放在最里层,声明抽象类方法的推荐方式是:

class MyABC(abc.ABC):
    @classmethod 
    @abc.abstractmethod 
    def an_abstract_classmethod(cls, ...):
        pass

继承抽象基类

具体子类

必须实现全部的抽象方法

class B(Abstract):
     '''实现全部抽象函数'''
虚拟子类

注册虚拟子类的方式是在抽象基类上调用 register 方法。这么做之后,注册的类会变成 抽象基类的虚拟子类,而且 issubclass 和 isinstance 等函数都能识别,但是注册的类 不会从抽象基类中继承任何方法或属性

register 方法通常作为普通的函数调用(参见 11.9 节),不过也可以作为装饰器使用。

@Tombola.register   # Tombola是抽象基类
class TomboList(list):
    pass

# Python 3.3 或之前的版本,不能把 .register 当作类装饰器使用,必须使用标准的调用句法。
Tombola.register(TomboList)

虚拟子类不会继承注册的抽象基类,而且任何时候都不会检查它是否符合抽 象基类的接口,即便在实例化时也不会检查。为了避免运行时错误,虚拟子类要实现 所需的全部方法。(不明白)TODO

异常

python异常之间的层次结构:Exception hierarchy

image-20190408221552773

元类编程

类工厂函数

通常,我们把 type 视作函数,因为我们像函数那样使用它,例如,调用 type(obj) 获取对象所属的类——作用与 obj.__class__ 相同。然而,type 是一个类。当成类使用时,传入三个参数可以新建一个类:

MyClass = type('MyClass', (MySuperClass, MyMixin), {'x': 42, 'x2': lambda self: self.x * 2})

type 的三个参数分别是 namebasesdict。最后一个参数是一个映射,指定新类的属性名和值。上述代码的作用与下述代码相同:

class MyClass(MySuperClass, MyMixin):
    x = 42

    def x2(self):
        return self.x * 2

因此尝试创建一个类似于 namedtuple 的类工厂

def record_factory(cls_name, field_names):
    try:
        field_names = field_names.replace(',', ' ').split()
    except AttributeError:  # 不能调用.replace或.split方法
        pass  # 假定field_names本就是标识符组成的序列 field_names = tuple(field_names) ➋

    # 下面3个定义的函数是给新类的引用
    def __init__(self, *args, **kwargs):  # ➌
        attrs = dict(zip(self.__slots__, args)) # 位置参数 依次对应 到 slots 中的属性
        attrs.update(kwargs) # 关键字参数直接加入 attrs 中,关键字必须在 slots 中
        for name, value in attrs.items():
            setattr(self, name, value)

    def __iter__(self):  # ➍
        for name in self.__slots__:
            yield getattr(self, name)

    def __repr__(self):  # ➎
        values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__, self))
        return '{}({})'.format(self.__class__.__name__, values)

    # 组建类属性
    cls_attrs = dict(__slots__=field_names,
                     __init__=__init__,
                     __iter__=__iter__,
                     __repr__=__repr__)
        # 创建新的类
    return type(cls_name, (object,), cls_attrs)

record_factory 函数创建的类,其实例有个局限——不能序列化,如果想了解完整的方案,请分析 collections.nameduple 函数的源码(https://hg.python.org/cpython/file/3.4/Lib/collections/__init__.py#l236),搜索“pickling”这个词。

定制描述符的类装饰器

一旦组建好整个类,而且把描述符绑定到类属性上之后,我们就可以审查类,并为描述符设置合理的储存属性名称。因此我们要在创建类时设置储存属性的名称,使用类装饰器或元类可以做到这一点。我们首先使用较简单的方式。

类装饰器与函数装饰器非常类似,是参数为类对象的函数,返回原来的类或修改后的类

def entity(cls): #➊ cls 是类对象
    for key, attr in cls.__dict__.items(): # ➋ 遍历类属性
        if isinstance(attr, Validated): # ➌ 对 Validated 描述符的实例处理
            desc = attr # 对描述符进行处理
            type_name = type(desc).__name__
            desc.storage_name = '_{}#{}'.format(type_name, key)
    return cls

@entity  
class LineItem:
      description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

类装饰器有个重大缺点:只对直接依附的类有效。这意味着,被装饰的类的子类可能继承 也可能不继承装饰器所做的改动,具体情况视改动的方式而定。

因此需要元类。

导入时与运行时

在导入时,解释器会从上到下一次性解析完 .py 模块的源码,然后生成用于执行的字节码。如果句法有错误,就在此时报告。如果本地的 __pycache__ 文件夹中有最新的 .pyc 文件,解释器会跳过上述步骤。

import 语句,它不只是声明,在进程中首次导入模块时,还会运行所导入模块中的全部顶层代码——以后导入相同的模块则使用缓存,只做名称绑定

顶层代码可以做任何事,包括通常在“运行时”做的事,如:连接数据库

  • 解释器会编译函数的定义体(首次导入模块时),把函数对象绑定到对应的全局名称上,但是显然解释器 不会执行函数的定义体

  • 解释器会执行每个类的定义体,甚至会执行嵌套类的定义体,说明类的定义体属于”顶层代码”

接下来分析一下实际的import 与运行的情况

# evalsupport.py

print('<[100]> evalsupport module start')

def deco_alpha(cls):
    print('<[200]> deco_alpha')

    def inner_1(self):
        print('<[300]> deco_alpha:inner_1')

    cls.method_y = inner_1
    return cls


class MetaAleph(type):
    print('<[400]> MetaAleph body')

    def __init__(cls, name, bases, dic):
        print('<[500]> MetaAleph.__init__')

        def inner_2(self):
            print('<[600]> MetaAleph.__init__:inner_2')

        cls.method_z = inner_2

print('<[700]> evalsupport module end')
# evaltime.py
from evalsupport import deco_alpha

print('<[1]> evaltime module start')

class ClassOne():
    print('<[2]> ClassOne body')

    def __init__(self):
        print('<[3]> ClassOne.__init__')

    def __del__(self):
        print('<[4]> ClassOne.__del__')

    def method_x(self):
        print('<[5]> ClassOne.method_x')

    class ClassTwo(object):
        print('<[6]> ClassTwo body')


@deco_alpha
class ClassThree():
    print('<[7]> ClassThree body')

    def method_y(self):
        print('<[8]> ClassThree.method_y')


class ClassFour(ClassThree):
    print('<[9]> ClassFour body')

    def method_y(self):
        print('<[10]> ClassFour.method_y')

if __name__ == '__main__':
    print('<[11]> ClassOne tests', 30 * '.')
    one = ClassOne()
    one.method_x()
    print('<[12]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[13]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()

print('<[14]> evaltime module end')

导入时代码的运行结果

>>> import evaltime
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body # 每个类的定义体都执行了
<[6]> ClassTwo body # 包括嵌套的类
<[7]> ClassThree body
<[200]> deco_alpha  # 解释器先计算类3的定义体,然后调用依附在类上的装饰器函数
<[9]> ClassFour body
<[14]> evaltime module end

运行时的运行结果,顺序与 import 时一样,只不过是增加了 main 函数中的执行结果

$ python3 evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end

元类

如果想定制整个类层次结构,而不是一次只定制一个类,使用元类更高效

元类是制造类的工厂,不过不是函数,而是类。元类是生产机器的机器。

根据 Python 对象模型,类是对象,因此类肯定是另外某个类的实例。默认情况下,Python 中的类是 type 类的实例,为了避免无限回溯,type 是其自身的实例

元类与类之间关系

object 类和 type 类之间的关系很独特:object类 是 type 的实例,而 type 是 object 的子类。这种关系很“神奇”,无法使用 Python 代码表述,因为定义其中一个之前另一个必须存在。type 是自身的实例这一点也很神奇。

所有类(包括元类)都直接或间接地是 type 的实例, 不过只有元类同时也是 type 的子类。若想理解元类,一定要知道这种关系:元类(如 ABCMeta)从 type 类继承了构建类的能力。

元类的入口: init方法

就元类本身而言,它们其实是很简单的:

1) 拦截类的创建

2) 修改类(在元类的__init__中)

3) 返回修改之后的类

在使用元类时,只要通过关键字metaclass=元类,那么在类对象cls加载(创建)完后,就会调用元类的__init__方法,对cls进行改造,而类装饰器是通过将 cls 直接传给函数,然后在函数中进行修改。

元类可以通过实现 __init__ 方法定制实例, __init__ 方法可以做到类装饰器能做的任何事情,但是作用更大。

__init__ 方法有四个参数,后面3个参数元 type 的一样:

  • cls   要修改操作的类对象引用
  • name 类的名字
  • bases 类的超类
  • dic   类的属性字典

元类支持继承:ClassSix 类没有直接引用 MetaAleph 类,但是却受到了影响,因为它是 ClassFive 的子类,进而也是 MetaAleph 类的实例,所以由 MetaAleph.__init__ 方法初始化。

# evalsupport.py

print('<[100]> evalsupport module start')

def deco_alpha(cls):
    print('<[200]> deco_alpha')

    def inner_1(self):
        print('<[300]> deco_alpha:inner_1')

    cls.method_y = inner_1
    return cls


class MetaAleph(type):
    print('<[400]> MetaAleph body')

    def __init__(cls, name, bases, dic):
        print('<[500]> MetaAleph.__init__')

        def inner_2(self):
            print('<[600]> MetaAleph.__init__:inner_2')

        cls.method_z = inner_2

print('<[700]> evalsupport module end')
# evaltime_meta.pyt

from evalsupport import deco_alpha
from evalsupport import MetaAleph

print('<[1]> evaltime_meta module start')

@deco_alpha
class ClassThree():
    print('<[2]> ClassThree body')

    def method_y(self):
        print('<[3]> ClassThree.method_y')


class ClassFour(ClassThree):
    print('<[4]> ClassFour body')

    def method_y(self):
        print('<[5]> ClassFour.method_y')


class ClassFive(metaclass=MetaAleph):
    print('<[6]> ClassFive body')

    def __init__(self):
        print('<[7]> ClassFive.__init__')

    def method_z(self):
        print('<[8]> ClassFive.method_z')


class ClassSix(ClassFive):
    print('<[9]> ClassSix body')

    def method_z(self):
        print('<[10]> ClassSix.method_z')


if __name__ == '__main__':
    print('<[11]> ClassThree tests', 30 * '.')
    three = ClassThree()
    three.method_y()
    print('<[12]> ClassFour tests', 30 * '.')
    four = ClassFour()
    four.method_y()
    print('<[13]> ClassFive tests', 30 * '.')
    five = ClassFive()
    five.method_z()
    print('<[14]> ClassSix tests', 30 * '.')
    six = ClassSix()
    six.method_z()

print('<[15]> evaltime_meta module end')
$python3 evaltime_meta.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime_meta module start
<[2]> ClassThree body
<[200]> deco_alpha
<[4]> ClassFour body
<[6]> ClassFive body
<[500]> MetaAleph.__init__ # 创建 ClassFive 时调用了 MetaAleph.__init__ 
<[9]> ClassSix body
<[500]> MetaAleph.__init__ # 创建 ClassSix 时也调用了 MetaAleph.__init__
<[11]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1
<[12]> ClassFour tests ..............................
<[5]> ClassFour.method_y
<[13]> ClassFive tests ..............................
<[7]> ClassFive.__init__
<[600]> MetaAleph.__init__:inner_2
<[14]> ClassSix tests ..............................
<[7]> ClassFive.__init__
<[600]> MetaAleph.__init__:inner_2
<[15]> evaltime_meta module end

注意,ClassSix 类没有直接引用 MetaAleph 类,但是却受到了影响,因为它是 ClassFive 的子类,进而也是 MetaAleph 类的实例,所以由 MetaAleph.init 方法初始化。

定制描述符的元类

如果用户完全不用知道描述符或元类,直接继承库提供的类就能满足需求,那该多好

class EntityMeta(type):
    """元类,用于创建带有验证字段的业务实体"""

    def __init__(cls, name, bases, attr_dict):
        super().__init__(name, bases, attr_dict)
        for key, attr in cls.__dict__.items():  # ➋ 遍历类属性
            if isinstance(attr, Validated):  # ➌ 对 Validated 描述符的实例处理
                desc = attr  # 对描述符进行处理
                type_name = type(desc).__name__
                desc.storage_name = '_{}#{}'.format(type_name, key)


class Entity(metaclass=EntityMeta):
    """带有验证字段的业务实体"""

这样,用户直接继承 Entity 类即可,完全不需要考虑复杂的处理,甚至不用知道元类的存在

class LineItem(Entity):
      description = NonBlank()
    weight = Quantity()
    price = Quantity()

    def __init__(self, description, weight, price):
        self.description = description
        self.weight = weight
        self.price = price

    def subtotal(self):
        return self.weight * self.price

__prepare__方法

这个特殊方法只在元类中有用,而且必须声明为类方法

在某些应用中,可能需要知道类的属性定义的顺序。在默认情况下,名称到属性的映射是无序字典dict;元类或类装饰器获得映射时,属性在类定义体中的顺序已经丢失了。

If the metaclass has no __prepare__ attribute, then the class namespace is initialised as an empty ordered mapping.(官网:默认是有序的了嘛?)

解决办法:Python 3 引入了特殊方法 __prepare__ (用 @classmethod 装饰器定义),返回一个 OrderedDict 对象,用于储存名称到属性的映射。这样就保留了要构建的类在定义体中绑定属性的顺序。

在调用 __new____init__方法之前,由解释器调用 __prepare__ 方法获得 OrderedDict 对象,将其传给元类的 __new____init__ 方法的 attr_dict

image-20190425110329118

特殊属性

dir(...) 函数不会列出本节提到的任何一个属性

参见标准库参考中“Built-in Types”一章的“4.13. Special Attributes”一节。 其中三个属性在本书中已经见过多次:__mro____class____name__。此外,还有以下属性。

  1. cls.__bases__:由类的基类组成的元组。
  2. cls.__qualname__:Python 3.3 新引入的属性,其值是类或函数的限定名称,即从模块的全局作用域到类的点分路径。例如,ClassOne的内部类 ClassTwo 的 __qualname__ 属性,其值是字符串 ‘ClassOne.ClassTwo‘,而 __name__ 属性的值是 ‘ClassTwo’
  3. cls.__subclasses__():这个方法返回一个列表,列表中是内存里现存的子类,包含类的直接子类。这个方法的实现使用弱引用,防止在超类和子类(子类在 __bases__ 属性中储存指向超类的强引用)之间出现循环引用。
  4. cls.mro():构建类时,如果需要获取储存在类属性 __mro__ 中的超类元组,解释器会调用这个方法。元类可以覆盖这个方法,定制要构建的类解析方法的顺序。

类的创建过程

默认情况下,类通过type()函数创建,类的定义体在一个新的命名空间中执行,类名被绑定为type(name, bases, namespace)的结果

当类的定义代码被执行了以后,会进行以下步骤:

  • MRO entries are resolved;
  • the appropriate metaclass is determined;
  • the class namespace is prepared;
  • the class body is executed;
  • the class object is created.

想要自定义 类的创建过程,可以在定义类时,通过 metaclass参数指定元类,或者继承自有 metalclass 参数的父类,如下:MyClass 和 MySubclass 都是 元类 Meta 的实例

# both MyClass and MySubclass are instances of Meta:
class Meta(type):
    pass

class MyClass(metaclass=Meta):
    pass

class MySubclass(MyClass):
    pass

1.处理 MRO 名单

Resolving MRO entries 不明白

If a base that appears in class definition is not an instance of type, then an __mro_entries__ method is searched on it. If found, it is called with the original bases tuple. This method must return a tuple of classes that will be used instead of this base. The tuple may be empty, in such case the original base is ignored.

2.确定合适的元类

  1. 没有基类和指定的元类,那么就是默认的 type 元类

  2. 有指定元类,并且不是 type()的实例,那么直接用它作为元类

  3. type()的实例被指定为元类,或者指定了基类,那么使用the most derived metaclass

    most derived metaclass 是从显示指定的元类(if any)和所有基类的元类中选出来的。most derived metaclass 是这些候选元类的子类型(subtype),如果不满足该条件,抛出TypeError

3.准备类的命名空间

合适元类决定下来后,就开始准备命名空间。

如果元类有__prepare__()属性(方法),那么命名空间就由该函数的返回值确定,namespace =metaclass.__prepare__(name, bases, **kwds)

kwds 是来自类定义时的参数(if any)

如果没有__prepare__()属性(方法),那么命名空间就初始化为空的有序字典(empty ordered mapping)

4.执行类的定义体

类定义体 会以exec(body, globals(), namespace)的方式执行,与普通调用exec()的关键不同是:当类的定义是在函数中时,lexical scoping 允许类定义体引用当前范围与外部范围的名字(不是很明白)

5.创建出类对象

执行类主体填充类命名空间后,通过调用metaclass(name, bases, namespace, **kwds)来创建 类对象,也就会调用 metaclass .__init__()

类对象会被super()所引用,在类中,如果有方法使用了__class__super__class__就会成为一个隐式的闭包。

当使用默认的元类type,或者最终调用type.__new__的其他元类,在创建类对象后,下面这些额外可自定义的步骤会被调用

  • first, type.__new__ collects all of the descriptors in the class namespace that define a __set_name__() method;
  • second, all of these __set_name__ methods are called with the class being defined and the assigned name of that particular descriptor;
  • finally, the __init_subclass__() hook is called on the immediate parent of the new class in its method resolution order.

在类对象创建完成后,如果有类装饰器的话,就会被传到其中,返回的新的类对象才会被绑定到本地的命名空间中,成为定义的类

类属性访问的优先级

参考:http://www.betterprogramming.com/object-attribute-lookup-in-python.html

Class “C” 是 Metaclass “M”的实例 , 调用 C.name 会进行下面的步骤:

  • 获取类的Metaclass (Get the Metaclass from Class)
  • 调用Metaclass的特殊方法 __getattribute__

    __getattribute__的内部逻辑:

def __getattribute__():

    meta_attr= None # 元类属性

        for meta_parent in Metaclass.__mor__:
                if meta_parent.__dict__['name'] is not None:
                        meta_attr = meta_parent.__dict__['name']

                        if meta_attr is data descriptor: 
                                return meta_attr.__get__()    # 返回元类的数据描述符的值
                        break

    # 在继承链中查找,只要找到就返回,
    for class_parent in Class.__mro__:
          if class_parent.__dict__['name'] is not None:
              cls_attr = class_parent.__dict__['name']
              if cls_attr is data/non-data descriptor: 
                  return cls_attr.__get__()    # 获取描述符的值
            return cls_attr    #返回普通的值

    # 返回元类的属性
    if meta_attr is not None:
          if meta_attr is non-data descriptor:
              return meta_attr.__get__()
        return meta_attr

        raise an AttributeError # 都没有找到,抛出异常

if MetaClass has the special method __getattr__:
  return MetaClass.__getattr__()

结语

元类是很复杂的。对于非常简单的类,你可能不希望通过使用元类来对类做修改。你可以通过其他两种技术来修改类:

1) Monkey patching

2) class decorators

当你需要动态修改类时,99%的时间里你最好使用上面这两种技术。当然了,其实在99%的时间里你根本就不需要动态修改类

重载

Python 施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:

  • 不能重载内置类型的运算符

  • 不能新建运算符,只能重载现有的

  • 某些运算符不能重载——is、and、or 和 not(不过位运算符 &、| 和 ~ 可以)

一元运算符

只需实现相应的特殊方法。这些特殊方法只有一个参数:self,始终返回一个新对象,也就是说,不能修改self

-(__neg__):一元取负算术运算符。如果 x 是 -2,那么 -x == 2。

+(__pos__):一元取正算术运算符。通常,x == +x,但也有一些例外。如果好奇,请阅读“x 和 +x 何时不相等”附注栏。

~(__invert__):对整数按位取反,定义为 ~x == -(x+1)。如果 x 是 2,那么 ~x == -3。

def __abs__(self):
    return math.sqrt(sum(x * x for x in self))

def __neg__(self):
    return Vector(-x for x in self)

def __pos__(self):
    return Vector(self)

x 和 +x 何时不相等

第一例与 decimal.Decimal 类有关,

x 是 Decimal 实例,在算术运算的上下文中创建,然后在不同的上下文中计算 +x,那么 x != +x

import decimal

ctx = decimal.getcontext()

ctx.prec = 40 # 设置上下文精度
one_third = decimal.Decimal('1') / decimal.Decimal('3')
# Decimal('0.3333333333333333333333333333333333333333')
one_third == +one_third ➎
# True

ctx.prec = 28 # 修改精度,不同上下文
one_third == +one_third ➐
# False
+one_third
# Decimal('0.3333333333333333333333333333')

第二例与 collections.Counter 类有关

Counter 相加时,负值和零值计数会从结果中剔除。而一元运算符 + 等同于加上一个空 Counter,因此它产生一个新的 Counter 且仅保留大于零的计数器

ct = Counter('abracadabra')
# Counter({'a': 5, 'r': 2, 'b': 2, 'd': 1, 'c': 1})

ct['r'] = -3
ct['d'] = 0

ct
# Counter({'a': 5, 'b': 2, 'c': 1, 'd': 0, 'r': -3})

+ct 
# Counter({'a': 5, 'b': 2, 'c': 1})

二元运算符

实现一元运算符和中缀运算符的特殊方法一定不能修改操作数__add__返回一个新 Vector 实例,而没有影响 self 或 other。

二元运算的反向(不同类型之间的运算)

为了支持不同类型之间的运算,Python 为中缀运算符特殊方法提供了特殊的分派机制:

image-20190410095307180

对表达式 a + b 来说,解释器会执行以下几步操作(见图)

  1. 如果 a 有__add__ 方法,而且返回值不是 NotImplemented,调用 a.__add__(b), 然后返回结果。

  2. 如果 a 没有__add__方法,或者调用 __add__ 方法返回 NotImplemented,检查 b 有没有__radd 方法,如果有,而且没有返回 NotImplemented,调用 `b.radd__(a)`,然后返回结果。

    __radd____add__ 的反向”(reversed)版本或者“反射”(reflected)版本

  3. 如果 b 没有 __radd__ 方法,或者调用 __radd__ 方法返回 NotImplemented,抛出 TypeError,解释器会在错误消息中指明操作数类型不支持:TypeError: unsupported operand type(s) for +: 'Class1' and 'Class2'

别把 NotImplemented 和 NotImplementedError 搞混了。

前者是特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它return给解释器,让解释器尝试对调操作数,调用运算符的反向特殊方法(如__radd__ )。

NotImplementedError 是一种异常,抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖

反向方法一般只需委托正向的方法即可

如果中缀运算符的正向方法(如 __mul__)只处理与 self 属于同 一类型的操作数,那就无需实现对应的反向方法(如__rmul__),因为按照定义, 反向方法是为了处理类型不同的操作数。

操作数类型不同时的异常处理

考虑一下可能发生的错误(Vector 代表向量):

  1. 如果提供的对象不可迭代(Vector([1,2]) + 1),那么 __add__ 就无法处理,而且提供的错误消息不是很有用。

  2. 如果操作数是可迭代对象(Vector([1,2]) + 'ABC'),但是它的元素不能与 Vector 中的浮点数元素相加,给出的消息也没什么用

如果操作数的类型不同,我们要检测出不能处理的操作数。两种方式可以处理这个问题:

  1. 鸭子类型,直接尝试执行运算,如果有问题,捕获 TypeError 异常;
  2. 另一种是显式使用 isinstance 测试,__mul__ 方法就是这么做的。

这两种方式各有优缺点:鸭子类型更灵活,但是显式检查更能预知结果。如果选择使用isinstance,要小心,不能测试具体类,而要测试抽象基类,比如:numbers.Real

方法1:为了遵守鸭子类型精神,我们不能测试 other 操作数的类型,或者它的元素的类型,我们要捕获异常,然后返回 NotImplemented

# 最终版本
def __add__(self, other):
    try:
        pairs = itertools.zip_longest(self, other, fillvalue=0.0)
        return Vector(a + b for a, b in pairs)
    except TypeError:
        return NotImplemented

def __radd__(self, other):
    return self + other

方法2:白鹅类型。我们将使用 isinstance() 检查 scalar 的类型,但是不硬编码具体的类型,而是检查 numbers.Real 抽象基类。

# 最终版本
import numbers

def __mul__(self, scalar):
    if isinstance(scalar, numbers.Real): 
        return Vector(n * scalar for n in self)
    else: 
        return NotImplemented

def __rmul__(self, scalar):
     return self * scalar

比较运算符号

  • 正向方法返回NotImplemented的话, 调用反向方法

  • 参数对调:对 == 来说,正向和反向调用都是__eq__方法,只是把参数对调了;正向的 __gt__ 方法调用的是反向的 __lt__方法,并把参数对调

  • 后备机制:对 ==!= 来说,如果反向调用失败,Python有后备机制, 会比较对象的 ID,而不抛出 TypeError

  • != 运算符我们不用实现它:因为从 object 继承的 __ne__ 方法的后备行为满 足了我们的需求:定义了 __eq__ 方法,而且它不返回 NotImplemented__ne__ 会对__eq__返回的结果取反。

image-20190410142104597

注意类型检查

对操作数过度宽容可能导致令人惊讶的结果,因此,我们要保守一点,做些类型检查。

def __eq__(self, other):
    # 如果other操作数是Vector实例或子类的实例,那就像之前那样比较。
    if isinstance(other, Vector):
        return (len(self) == len(other) 
                and all(a == b for a, b in zip(self, other)))
    else:
        return NotImplemented

增量赋值运算符(就地方法)

  • 就地运算符(+=,*=)用于增量赋值

  • 如果一个类没有实现表中列出的就地运算符,增量赋值运算符只是语法糖:a += b 的 作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了 __add__ 方法的话,不用编写额外的代码,+= 就能使用。

  • 增量赋值不会修改不可变类型,而是新建实例,然后重新绑定
  • 可变类型实现了就地运算符方法,会就地修改左操作数,而不会创建新对象作为结果,必须返回 self

image-20190410141306137

例子

import itertools
from tombola import Tombola
from bingo import BingoCage

class AddableBingoCage(BingoCage):

    def __add__(self, other):
        if isinstance(other, Tombola):
            return AddableBingoCage(self.inspect() + other.inspect())
        else:
            return NotImplemented

    def __iadd__(self, other):
        # 是否同类型
        if isinstance(other, Tombola):
            other_iterable = other.inspect()
                # 尝试迭代 other
        else:
            try:
                other_iterable = iter(other)
            except TypeError:
                self_cls = type(self).__name__
                msg = "right operand in += must be {!r} or an iterable"
                raise TypeError(msg.format(self_cls))
        self.load(other_iterable)
        return self

在实现的过程中,我们得知在可接受的类型方面,+ 应该比 += 严格。对序列类型来说,+ 通常要求两个操作数属于同一类型,而 += 的右操作数往往可以是任何可迭代对象。

上下文管理器和 else 块

else

不过与if/else差别很大,使用then关键字更好理解

总结:如果异常或者 return、break 或 continue 语句导致控制权跳到了复合语句的主块之外,else 子句也会被跳过。

  • for:仅当 for 循环运行完毕时(即 for 循环没有被 break 语句中止)才运行 else 块。
  • while:仅当 while 循环因为条件为假值而退出时(即 while 循环没有被 break 语句中止) 才运行 else 块。
  • try :仅当 try 块中没有异常抛出时才运行 else 块。官方文档还指出:“else 子句抛出的异常不会由前面的 except 子句处理。”
for item in my_list:
    if item.flavor == 'banana':
        break 
else:
     raise ValueError('No banana flavor found!')

下面有个疑问:有必要用 else吗?(TODO),直接写 after_call()可以吗?

try:
    dangerous_call()
except OSError:
    log('OSError...')
else:
    after_call()

with与上下文管理器

  • 上下文管理器协议包含 __enter____exit__ 两个方法。

  • 在定义类时,含有__enter____exit__方法的对象就是上下文管理器。

  • with语句的目的是简化try/finally模式。当with遇到上下文管理器,就会在执行语句体之前,先执行上下文管理器的__enter__方法,然后再执行语句体,执行完语句体后,最后执行__exit__方法,以此扮演 finally子句的角色

    这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常、return语句或sys.exit()调用而中止,也会执行指定的操作

class ContextManager(object):
    def __init__(self):
        print('实例化一个对象')

    def __enter__(self):
        print('进入')
        # return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('退出')
        # return True

# with 上下文管理器:
#     语句体
obj = ContextManager()
with obj:
    print('正在执行')

# 实例化一个对象
# 进入
# 正在执行
# 退出

与函数和模块不同,with 块没有定义新的作用域,退出后变量还可以使用。

enter 与 exit 细节

with 上下文管理器 as target:
    代码语句体

参数

  • 解释器调用 __enter__ 方法时,除了隐式的 self 之外,不会传入任何参数。

  • 传给 __exit__ 方法的三个参数列举如下:

    • exc_type:异常类(例如 ZeroDivisionError)
    • exc_value:异常实例。有时会有参数传给异常构造方法,例如错误消息,这些参数可以使用 exc_value.args 获取。
    • traceback:traceback 对象

    如果一切正常,Python 调用 __exit__ 方法时传入的参数是 None, None, None;如果抛出了异常,这三个参数是异常数据

返回值

  • 如果使用了as,则是把上下文管理器的 __enter__() 方法的返回值赋值给 target,target 可以是单个变量,或者由“()”括起来的元组(不能是仅仅由“,”分隔的变量列表,必须加“()”),__enter__ 方法除了返回上下文管理器之外,还可能返回其他对象。

  • 出现异常时,__exit__ 返回 False(默认不写返回值时,即为False),则会重新抛出异常,让with之外的语句逻辑来处理异常,这也是通用做法;如果返回True,则忽略异常,不再对异常进行处理

请看下面的具体例子:

class LookingGlass:
    def __enter__(self):
        import sys
        self.original_write = sys.stdout.write
        sys.stdout.write = self.reverse_write # 替换标准输出
        return 'JABBERWOCKY'

    def reverse_write(self, text):
        self.original_write(text[::-1])

    def __exit__(self, exc_type, exc_value, traceback):
        import sys
        sys.stdout.write = self.original_write # 恢复标准输出
        if exc_type is ZeroDivisionError:
            print('Please DO NOT divide by zero!')
            return True

with LookingGlass() as what:
    print('Alice, Kitty and Snowdrop')
    print(what)

# pordwonS dna yttiK ,ecilA
# YKCOWREBBAJ

# with 块已经执行完毕。输出不再是反向的了。可以看出,__enter__ 方法返回的值——即存储在 what 变量中的值——是字符串'JABBERWOCKY'。
print(what) 
print('Back to normal.')
# 'JABBERWOCKY'
# Back to normal.

上下文管理器对象存在的目的是管理with语句,就像迭代器的存在是为了管理for语句一样。

使用@contextmanager

@contextmanager装饰器优雅且实用,把三个不同的 Python 特性结合到了一起:函数装 饰器、生成器和 with 语句。

用法形式

在使用 @contextmanager 装饰的生成器中,yield 语句的作用是把函数的定义体分成两部分:

  1. yield 语句前面的所有代码在 with开始时(即解释器调用 __enter__ 方法时)执行
  2. yield 语句后面的代码在 with结束时(即调用 __exit__ 方法时)执行

在 @contextmanager 装饰器装饰的生成器中,yield 与迭代没有任何关系

原理

其实,contextlib.contextmanager 装饰器会把函数包装成实现 __enter____exit__ 方法的类。

类的名称是 _GeneratorContextManager。具体的工作方式,可以阅读 Python 3.4 发行版中 Lib/contextlib.py

这个类的 __enter__ 方法有如下作用:

  1. 调用生成器函数,保存生成器对象(这里把它称为 gen)
  2. 调用 next(gen),执行到 yield 关键字所在的位置
  3. 返回 next(gen) 产出的值,以便把产出的值绑定到 with/as 语句中的目标变量上。

    with 块终止时,__exit__ 方法会做以下几件事:

  4. 检查with有没有发生异常,有没有把异常传给 exc_type;如果有,调用 gen.throw(exception),在生成器函数定义体中包含 yield 关键字的那一行抛出异常,但是,如果那里没有处理错误的代码,可能出现无法避免的错误(具体见下面的例子)

  5. 否则,调用 next(gen),继续执行生成器函数定义体中 yield 语句之后的代码。
import contextlib

@contextlib.contextmanager
def looking_glass():
    import sys
    original_write = sys.stdout.write

    def reverse_write(text):
        original_write(text[::-1])

    sys.stdout.write = reverse_write
    msg = ''

    # 使用 try 的原因如下面的解释:
    try:
        yield 'JABBERWOCKY'
    except ZeroDivisionError:
        msg = 'Please DO NOT divide by zero!'
    finally:
        sys.stdout.write = original_write
        if msg:
            print(msg)

'''测试'''
with looking_glass() as what:
    print('Alice, Kitty and Snowdrop')
    print(what)
    raise ZeroDivisionError # 模拟在 with 块中抛出异常

# pordwonS dna yttiK ,ecilA
# YKCOWREBBAJ

print(what)
# 'JABBERWOCKY'

如果在 with 块中抛出了异常,Python 解释器会将其捕获, 然后在 looking_glass 函数的 yield 表达式里再次抛出。但是,如果那里没有处理错误的代码, looking_glass 函数会中止,永远无法恢复成原来的 sys.stdout.write 方法,导致系统处于无效状态。

默认返回值

使用 @contextmanager 装饰器时,默认的行为是返回 True,与上下文管理器默认返回False不同:装饰器提供的 __exit__方法假定发给生成器的所有异常都得到处理了,因此应该压制异常。 如果不想让 @contextmanager 压制异常,必须在被装饰的函数中显式重新抛出异常。

有意思的两个例子

上下文管理器有两种写法,下面一种是使用装饰器的方式,实现了一个计时器。

要点:

  1. 使用contextmanager装饰器
  2. 在函数中使用yield进入with语句块
from time import time
from contextlib import contextmanager

@contextmanager
def timer():
    print("进入'with'语句块 之前,进行初始化工作...")

    start = time()
    print(f'start:{start}')

    print("马上就进入'with'语句块...")

    yield   # 进入 with 语句块,执行其内容 

    print("退出 'with' 语句块 之后,进行收尾工作...")

    print(f'end:{time()}, cost time: {time() - start}')

def do_something():
    xx = []
    for i in range(10000):
        for j in range(10000):
            xx.append(j)
        xx.clear()

if __name__ == '__main__':
    with timer() as t:
        do_something()

再来一个通过实现接口定义类的方式,实现了一个文本缩进。

要点:

  1. 实现__enter__,__exit__ 方法
  2. 可以重复使用同一个上下文
class Indeter:
    def __init__(self):
        self.level = -1

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.level -= 1

    def print(self, text):
        print("\t"*self.level+text)


if __name__=="__main__":
    with Indeter() as idt:
        idt.print('第一章')
        with idt:
            idt.print('第一节')
            with idt:
                idt.print('第一段')

协程

协同程序,协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值。

从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。

但是在协程中,yield 通常出现在表达式的右边(例如,datum = yield data),可以产出值,也可以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None

协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是 next(…) 函数。通常,调用方会把值推送给协程。

def simple_coroutine():
    print('-> coroutine started')
    x = yield    # 需要调用 next 后,才会停在这里
    print('-> coroutine received:', x)

my_coro = simple_coroutine()

print(my_coro)
# ➌ <generator object simple_coroutine at 0x100c2be10>

#预先激活
next(my_coro) 
# -> coroutine started

#发送数据
my_coro.send(42) #
# -> coroutine received: 42
# 运行到结束,抛出 stopIteration
# Traceback (most recent call last):
#   ...
# StopIteration

协程状态

协程可以身处四个状态中的一个。当前状态可以使用 inspect.getgeneratorstate(...) 函数确定:

  1. ‘GEN_CREATED’:等待开始执行,通过对其调用next()进入下一个状态

  2. ‘GEN_RUNNING’ :解释器正在执行

    只有在多线程应用中才能看到这个状态。此外,生成器对象在自己身上调用 getgeneratorstate 函数也行,不过这样做没什么用。

  3. ‘GEN_SUSPENDED’ :在 yield 表达式处暂停

  4. ‘GEN_CLOSED’ :执行结束

协程在 yield 关键字所在的位置暂停执行,在赋值语句中,= 右边的代码在赋值之前执行。因此,对于 b = yield a 这行代码来说,等到客户端代码 再激活协程时才会设定 b 的值。具体看下面的图示:

def simple_coro2(a):
    print('-> Started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)

from inspect import getgeneratorstate

my_coro2 = simple_coro2(14)
print(getgeneratorstate(my_coro2))
# 'GEN_CREATED'

print(next(my_coro2)) #预激,进入下一个状态
# -> Started: a = 14
# 14

print(getgeneratorstate(my_coro2))
# 'GEN_SUSPENDED'

my_coro2.send(28)
# -> Received: b = 28
42

my_coro2.send(99)
# -> Received: c = 99
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# StopIteration

print(getgeneratorstate(my_coro2))
# 'GEN_CLOSED'

image-20190412104610202

预激协程的装饰器

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average #➊
        total += term
        count += 1
        average = total/count

coro_avg = averager()

next(coro_avg)# 需要通过next()进行预激,让代码运行到➊处
print(coro_avg.send(10))
# 10.0
print(coro_avg.send(30))
# 20.0
print(coro_avg.send(5))
# 15.0

使用协程之前必须预激,可是这一步容易忘记。为了避免忘记,可以在协程上使用一个特殊的装饰器

from functools import wraps

def coroutine(func):
    """装饰器:向前执行到第一个`yield`表达式,预激`func`"""
    @wraps(func)
    def primer(*args,**kwargs):
        gen = func(*args,**kwargs)
        next(gen) # 预激
        return gen
    return primer


@coroutine
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

'''测试'''
from inspect import getgeneratorstate
coro_avg = averager()

getgeneratorstate(coro_avg)
# 'GEN_SUSPENDED'

coro_avg.send(10)
# 10.0
coro_avg.send(30)
# 20.0
coro_avg.send(5)
# 15.0

终止协程和异常处理

协程中未处理的异常会向上冒泡,传给 next 函数或 send 方法的调用方(即触发协程的对象)

在协程内没有处理异常,协程会终止。如果试图重新激活协程,会抛出 StopIteration 异常。

从 Python 2.5 开始,客户代码可以在生成器对象上调用两个方法,显式地把异常发给协程。

  • generator.throw(exc_type[, exc_value[, traceback]])

    致使生成器在【暂停的 yield 表达式处】抛出指定的异常。

    如果生成器处理了抛出的异常,代码会向前执行到下一个 yield 表达式,而产出的值会成为throw 方法的返回值。

    如果生成器没有处理抛出的异常,异常会向上冒泡,传到调用方的上下文中。

  • generator.close()

    致使生成器在【暂停的 yield 表达式处】抛出 GeneratorExit 异常,协程正常关闭

    如果生成器没有处理这个异常,或者抛出了 StopIteration 异常(通常是指运行到结尾),调用方不会报错。

    收到 GeneratorExit 异常后,生成器一定不能产出值,否则解释器会抛出 RuntimeError 异常。生成器抛出的其他异常会向上冒泡,传给调用方。

class DemoException(Exception):
    """为这次演示定义的异常类型。"""
    pass

def demo_exc_handling():
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing...')
        else:
            print('-> coroutine received: {!r}'.format(x))
    raise RuntimeError('This line should never run.')

exc_coro = demo_exc_handling()
next(exc_coro)
# -> coroutine started

exc_coro.send(11)
# -> coroutine received: 11

from inspect import getgeneratorstate

# 1. 正常 close
exc_coro.close()
print(getgeneratorstate(exc_coro))
# 'GEN_CLOSED'

# 2. 抛出可处理的异常,协程继续
exc_coro.throw(DemoException)
# *** DemoException handled. Continuing...
print(getgeneratorstate(exc_coro))
# GEN_SUSPENDED

# 3. 抛出不可处理的异常,协程关闭,异常冒泡
exc_coro.throw(ZeroDivisionError)
# Traceback (most recent call last):
# ZeroDivisionError
print(getgeneratorstate(exc_coro))
# 'GEN_CLOSED'

协程的返回值

在 Python 3.3 之前,如果生成器返回值,解释器会报句法错误。

return 表达式的值会传给调用方,赋值给 StopIteration 异常的一个属 性。这样做有点不合常理,但是能保留生成器对象的常规行为——耗尽时抛出 StopIteration 异常。

from collections import namedtuple

Result = namedtuple('Result', 'count average')

def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield  # 取消了term = yield average 形式
        if term is None:
            break     # 通过传入 None 跳出并返回 Result
        total += term
        count += 1
        average = total/count
    return Result(count, average)

coro_avg = averager()
next(coro_avg) # 预激
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(6.5)
# 获取返回值
try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value

print(result)
# Result(count=3, average=15.5)

yield from

概念与功能

使用 yield from 句法调用协程时,会自动预激

yield from x 表达式对 x 对象所做的第一件事是,调用 iter(x),从中获取迭代器。因此,x 可以是任何可迭代的对象。(替代产出值的嵌套 for 循环)

yield from主要功能是打开双向通道把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产出值,还可以直接传入异常,而不用在位于中间的协程中添加大量处理异常的样板代码。有了这个结构,协程可以通过以前不可能的方式委托职责。

术语概念

image-20190412145617283

委派生成器在 yield from 表达式处暂停时, 调用方可以直接把数据发给子 生成器, 子生成器再把产出的值发给调用方。 子生成器返回之后, 解释器会抛出 StopIteration 异常, 并把返回值附加到异常对象上, 此时委派生成器会恢复

  • 委派生成器:包含 yield from <iterable> 表达式的生成器函数。

  • 子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器。

    这就是 PEP 380 的标题 (“Syntax for Delegating to a Subgenerator”)中所说的“子生成器”(subgenerator)。

  • 调用方 :PEP 380 指代调用委派生成器的客户端代码。

    在不同的语境中,我会使用“客户端”代替“调用方”,以此与委派生成器(也是调用方,因为它调用了子生成器)区分开。

在生成器 gen 中使用 yield from subgen() 时,子生成器 subgen 会获得控制权,把产出的值传给 gen 的调用方,即调用方可以直接控制 subgen。与此同时,gen 会阻塞,等待 subgen终止返回

subgen 返回的值会成为 yield from 表达式的值yield from 会处理解释器会抛出 StopIteration 异常,获取附加到异常对象上的返回值, 此时委派生成器会恢复

子生成器 subgen 可能是简单的迭代器,只实现了 __next__ 方法,yield from 也能处理这种子生成器。不过,引入 yield from 结构的目的是为了支持实现了 __next__sendclosethrow 方法的生成器。

from collections import namedtuple
Result = namedtuple('Result', 'count average')

# 子生成器
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield
        if term is None:
            break
        total += term
        count += 1
        average = total / count
    return Result(count, average) # 返回的值会成为 yield from 表达式的值

# 委派生成器
def grouper(results, key):
    # 这是个协程,运行结束后会抛出 StopIteration,因此 True 是让它永不结束,然后在➊处覆盖实例,让解释器垃圾回收即可,不用担心无限循环,后面有个不用 True 的版本
    while True:  
        results[key] = yield from averager() 
        # yield from 会处理 解释器会抛出 StopIteration 异常,获取附加到异常对象上的返回值, 此时委派生成器会恢复

# 客户端代码,即调用方
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key) # ➊ group 是个生成器
        next(group)
        for value in values:
            group.send(value)
        group.send(None)  # 重要!

    # print(results) # 如果要调试,去掉注释
    report(results)


# 输出报告
def report(results):
    for key, result in sorted(results.items()):
        group, unit = key.split(';')
        print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))


data = {
    'girls;kg':
        [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m':
        [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg':
        [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m':
        [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}

if __name__ == '__main__':
    main(data)

不用 True 的版本


# 委派生成器
def grouper(results, key):
    # while True:
    #     print("start")
    #     results[key] = yield from averager()
    #     print("end")
    #     print(results)
    results[key] = yield from averager()
    # 收到 None 后,委托生成器恢复运行
    print("end") 
    # 然后该协程会抛出 StopIteration

# 客户端代码,即调用方
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)
        next(group)
        for value in values:
            group.send(value)
        try:
            group.send(None)  # 重要!
        except StopIteration: # 处理委托生成器的异常
            pass

yield from 的行为

  • 子生成器产出的值都直接传给委派生成器的调用方(即客户端代码)。

  • 调用方使用 send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是 None,那么会调用子生成器的 __next__() 方法。如果发送的值不是 None,那么会调用子生成器的 send() 方法。如果调用的方法抛出 StopIteration 异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

  • 生成器退出时,生成器(或子生成器)中的 return expr 表达式会触发 StopIteration(expr) 异常抛出。
  • yield from 表达式的值是子生成器终止时传给 StopIteration 异常的第一个参数

yield from 结构的另外两个特性与异常和终止有关。

  • 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的 throw() 方法。如果调用 throw() 方法时抛出 StopIteration 异常,委派生成器恢复运 行。StopIteration 之外的异常会向上冒泡,传给委派生成器。

  • 如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用 close() 方 法,那么在子生成器上调用 close() 方法,如果它有的话。如果调用 close() 方法 导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

伪代码模拟yield from 的行为

伪代码,等效于委派生成器中的 RESULT = yield from EXPR 语句

_i = iter(EXPR)  # ➊ EXPR 可以是任何可迭代的对象
try:
    _y = next(_i)  # ➋ 预激子生成器
except StopIteration as _e:
    _r = _e.value  # ➌
else:
    while True:  # ➍
        try:
            _s = yield _y  # ➎
        # ➏ 这一部分用于关闭委派生成器和子生成器。
        except GeneratorExit as _e:  
           # 因为子生成器可以是任何可迭代的对象, 所以可能没有 close 方法。
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            raise _e
        # 这一部分处理调用方通过 .throw(...) 方法传入的异常
        except BaseException as _e: 
            _x = sys.exc_info()
            try:
                _m = _i.throw
            except AttributeError:  # ➐如果子生成器可以是迭代器,从而没有 throw 方法可调用
                raise _e
            else:  # ➑ 如果子生成器有 throw 方法,调用它并传入调用方发来的异常。
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                    _r = _e.value
                    break
        else:  # ➒ 如果产出值时没有异常
            try:  # ➓ 尝试让子生成器向前执行
                if _s is None:  # ⓫
                    _y = next(_i)
                else:
                    _y = _i.send(_s)
            except StopIteration as _e:  # ⓬ 子生成器抛出 StopIteration 异常,获取 value 属性的值,赋值给 _r,然后退 出循环,让委派生成器恢复运行。
                _r = _e.value
                break
RESULT = _r

内建模块

collections

namedtuple
Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(1, 2)
>>> p.x
1

namedtuple可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用,使用十分方便。

deque

deque是为了高效实现插入和删除操作的双向列表,适合用于队列和栈。

defaultdict

如果希望key不存在时,返回一个默认值,就可以用defaultdictdd = defaultdict(lambda: 'N/A') 默认值是调用函数返回的,而函数在创建defaultdict对象时传入。

OrderedDict

OrderedDict的Key会按照插入的顺序排列,不是Key本身排序

ChainMap

ChainMap可以把多个dict组成一个逻辑上的dictChainMap本身也是一个dict,但是查找的时候,会按照顺序在内部的dict依次查找。

# 举个例子:应用程序往往都需要传入参数,参数可以通过命令行传入,可以通过环境变量传入,还可以有默认参数。
# 我们可以用ChainMap实现参数的优先级查找,即先查命令行参数,如果没有传入,再查环境变量,如果没有,就使用默认参数。

from collections import ChainMap
import os, argparse

# 构造缺省参数:
defaults = {
    'color': 'red',
    'user': 'guest'
}

# 构造命令行参数:
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = { k: v for k, v in vars(namespace).items() if v }

# 组合成ChainMap: 依次从这3个字典里查找是否有指定的字段,优先级依次递减
combined = ChainMap(command_line_args, os.environ, defaults)

# 打印参数:
print('color=%s' % combined['color'])
print('user=%s' % combined['user'])
Counter

常用的就是items()k-v对, keys()键, values()值 ,elements()打印全部的 k(出现几次就重复几次)

from collections import Counter
s=['asdf','asd','asdf','sdf']
c=Counter(s)
print(c)
for k,v in c.items():
    print(k,v)
print("-----")
for i in c.elements():
    print(i)
print("-----")
for i in c.keys():
    print(i)
print("-----")
for i in c.values():
    print(i)

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431953239820157155d21c494e5786fce303f3018c86000

itertools

可以参考流畅的 python14.9中的标准函数库,里面有详细的介绍

reduce

reduce 函数时最好提供第三个参数,reduce(function, iterable, initializer),这样能避免这个异常:TypeError: reduce() of empty sequence with no initial value(这个错误消息很棒,说明了问题,还提供了解决方法)。如果序列为空,initializer 是返回的结果;

zip

zip 函数的名字取自拉链系结物(zipper fastener),因为这个物品用于把 两个拉链边的链牙咬合在一起,与文件压缩没有关系。

itertools.zip_longest 函数的行为有所不同:使用可选的 fillvalue(默认 值为 None)填充缺失的值,因此可以继续产出,直到最长的可迭代对象耗尽。

all

如果所有分量对的比较结果都是 True,那么结果就是 True。只要有一次比较的结果是 False,all 函数就返回 False。

return len(self) == len(other) and all(a == b for a, b in zip(self, other))

可迭代的归约函数

image-20190411102548312

表中的函数都接受一个可迭代的对象(iterable),然后返回单个结果。这些函数叫“归约”函数(“合拢”函数或“累加”函数)

这里列出的每个内置函数都可以使用 functools.reduce 函数实现

但是对 allany 函数来说,有一项重要的优化措施是 reduce 函数做不到的:这两个函数会短路(一旦确定了结果就立即停止使用迭代器)

# 短路现象
g = (n for n in [0, 0.0, 7, 8])
any(g) 
# True
next(g) 
# 8

sorted 和这些归约函数只能处理最终会停止的可迭代对象

contextlib

自己定义上下文管理器类之前,先看一下 Python 标准库文档中的“29.6 contextlib Utilities for with-statement contexts“。contextlib 模块中有一些类和其他函数,使用范围广泛。

奇淫巧技

长度为1的切片,是一个元素还是一个列表?

s[0] == s[:1] 只对 str 这个序列类 型成立。不过,str 类型的这个行为十分罕见。对其他各个序列类型来说,s[i]返 回一个元素,而 s[i:i+1] 返回一个相同类型的序列,里面是 s[i] 元素。

True False测试

默认情况,一个对象都会被认为是 true ,除非定义的 __bool__() 返回 False 或者 __len__() 返回 0 , 下面是常见的内建对象返回 False 的情况:

  • constants defined to be false: None and False.
  • zero of any numeric type: 0, 0.0, 0j, Decimal(0), Fraction(0, 1)
  • empty sequences and collections: '', (), [], {}, set(), range(0)

在 Python 中,很多情况下类和函数可以互换

这不仅是因为 Python 没有 new 运算符,还因为有特殊的 __new__ 方法,可以把类变成工厂方法,生成不同类型的对象,或者返回事先构建好的实例,而不是每次都创建一个新实例。

命名

我编写的每个类都使用驼峰式名称

其他

语言类型

强类型和弱类型语言

  如果一门语言很少隐式转换类型,说明它是强类型语言;如果经常这么做,说明 它是弱类型语言。Java、C++ 和 Python 是强类型语言。PHP、JavaScript 和 Perl 是弱类型语言。

Python 不会自动在字符串和数字之间强制转换

静态类型和动态类型

编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言。静态类型需要声明类型(有些现代语言使用类型推导避免部分类型声明)。

静态类型使得一些工具(编译器和 IDE)便于分析代码、找出错误和提供其他服务 (优化、重构,等等)。动态类型便于代码重用,代码行数更少,而且能让接口自然 成为协议而不提早实行。

综上,Python 是动态强类型语言

必须使用 self 访问实例属性也备受批评。 我自己并不介意输入 self 限定符,这样便于把局部变量和属性区分开

评论