Skip to content

Latest commit

 

History

History
553 lines (377 loc) · 21.3 KB

File metadata and controls

553 lines (377 loc) · 21.3 KB

1、 内置类型dict的__init__和__update__方法会忽略我们覆盖的__setitem__方法

>>> class DoppelDict(dict):
...     def __setitem__(self, key, value):
...             super().__setitem__(key, [value]*2) #1
... 
>>> 
>>> dd = DoppelDict(one=1)  #2
>>> dd 
{'one': 1}
>>> dd['two'] = 2   #3
>>> dd
{'one': 1, 'two': [2, 2]}
>>> dd.update(three=3)  #4
>>> dd
{'one': 1, 'two': [2, 2], 'three': 3}

❶ DoppelDict.__setitem__方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。

❷ 继承自dict的__init__方法显然忽略了我们覆盖的__setitem__方法:'one'的值没有重复。

❸ []运算符会调用我们覆盖的__setitem__方法,按预期那样工作:'two'对应的是两个重复的值,即[2, 2]。

❹ 继承自dict的update方法也不使用我们覆盖的__setitem__方法:'three'的值没有重复。

2、 dict.update方法会忽略 AnswerDict.__getitem__方法

>>> class AnswerDict(dict):
...     def __getitem__(self, key): #1
...             return 42
... 
>>> ad = AnswerDict(a='foo')  #2
>>> ad['a']  #3
42
>>> d = {}
>>> d.update(ad) #4
>>> d['a']
'foo'
>>> d
{'a': 'foo'}

❶ DoppelDict.__setitem__方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。

❷ 继承自dict的__init__方法显然忽略了我们覆盖的__setitem__方法:'one'的值没有重复。

❸ []运算符会调用我们覆盖的__setitem__方法,按预期那样工作:'two'对应的是两个重复的值,即[2, 2]。

❹ 继承自dict的update方法也不使用我们覆盖的__setitem__方法:'three'的值没有重复。

直接子类化内置类型(如dict、list或str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承collections模块中的类,例如UserDict、UserList和UserString,这些类做了特殊设计,因此易于扩展。

3、子类化collections.UserDict

>>> import collections
>>> class DoppelDict2(collections.UserDict):
...     def __setitem__(self, key, value):
...             super().__setitem__(key, [value] * 2)
... 
>>> dd = DoppelDict2(one=1)
>>> dd
{'one': [1, 1]}
>>> dd['two'] = 2
>>> dd
{'one': [1, 1], 'two': [2, 2]}
>>> dd.update(three=3)
>>> dd
{'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}
>>> 
>>> class AnswerDict2(collections.UserDict):
...     def __getitem__(self, key):
...             return 42
... 
>>> ad = AnswerDict2(a='foo')
>>> ad['a']
42
>>> d={}
>>> d.update(ad)
>>> d
{'a': 42}

使用collections.UserDict直接解决上面1,2问题。

4、多重继承和方法解析顺序

任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”。

(左)说明“菱形问题”的UML类图;(右)虚线箭头是示例使用的方法解析顺序

class A:
    def ping(self):
        print("A_ping: ", self)

class B(A):
    def pong(self):
        print("B_pong: ", self)

class C(A):
    def pong(self):
        print("C_pong: ", self)

class D(B, C):
    def ping(self):
        super().ping()
        print("post-ping: ", self)
    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)
>>> from diamond import *
>>> d = D()
>>> d.pong()  #1
B_pong:  <diamond.D object at 0x7f386ed7c358>
>>> C.pong(d) #2
C_pong:  <diamond.D object at 0x7f386ed7c358>
>>> d.ping() #1
A_ping:  <diamond.D object at 0x7f386ed7c358>  #2 
post-ping:  <diamond.D object at 0x7f386ed7c358> #3

❶ 直接调用d.pong( )运行的是B类中的版本。

❷ 超类中的方法都可以直接调用,此时要把实例作为显式参数传入。

❶ D类的ping方法做了两次调用。

❷ 第一个调用是super( ).ping( );super函数把ping调用委托给A类;这一行由A.ping输出。

❸ 第二个调用是print('post-ping:', self),输出的是这一行。

5、 __mro__解析顺序

Python能区分d.pong( )调用的是哪个方法,是因为Python会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为__mro__的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到object类。D类的__mro__属性如下

>>> D.__mro__
(<class 'diamond.D'>, <class 'diamond.B'>, <class 'diamond.C'>, <class 'diamond.A'>, <class 'object'>)

若想把方法调用委托给超类,推荐的方式是使用内置的super( )函数。在Python 3中,这种方式变得更容易了,如示例D类的pingpong方法所示。[插图]然而,有时可能需要绕过方法解析顺序,直接调用某个超类的方法——这样做有时更方便。

>>> d.pingpong()
A_ping:  <diamond.D object at 0x7f386ed7c358> #1
post-ping:  <diamond.D object at 0x7f386ed7c358> 
A_ping:  <diamond.D object at 0x7f386ed7c358> #2
B_pong:  <diamond.D object at 0x7f386ed7c358> #3
B_pong:  <diamond.D object at 0x7f386ed7c358> #4
C_pong:  <diamond.D object at 0x7f386ed7c358> #5

❶ 第一个调用是self.ping( ),运行的是D类的ping方法,输出这一行和下一行。

❷ 第二个调用是super( ).ping( ),跳过D类的ping方法,找到A类的ping方法。

❸ 第三个调用是self.pong( ),根据__mro__ ,找到的是B类实现的pong方法。

❹ 第四个调用是super( ).pong( ),也根据__mro__ ,找到B类实现的pong方法。

➎第五个调用是C.pong(self),忽略mro ,找到的是C类实现的pong方法。

方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果在diamond.py文件中把D类声明为class D(C, B):,那么D类的__mro__属性就会不一样:先搜索C类,再搜索B类。

6、多层继承

把接口继承和实现继承区分开

使用多重继承时,一定要明确一开始为什么创建子类。主要原因可能有:

继承接口,创建子类型,实现“是什么”关系

继承实现,通过重用避免代码重复

其实这两条经常同时出现,不过只要可能,一定要明确意图。通过继承重用代码是实现细节,通常可以换用组合和委托模式。而接口继承则是框架的支柱。

使用抽象基类显式表示接口

现代的Python中,如果类的作用是定义接口,应该明确把它定义为抽象基类。Python 3.4及以上的版本中,我们要创建abc.ABC或其他抽象基类的子类。

通过混入重用代码

如果一个类的作用是为多个不相关的子类提供方法实现,从而实现重用,但不体现“是什么”关系,应该把那个类明确地定义为混入类(mixin class)。从概念上讲,混入不定义新类型,只是打包方法,便于重用。混入类绝对不能实例化,而且具体类不能只继承混入类。混入类应该提供某方面的特定行为,只实现少量关系非常紧密的方法。

在名称中明确指明混入

因为在Python中没有把类声明为混入的正规方式,所以强烈推荐在名称中加入...Mixin后缀。

抽象基类可以作为混入,反过来则不成立

抽象基类可以实现具体方法,因此也可以作为混入使用。不过,抽象基类会定义类型,而混入做不到。此外,抽象基类可以作为其他类的唯一基类,而混入决不能作为唯一的超类,除非继承另一个更具体的混入——真实的代码很少这样做。

抽象基类有个局限是混入没有的:抽象基类中实现的具体方法只能与抽象基类及其超类中的方法协作。这表明,抽象基类中的具体方法只是一种便利措施,因为这些方法所做的一切,用户调用抽象基类中的其他方法也能做到。

不要子类化多个具体类

具体类可以没有,或最多只有一个具体超类。[插图]也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入。

为用户提供聚合类

如果抽象基类或混入的组合对客户代码非常有用,那就提供一个类,使用易于理解的方式把它们结合起来。

__mro__类属性中蕴藏的方法解析顺序,有了这一机制,继承方法的名称不再会发生冲突。我们还提到,内置的super( )函数会按照__mro__属性给出的顺序调用超类的方法。

大多数程序员编写应用程序而不开发框架。即便是开发框架的那些人,多数时候(或大多数时候)也是在编写应用程序。编写应用程序时,我们通常不用设计类的层次结构。我们至多会编写子类、继承抽象基类或框架提供的其他类。作为应用程序开发者,我们极少需要编写作为其他类的超类的类。我们自己编写的类几乎都是末端类(即继承树的叶子)。

7、 运算符重载的基础

在某些圈子中,运算符重载的名声并不好。这个语言特性可能(已经)被滥用,让程序员困惑,导致缺陷和意料之外的性能瓶颈。但是,如果使用得当,API会变得好用,代码会变得易于阅读。Python施加了一些限制,做好了灵活性、可用性和安全性方面的平衡:

不能重载内置类型的运算符

不能新建运算符,只能重载现有的

某些运算符不能重载——is、and、or和not(不过位运算符&、|和~可以)

8、一元运算符

-(__neg__)

一元取负算术运算符。如果x是-2,那么-x==2。

+(__pos__)

一元取正算术运算符。通常,x==+x,但也有一些例外。如果好奇,请阅读“x和+x何时不相等”附注栏。

~(__invert__)

对整数按位取反,定义为x==-(x+1)。如果x是2,那么x==-3。

支持一元运算符很简单,只需实现相应的特殊方法。这些特殊方法只有一个参数,self。然后,使用符合所在类的逻辑实现。不过,要遵守运算符的一个基本规则:始终返回一个新对象。也就是说,不能修改self,要创建并返回合适类型的新实例。

9、x和+x何时不相等

算术运算上下文的精度变化可能导致x不等于+x

>>> import decimal
>>> ctx = decimal.getcontext()  #1
>>> ctx.prec = 40 #2
>>> one_third = decimal.Decimal('1')/decimal.Decimal('3') #3
>>> one_third #4
Decimal('0.3333333333333333333333333333333333333333')
>>> one_third == +one_third #5
True
>>> ctx.prec = 28  #6
>>> one_third == +one_third   #7
False
>>> one_third
Decimal('0.3333333333333333333333333333333333333333')
>>> +one_third #8
Decimal('0.3333333333333333333333333333')

❶ 获取当前全局算术运算的上下文引用。

❷ 把算术运算上下文的精度设为40。

❸ 使用当前精度计算1/3。

❹ 查看结果,小数点后有40个数字。

❺ one_third==+one_third返回True。

❻ 把精度降低为28,这是Python 3.4为Decimal算术运算设定的默认精度。

❼ 现在,one_third==+one_third返回False。

❽ 查看+one_third,小数点后有28个数字。

虽然每个+one_third表达式都会使用one_third的值创建一个新Decimal实例,但是会使用当前算术运算上下文的精度。

10、重载向量加法运算符+

涉及不同类型的运算,Python为中缀运算符特殊方法提供了特殊的分派机制。对表达式a+b来说,解释器会执行以下几步操作

(1)如果a有__add__方法,而且返回值不是NotImplemented,调用a.__add__(b),然后返回结果。

(2)如果a没有__add__方法,或者调用__add__方法返回NotImplemented,检查b有没有__radd__方法,如果有,而且没有返回NotImplemented,调用b._radd_(a),然后返回结果。

(3)如果b没有__radd__方法,或者调用__radd__方法返回NotImplemented,抛出TypeError,并在错误消息中指明操作数类型不支持。

使用__add__和__radd__计算a+b的流程图

__radd__是__add__的“反射”(reflected)版本或“反向”(reversed)版本。我喜欢把它叫作“反向”特殊方法。称之为“右向”(right)特殊方法,因为他们在右操作数上调用。不管你喜欢哪个以“r”开头的单词, __radd__和__rsub__等类似方法中的“r”就是这个意思。

这是一种后备机制,如果左操作数没有实现__add__方法,或者实现了,但是返回NotImplemented表明它不知道如何处理右操作数,那么Python会调用__radd__方法。

别把NotImplemented和NotImplementedError搞混了。前者是特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回(return)给解释器。而NotImplementedError是一种异常,抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖。

__radd__通常就这么简单:直接调用适当的运算符,在这里就是委托__add__。任何可交换的运算符都能这么做。处理数字和向量时,+可以交换,但是拼接序列时不行。

在Python中,所有集合都可以迭代。

在Python语言内部,迭代器用于支持:

for循环

构建和扩展集合类型

逐行遍历文本文件

列表推导、字典推导和集合推导

元组拆包

调用函数时,使用*拆包实参

11、序列可以迭代的原因:iter函数

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

(1)检查对象是否实现了__iter__方法,如果实现了就调用它,获取一个迭代器。

(2)如果没有实现__iter__方法,但是实现了__getitem__方法,Python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素。

(3)如果尝试失败,Python抛出TypeError异常,通常会提示“C object is not iterable”(C对象不可迭代),其中C是目标对象所属的类。

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

第一版 序列

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 __getitem__(self, index):
        return self.words[index]

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

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

使用如下:

>>> from sentence import Sentence
>>> s = Sentence('"The time has come," the Walrus said,')
>>> s
Sentence('"The time ha... Walrus said,')
>>> for word in s:
...     print(word)
... 
The
time
has
come
the
Walrus
said
>>> list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
>>> s[0]
'The'
>>> s[5]
'Walrus'
>>> s[-1]
'said'

12、可迭代的对象

使用iter内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代;实现了__getitem__方法,而且其参数是从零开始的索引,这种对象也可以迭代。

我们要明确可迭代的对象和迭代器之间的关系:Python从可迭代的对象中获取迭代器。

StopIteration异常表明迭代器到头了。Python语言内部会处理for循环和其他迭代上下文(如列表推导、元组拆包,等等)中的StopIteration异常。

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

__next__

返回下一个可用的元素,如果没有元素了,抛出StopIteration异常。

__iter__

返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。

因为迭代器只需__next__和__iter__两个方法,所以除了调用next( )方法,以及捕获StopIteration异常之外,没有办法检查是否还有遗留的元素。此外,也没有办法“还原”迭代器。如果想再次迭代,那就要调用iter(...),传入之前构建迭代器的可迭代对象。传入迭代器本身没用,因为前面说过Iterator.__iter__方法的实现方式是返回实例本身,所以传入迭代器无法还原已经耗尽的迭代器。

迭代器

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

构建可迭代的对象和迭代器时经常会出现错误,原因是混淆了二者。要知道,可迭代的对象有个__iter__方法,每次都实例化一个新的迭代器;而迭代器要实现__next__方法,返回单个元素,此外还要实现__iter__方法,返回迭代器本身。

迭代器模式可用来:

访问一个聚合对象的内容而无需暴露它的内部表示

支持对聚合对象的多种遍历

为遍历不同的聚合结构提供一个统一的接口(即支持多态迭代)

可迭代的对象一定不能是自身的迭代器。也就是说,可迭代的对象必须实现__iter__方法,但不能实现__next__方法。

另一方面,迭代器应该一直可以迭代。迭代器的__iter__方法应该返回自身。

第二版 使用迭代器模式

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

使用如下:

>>> from sentence_iter import Sentence
>>> s = Sentence('"The time has come," the Walrus said,')
>>> s
Sentence('"The time ha... Walrus said,')
>>> for word in s:
...     print(word)
... 
The
time
has
come
the
Walrus
said
>>> list(s)
['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']

13、生成器

只要Python函数的定义体中有yield关键字,该函数就是生成器函数。调用生成器函数时,会返回一个生成器对象。也就是说,生成器函数是生成器工厂。

生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给next(...)函数时,生成器函数会向前,执行函数定义体中的下一个yield语句,返回产出的值,并在函数定义体的当前位置暂停。最终,函数的定义体返回时,外层的生成器对象会抛出StopIteration异常——这一点与迭代器协议一致。

第三版 生成器实现

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

re.finditer函数是re.findall函数的惰性版本,返回的不是列表,而是一个生成器,按需生成re.MatchObject实例。如果有很多匹配,re.finditer函数能节省大量内存。

第四版 惰性实现

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

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

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

    def __iter__(self):
        for match in RE_WORD.finditer(self.text):
            yield match.group()

第五版 生成器表达式

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

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

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

    def __iter__(self):
        return (match.group() for match in RE_WORD.finditer(self.text))

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

生成器表达式是创建生成器的简洁句法,这样无需先定义函数再调用。不过,生成器函数灵活得多,可以使用多个语句实现复杂的逻辑,也可以作为协程使用。

如果生成器表达式要分成多行写,我倾向于定义生成器函数,以便提高可读性。此外,生成器函数有名称,因此可以重用。

如果函数或构造方法只有一个参数,传入生成器表达式时不用写一对调用函数的括号,再写一对括号围住生成器表达式,只写一对括号就行了。然而,如果生成器表达式后面还有其他参数,那么必须使用括号围住,否则会抛出SyntaxError异常: