流畅的python 阅读笔记

文章目录

第一部分 序幕

第二部分 数据结构

第三部分 把函数视作对象

第五章 函数:一等对象

在python中,函数是一等对象,暨满足下述条件:

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

在python中,整数,字符串,和字典都是一等对象

5.1 把函数视作对象

创建一个对象,然后调用它,读取它的__doc__属性,并且确定函数对象本身是function类的实例

  1. 函数是运行时创建的
  2. __doc__方法用来返回函数中的帮助文本
  3. 函数是function类的实例
def foo(n):
    """
    return n
    :param n:
    :return:
    """
    return f'num:{n}'


if __name__ == '__main__':
    print(foo(2))
    print('-'*50)
    print(foo.__doc__)
    print('-' * 50)
    print(type(foo))
num:2
--------------------------------------------------

    return n
    :param n:
    :return:
    
--------------------------------------------------
<class 'function'>

我们可以将函数foo赋值给变量f,然后通过变量名调用,我们也可以把它作为参数传给map函数,

def foo(n):
    """
    return n
    :param n:
    :return:
    """
    return f'num:{n}'


if __name__ == '__main__':
    print(foo(2))
    print('-'*50)
    print(foo.__doc__)
    print('-' * 50)
    print(type(foo))
    print('-' * 50)
    f = foo
    print(f(2))
    print('-' * 50)
    # print(map(foo, range(10)))
	print(list(map(foo, range(10))))
C:\DEV\fluent_python\venv\Scripts\python.exe "C:\DEV\fluent_python\第三部分 把函数视作对象\5.1 把函数视作对象.py" 
num:2
--------------------------------------------------

    return n
    :param n:
    :return:
    
--------------------------------------------------
<class 'function'>
--------------------------------------------------
num:2
--------------------------------------------------
['num:0', 'num:1', 'num:2', 'num:3', 'num:4', 'num:5', 'num:6', 'num:7', 'num:8', 'num:9']

5.2 高阶函数

接受函数为参数,或者把函数作为结果返回的函数是高阶函数,map函数就是一个高阶函数,sorted()也是高阶函数

l = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
l = sorted(l, key=len)
print(l)

map,filter,reduce`的现代替代品

函数式语言通常提供map,filter.reduce三个高阶函数,在python 3中,mapfilter还是内置函数,但由于引入了列表推导式和生成器表达式,他们变得没有那么必要了.(从python 3.0开始reduce不再是内置函数)

def foo(n):
    """
    return n
    :param n:
    :return:
    """
    return f'num:{n}'


print(list(map(foo, range(10))))
print([foo(i) for i in range(10)])

C:\DEV\fluent_python\venv\Scripts\python.exe "C:\DEV\fluent_python\第三部分 把函数视作对象\5.2 高阶函数.py" 
['num:0', 'num:1', 'num:2', 'num:3', 'num:4', 'num:5', 'num:6', 'num:7', 'num:8', 'num:9']
['num:0', 'num:1', 'num:2', 'num:3', 'num:4', 'num:5', 'num:6', 'num:7', 'num:8', 'num:9']

python 3mapfilter返回生成器,因此现在它们的直接替代品是生成器表达式

5.3 匿名函数

lambda关键字在python表达式内创建匿名函数

然而,python简单的语句限制了lambda函数的定义体只能使用纯表达式,换句话说,lambda函数定义体中不能赋值,也不能使用while,trypython语句

除了作为参数传给高阶函数外,python很少使用匿名函数,由于句法上的限制,非平凡的lambda表达式要么难以阅读,要么无法写出

lambda只是语法糖,与def一样,lambda表达式会创建函数对象,这是python中集中可调用对象的一种,下一节会说明所有可调用对象

5.4 可调用对象

除了用户定义的函数,调用运算符(即())还可以应用到其他对象上,如果像判断对象能否调用,可以使用内置的callable()函数,python数据模型文档列出了7中可调用对象

  • 用户定义的函数

    使用def语句或者lambda表达式创建

  • 内置函数

    使用C语言实现的函数,如lentime.strftime

  • 内置方法

    使用C语言实现的方法,如dict.get

  • 方法

    在类的定义体中定义的函数

  • 调用类时会运行类的__new__方法创建一个实例,然后运行__init__方法,初始化实例,最后把实力返回给调用方.因为python没有new运算符,所以调用类相当于调用函数.(通常,调用类会创建那个类的实例,不过覆盖__new__方法的话,也可能出现其他行为,19.1.3节会出现一个例子)

  • 类的实例

    如果定义了__call__方法,那么他的实例可以作为函数调用(5.5节)

  • 生成器函数

    使用yield关键字的函数或方法,调用生成器函数返回的是生成器对象

生成器在很多方面与其他可调用对象不同,生成器函数还可以作为协程(详见16章)

5.5 用户定义的可调用类型

不仅python函数是真正的对象,任何python对象都可以表现得像函数,为此,只需要实现实例方法__call__

实现一个BingoCage类,这个类的实例使用任何可迭代对象构建,而且会在内部存储一个随即顺序排列的列表,调用实例会取出一个元素

import random


class BingoCage:
    def __init__(self, items):
        self._items = list(items)
        random.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('空')

    def __call__(self, *args, **kwargs):
        return self.pick()


if __name__ == '__main__':
    b = BingoCage(range(10))
    for _ in range(11):
        print(b.pick())
  • __init__接受任何可迭代对象,在本地构建一个副本,防止列表参数的意外副作用(?)
  • random.shuffle()用于将一个列表中的元素顺序打乱,不会生成新列表,而是操作原列表
  • pick()方法用于执行取值操作,并在容器为空是抛出异常
  • b.pick()的快捷方法是b()

5.6 函数内省

内省:文言,意思是查看内部情况

函数内省:查看函数本身的内部属性

除了__doc__外,函数对象还有很多属性,使用dir函数可以探知函数具有下述属性,其中大部分属性是python对象共有的

def foo(n):
    return n


if __name__ == '__main__':
    print(foo.__dir__())
['__new__', '__repr__', '__call__', '__get__', '__closure__', '__doc__', '__globals__', '__module__', '__builtins__', '__code__', '__defaults__', '__kwdefaults__', '__annotations__', '__dict__', '__name__', '__qualname__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

我们重点说明下函数专有,而用户定义的一般对象没有的属性,通过计算两个属性集合的差集便能得到函数专有属性列表

class C: pass

def f(): pass

c = C()
print(sorted(set(dir(f)) - set(dir(c))))
['__annotations__', '__builtins__', '__call__', '__closure__', '__code__', '__defaults__', '__get__', '__globals__', '__kwdefaults__', '__name__', '__qualname__']
名称类型说明
__annotations__dict参数和返回值的注解
__builtins__dict对内建模块本身的引用,即__builtins__完全等价于builtins([builtins — 内建对象 — Python 3.11.3 文档](https://docs.python.org/zh-cn/3/library/builtins.html))
__call__method-wrapper实现()运算符,即可调用对象协议
__closure__tuple函数闭包,即自由变量的绑定(通常是None)
__code__code编译成字节码的函数元数据和函数定义体
__defaults__tuple形式参数的默认值
__get__method-wrapper实现只读描述符协议(20章)
__globals__dict函数负载模块中的全局变量
__kwdefaults__dict仅限关键字形式参数的默认值
__name__str函数名称
__qualname__str函数的限定名称

5.7 从定位参数到仅限关键字参数

python最好的特性之一,是提供了极为灵活的参数处理机制,而且python 3进一步提供了仅限关键字参数,调用函数时使用***展开可迭代对象,映射到单个参数

def tag(name, *content, cls=None, **attrs):
    # tag 函数用于生成HTML 标签;使用名为cls 的关键字参数传入“class”属性,这是一种变通方法,因为“class”是Python 的关键字
    if cls:
        attrs['class'] = cls
    if attrs:
        attrs_str = ''.join(f'{attr}= "{value}"' for attr, value in sorted(attrs.items()))
    else:
        attrs_str = ''
    if content:
        return '/n'.join(f'<{name}{attrs_str}>{c}</{name}>' for c in content)
    else:
        return f'<{name}{attrs_str} />'


if __name__ == '__main__':
    print(tag('br'))
    print(tag('p', 'hello'))
    print(tag('p', 'hello', 'world'))
    print(tag('p', 'hello', id=33))
    print(tag('p', 'hello', 'world', cls='sidebar'))
  1. name接收传入的第一个参数
  2. *content接收所有的单个非关键字参数
  3. cls=None接收关键字参数
  4. **attrs接收所有的键值对参数

5.8 获取关于参数的信息

  1. 函数对象的__defaults__属性,它的值是一个元组,用来储存定位参数和关键字参数的默认值

     def foo(n=10, m={'a': 'b'}):
         if n > 10:
             return n
         return 10
     
     print(foo.__defaults__)  # (10, {'a': 'b'})
    
  2. inspect模块,使用signature(fn).parameters.items()获取一个odict_items对象,可遍历出对应的入参和默认值

    from inspect import signature
    
    
    def foo(n=10, m={'a': 'b'}):
        if n > 10:
            return n
        return 10
    
    # print(foo.__defaults__)
    
    sig = signature(foo)
    print(sig)  # (n=10, m={'a': 'b'})
    print(sig.parameters)  # OrderedDict([('n', <Parameter "n=10">), ('m', <Parameter "m={'a': 'b'}">)])
    print(sig.parameters.items())  # odict_items([('n', <Parameter "n=10">), ('m', <Parameter "m={'a': 'b'}">)])
    for name, param in sig.parameters.items():
        print(name, param)
    

5.9 函数注解

python3提供了一种句法,用于为函数声明中的参数和返回值附加元数据:

  • 函数声明中的各个参数可以在:之后增加注解表达式.如果参数有默认值,注解放在参数名和=号之间,如果像注解返回值,在)和函数声明末尾的:之间添加->和一个表达式

  • 注解不会做任何处理,只是储存在函数的__annotations__属性中

    def foo(n: int) -> str:
        return str(n)
    
    
    print(foo.__annotations__)  # {'n': <class 'int'>, 'return': <class 'str'>}
    
    def foo(n):
        return str(n)
    
    
    print(foo.__annotations__)  # {}
    

5.10 支持函数式编程的包

1. operator模块

一个例子:计算阶乘

# 递归:python有最大递归深度,能不用递归就别用,就算加上lru_cache()缓存装饰器
def fn_1(n):
    if n > 1:
        return n * fn_2(n - 1)
    else:
        return n

可以使用reduce函数

reduce() 函数会对参数序列中元素进行累积。

函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

from functools import reduce
def fn_2(n):
    return reduce(lambda a,b: a*b, range(1, n+1))

使用operator.mul函数

from operator import mul
from functools import reduce
def fn_3(n):
    return reduce(mul, range(1, n+1))
2. 使用functools.partial冻结参数

functools模块提供了一系列高阶函数,除了reduce外,最有用的就是partialpartialmethod

  1. partial应用于固定某个函数的某个入参

    from functools import partial
    
    
    def fn_4(a=None, b=None):
        return a * (b + 1)
    
    
    fn_5 = partial(fn_4, a=2)
    print(fn_5(b=5))
    

    注意如果fn_5(b=5)中的入参不是关键字参数时,即fn_5(5),会报错TypeError: fn_4() got multiple values for argument 'a',不知道为什么

第六章 使用一等函数实现设计模式

6.1 案例分析: 重构’策略’模式

1. 经典的’策略’模式

在《设计模式:可复用面向对象软件的基础》一书中,时这样描述策略模式的:定义一系列算法,把它们一一封装起来,并使它们可以相互替换,本模式是的算法可以独立于使用它的客户而变化

一个例子,根据不同情况选择促销折扣策略

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author   : 于建津
# @Time     : 2023/4/13 13:03
# @File     : 6.1 重构策略模式.py
# @Project  : fluent_python
# @Desc     :
# 1. 经典的策略模式
from abc import ABC, abstractmethod
from typing import NamedTuple


class Customer(NamedTuple):
    name: str  # 名字
    fidelity: float  # 积分


class LineItem:
    def __init__(self, product, quantity, price):
        self.product = product  # 产品
        self.quantity = quantity  # 数量
        self.price = price  # 价格

    def total(self):
        return self.price * self.quantity


class Order:
    def __init__(self, customer, cart, promotion=None):
        self.__total = None
        self.customer = customer
        self.cart = list(cart)
        self.promotion = promotion

    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(item.total() for item in self.cart)
        return self.__total

    def due(self):
        if self.promotion is None:
            discount = 0
        else:
            discount = self.promotion.discount(self)
        return self.total() - discount

    def __repr__(self):
        return


class Promotion(ABC):
    @abstractmethod
    def discount(self, order):
        """返回折扣金额(正值)"""


class FidelityPromo(Promotion):
    """为积分为1000以上的顾客提供5%折扣"""

    def discount(self, order):
        return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0


class BulkItemPromo(Promotion):
    """单个商品为20个或以上时,提供10%折扣"""

    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 20:
                discount += item.total() * 0.1

        return discount


class LargeOrderPromo(Promotion):
    """订单中的不同商品达到10个或以上是提供7%折扣"""

    def discount(self, order):
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
            return order.total() * 0.07
        return 0

2. 使用函数实现’策略’模式

每个具体策略都是一个类.而且都只定义了一个方法,即discount,此外策略实例没有状态(没有实例属性),把具体策略换成简单的函数,而且去掉了promo抽象类

# 简化promo类为方法
def fidelity_promo(order):
    """为积分为1000以上的顾客提供5%折扣"""
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0


def bulk_item_promo(order):
    """单个商品为20个或以上时,提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * 0.1

    return discount


def large_order_promo(order):
    """订单中的不同商品达到10个或以上是提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * 0.07
    return 0
3. 选择最佳策略:简单的方式

一个新方法,为客户选择折扣最多的方案

promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order):
    """选择折扣最多的方案"""
    return max(promo(order) for promo in promos)

虽然这个方法易读,但是若想添加新的促销策略,要定义相应的函数,还要记得将他添加到promos列表中,否则新的促销策略不会被best_promo方法考虑

4. 找出模块中的全部策略

第七章 函数装饰器和闭包

  1. 装饰器用于在源码中标记函数,以某种方式增强函数的行为.这个是一项强大的功能,但是若想掌握,必须理解闭包
  2. nonlocal(外部嵌套函数内的变量)是新近出现的保留关键字,在python3.0引入,作为python程序员,如果严格遵守基于类的面向对象编程方式,即使不知道这个关键字也不受影响,但是如果想实现函数装饰器,就必须了解闭包的方方面面,因此也就需要知道nonlocal
  3. 除了在装饰器中有作用之外,闭包还是回调异步编程和函数式编程风格的基础
  4. 在本章需要讨论的话题
    • python如何计算装饰器句法
    • python如何判断变量是不是局部的
    • 闭包存在的原因和工作原理
    • nonlocal能解解决什么问题
  5. 进一步讨论装饰器
    • 实现行为良好的装饰器
    • 标准库中有用的装饰器
    • 实现一个参数化装饰器

7.1 装饰器基础知识

装饰器是可调用对象,其参数是另一个函数(被装饰的函数).装饰器可能会处理被装饰的函数,然后把他返回,或者将其他换成另一个函数或可调用对象

假如有一个名为decorate的装饰器

def decorate(fn):
    def inner():
        fn()
        print('run in decorate()')

    return inner

那么下面两种写法的执行结果一致

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


target = decorate(target)
target()

此时targetinner的引用<function decorate.<locals>.inner at 0x000002257DF0A020>

严格来说,装饰器只是语法糖,如前所示,装饰器可以向常规的可调用对象一样调用,其参数是另一个函数,有时这样做更方便,尤其是元编程(在运行时改变程序的行为)

**综上:**装饰器的一大特性是,能把装饰器的函数替换成其他函数,第二是,装饰器在加载模块时立即执行

7.2 Python何时执行装饰器

装饰器的一个关键特性:它们在被装饰的函数定义之后立即运行,这通常是在导入时

registry = []
def register(fn):
    print(f'running register({fn})')
    registry.append(fn)
    return fn

@register
def f1():
    print('running f1')

@register
def f2():
    print('running f2')

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

if __name__ == '__main__':
    print(f'registry:{registry}')
    f1()
    f2()
    f3()
running register(<function f1 at 0x000001B040D38E00>)
running register(<function f2 at 0x000001B040D3A020>)
registry:[<function f1 at 0x000001B040D38E00>, <function f2 at 0x000001B040D3A020>]
running f1
running f2
running f3
  1. 装饰器register在函数运行前执行了两次次,分别将函数添加到了registry列表中,即导入时执行
  2. 函数只有在调用的时候才被执行
  3. register装饰器原封不动的返回了被装饰的函数,这种技术并非没有用处(例如把url模式映射到生成Http相映的函数上的注册处),这种装饰器可能会也可能不会修改被装饰的函数,后面举例说明

7.3 使用装饰器改进策略模式

使用注册装饰器,可以改进6.1节中电商促销折扣示例

promos = []


def promotion(promo_func):
    promos.append(promo_func)
    return promo_func


@promotion
def fidelity(order):
    """为积分为1000或以上的顾客提供5%折扣"""
    return order.total() * .05 if order.customer.fidelity >= 1000 else 0


@promotion
def bulk_item(order):
    """单个商品为20个或以上时提供10%折扣"""
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * .1
    return discount


@promotion
def large_order(order):
    """订单中的不同商品达到10个或以上时提供7%折扣"""
    distinct_items = {item.product for item in order.cart}
    if len(distinct_items) >= 10:
        return order.total() * .07
    return 0


def best_promo(order):
    """选择可用的最佳折扣"""
    return max(promo(order) for promo in promos)

  • promos列表起始为空
  • promotion装饰器把promo_func添加到列表中,然后原封不动的返回
  • promotion装饰的函数都会被添加到promos列表中
  • best_pormos无需修改,它依赖promos列表执行

相比6.1节给出的方案相比,这个方案有几个有点

  • 促销策略无需使用特殊名称(即不需要用_promo结尾)
  • @promotion装饰器突出了被装饰的函数的作用(被装饰的函数就是现在生效的折扣策略),只需要注释装饰器,就可以禁用某个折扣策略
  • 促销折扣策略可以在其他模块中定义(好处是随处都能定义策略,坏处时不容易定位),只要使用@promotion装饰

7.4 变量作用域规则

多数装饰器会修改被装饰的函数,通常它们会定义一个内部函数,然后将其返回,替换被装饰的函数,使用内部函数的代码几乎都要靠闭包才能正确运作,为了理解闭包,我们先了解python的变量作用域

下面的例子中,我们定义并测试一个函数,他读取两个变量的值,一个是局部变量a,是函数的参数,另一个变量b,这个函数没有定义它

def fn(a):
    print(a)
    print(b)


fn(3)
NameError: name 'b' is not defined
3

因为没有定义b,所以报错,此时为全局变量b复制,在执行fn,此时可以看到函数正确执行

b = 6
def fn(a):
    print(a)
    print(b)


fn(3)
3
6

修改函数,在调用后为b赋值,在执行函数

b = 6
def fn(a):
    print(a)
    print(b)
    b = 9


fn(3)
UnboundLocalError: cannot access local variable 'b' where it is not associated with a value(UnboundLocalError:无法访问与值无关的局部变量“b”)
3

可以看到第一个print输出了,但是第二个print执行不了,因为python在编译时,判断b是局部变量,因为在函数中给它赋值了,python会尝试在本地环境获取b,并打印b的值,此时发现b未绑定值

python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量

如果想让解释器把b当成全局变量,需要用global声明

b = 6
def fn(a):
    global b
    print(a)
    print(b)
    b = 9
    print(b)


fn(3)

比较字节码(了解)

使用dis模块dis — Python 字节码反汇编器 — Python 3.11.3 文档

from dis import dis

b = 6
def fn(a):
    global b
    print(a)
    print(b)
    b = 9
    print(b)


dis(fn)
 10           0 RESUME                   0

 12           2 LOAD_GLOBAL              1 (NULL + print)
             14 LOAD_FAST                0 (a)
             16 PRECALL                  1
             20 CALL                     1
             30 POP_TOP

 13          32 LOAD_GLOBAL              1 (NULL + print)
             44 LOAD_GLOBAL              2 (b)
             56 PRECALL                  1
             60 CALL                     1
             70 POP_TOP

 14          72 LOAD_CONST               1 (9)
             74 STORE_GLOBAL             1 (b)

 15          76 LOAD_GLOBAL              1 (NULL + print)
             88 LOAD_GLOBAL              2 (b)
            100 PRECALL                  1
            104 CALL                     1
            114 POP_TOP
            116 LOAD_CONST               0 (None)
            118 RETURN_VALUE

python的dis模块简介

python的dis模块简介 - 知乎 (zhihu.com)

1. 介绍dis模块
1.1. 引言

这个模块并不常用,可能你从来没接触过,那么你可能会问:这个模块有什么用,我会在那些地方使用到? 的确,这是python的高阶技巧,不常用,通常只会用在:

  1. python的软件调优
  2. 深入理解python内部执行过程(将你的代码反编译成低级的字节码来查看它底层的工作机制)
1.2. 介绍dis模块

dis是“Disassembler”的缩写,也就是 反汇编 的意思。

  • 汇编【Assemble】: 计算机只能执行01010101……等二进制内容,汇编就是把人可以识别的汇编代码转化为计算机可以识别的二进制代码
  • 反汇编【Disassemble】: 把计算机可以识别的二进制内容转化为人可以识别的汇编代码

刚多关于汇编的介绍, 请参考: 阮一峰关于汇编的博客: http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

根据上面的描述,那么dis模块自然是提供了很多与反汇编有关的类、方法、函数。

1.3. 不可不提的字节码(bytecode)

想要介绍python的dis模块,必然先需要了解python的 字节码(bytecode) 。 什么是python的字节码(bytecode)

Python source code is compiled into bytecode, the internal representation of a Python program in the CPython interpreter. The bytecode is also cached in .pyc files so that executing the same file is faster the second time (recompilation from source to bytecode can be avoided). This “intermediate language” is said to run on a virtual machine that executes the machine code corresponding to each bytecode. Do note that bytecodes are not expected to work between different Python virtual machines, nor to be stable between Python releases.
中文版说明: Python 源代码会被编译为字节码,即 CPython 解释器中表示 Python 程序的内部代码。字节码还会缓存在 .pyc 文件中,这样第二次执行同一文件时速度更快(可以免去将源码重新编译为字节码)。这种 “中间语言” 运行在根据字节码执行相应机器码的 virtual machine 之上。请注意不同 Python 虚拟机上的字节码不一定通用,也不一定能在不同 Python 版本上兼容。

如果需要了解 字节码(bytecode) 的缓存思路,可以参考:

2. dis模块入门
2.1. dis模块最常用的方法: dis.dis()

我们使用dis模块的时候,最常用的方法是dis.dis(), 官方参考文档链接: dis.dis

dis.dis(x=None, *, file=None, depth=None)

每个参数的含义:

  • x参数: 需要被反汇编的对象。 x可以表示模块(module)、类(a class)、方法(a method)、函数(a function)、生成器(a generator)、异步生成器(a asynchronous generator)、协程(a coroutine)、代码对象(a code object)、源代码字符串(a string of source code)或原始字节码的字节序列(a byte sequence of raw bytecode)。

    • 对于模块(module),它会反汇编所有功能。
    • 对于类(class),它反汇编所有方法(包括类方法(class methods)和静态方法(static methods))。
    • 对于代码对象(code object)原始字节码序列(sequence of raw bytecode),它每字节码指令打印一行。
    • 它还递归地反汇编嵌套代码对象(推导式代码,生成器表达式和嵌套函数,以及用于构建嵌套类的代码)。在被反汇编之前,首先使用 compile() 内置函数将字符串编译为代码对象。
    • 如果未提供任何对象,则此函数会反汇编最后一次回溯(the last traceback)。
  • 如果提供了入参:file, 会将反汇编的内容作为文本写入 file 参数中; 否则写入 sys.stdout

  • 递归的最大深度受 depth 限制,除非它是 Nonedepth=0 表示没有递归。

其他补充细节:

在 3.4 版更改: 添加 file 形参。
在 3.7 版更改: 实现了递归反汇编并添加了 depth 参数。
在 3.7 版更改: 现在可以处理协程和异步生成器对象。

2.2. 以一个简单的函数为例,说明dis.dis()的使用

如何使用dis.dis(), 并阅读dis.dis()输出内容?

在stackoverflow中有非常好的回答,请参考: https://stackoverflow.com/questions/12673074/how-should-i-understand-the-output-of-dis-dis 我将其摘录下来并做了翻译,如下:

我们先给一个简单的例子:

def f(num):
    if num == 42:
        return True
    return False


import dis  # 导入dis库
dis.dis(f)  # 将f函数的反编译结果打印出来

运行程序后反编译的输出结果可能如下(python3.6、python3.8):

没有最上面的1、2、3、4、5、6、7 那一列, 之所以加这个,是为了便于解释每一列的作用。

(1)|(2)|(3)|(4)|          (5)         |(6)|  (7)
---|---|---|---|----------------------|---|-------
  2|   |   |  0|LOAD_FAST             |  0|(num)
   |-->|   |  2|LOAD_CONST            |  1|(42)
   |   |   |  4|COMPARE_OP            |  2|(==)
   |   |   |  6|POP_JUMP_IF_FALSE     | 12|
   |   |   |   |                      |   |
  3|   |   |  8|LOAD_CONST            |  2|(True)
   |   |   | 10|RETURN_VALUE          |   |
   |   |   |   |                      |   |
  4|   |>> | 12|LOAD_CONST            |  3|(False)
   |   |   | 14|RETURN_VALUE          |   |

每一列的作用如下:

  1. 第1列: 源代码中的相应行号. The corresponding line number in the source code
  2. 第2列: 可选地指示执行的当前指令(例如,当字节码来自框架对象时). Optionally indicates the current instruction executed (when the bytecode comes from a frame object for example)
  3. 第3列: 一个标签,表示从早期指令到此指令的可能JUMP. A label which denotes a possible JUMP from an earlier instruction to this one
  4. 第4列: 字节码中与字节索引相对应的地址(这些是2的倍数,因为Python 3.6对每条指令使用2字节,而之前的版本可能会有所不同). The address in the bytecode which corresponds to the byte index (those are multiples of 2 because Python 3.6 use 2 bytes for each instruction, while it could vary in previous versions)
  5. 第5列: 指令名称(也称为opname),每个指令名称都在dis模块中简要解释,它们的实现可以在ceval.c(CPython的核心文件)中找到. The instruction name (also called opname), each one is briefly explained in the dis module and their implementation can be found in ceval.c (the core loop of CPython)
  6. 第6列: Python内部用于获取一些常量或变量、管理堆栈、跳转到特定指令等的指令的参数(如果有的话). The argument (if any) of the instruction which is used internally by Python to fetch some constants or variables, manage the stack, jump to a specific instruction, etc.
  7. 第7列: 对解释器的参数的人性化的解释. The human-friendly interpretation of the instruction argument

很明显,这这里最重要的就是第5列的字节码指令(bytecode instructions)。 所有的字节码指令(bytecode instructions) 可以在如下参考文档中找到:

2.3 对比反编译后的内容与原始的字节码

在上面代码的基础上,我们增加一行:将原始的字节码打印出来. 修改后的代码:

def f(num):
    if num == 42:
        return True
    return False


import dis  # 导入dis库
dis.dis(f)  # 将f函数的反编译结果打印出来
print('*' * 50)  # 打印分割字符
print(f.__code__.co_code)  # 将f函数的原始字节码打印出来

执行后的结果:

  2           0 LOAD_FAST                0 (num)
              2 LOAD_CONST               1 (42)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE       12

  3           8 LOAD_CONST               2 (True)
             10 RETURN_VALUE

  4     >>   12 LOAD_CONST               3 (False)
             14 RETURN_VALUE
**************************************************
b'|\x00d\x01k\x02r\x0cd\x02S\x00d\x03S\x00'

通过这个对比,我们能明显看到为什么要进行反编译了,因为原始的字节码不是给人看的。。。

2.4 自己解释原始的字节码

如果此时你已经拿到了原始的字节码,想要自己解释这段原始的字节码,你需要使用一些在opcode模块中定义的常量。方法如下:

def f(num):
    if num == 42:
        return True
    return False


import dis  # 导入dis库
dis.dis(f)  # 将f函数的反编译结果打印出来
print('*' * 50)  # 打印分割字符
print(f.__code__.co_code)  # 将f函数的原始字节码打印出来
print('*' * 50)  # 打印分割字符
c = f.__code__.co_code
import opcode
for i in c:
    print(f'{i:<6d}:\t{opcode.opname[i]}')

执行后的效果:

  2           0 LOAD_FAST                0 (num)
              2 LOAD_CONST               1 (42)
              4 COMPARE_OP               2 (==)
              6 POP_JUMP_IF_FALSE       12

  3           8 LOAD_CONST               2 (True)
             10 RETURN_VALUE

  4     >>   12 LOAD_CONST               3 (False)
             14 RETURN_VALUE
**************************************************
b'|\x00d\x01k\x02r\x0cd\x02S\x00d\x03S\x00'
**************************************************
124   : LOAD_FAST
0     : <0>
100   : LOAD_CONST
1     : POP_TOP
107   : COMPARE_OP
2     : ROT_TWO
114   : POP_JUMP_IF_FALSE
12    : UNARY_NOT
100   : LOAD_CONST
2     : ROT_TWO
83    : RETURN_VALUE
0     : <0>
100   : LOAD_CONST
3     : ROT_THREE
83    : RETURN_VALUE
0     : <0>
3. 分析其他的对象
3.1. 反编译一个字典对象

代码如下:

import dis  # 导入dis库
dis.dis("my_dict = {'a': 1}")  # 直接反编译一个字典对象

执行的结果如下:

  1           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
3.2. 反编译一个代码段

代码如下:

import dis

code = """
my_dict = {'a': 1}
"""

print(f"{'*'* 10}Show Disassembly result{'*'* 10}")
dis.dis(code)

print(f"{'*'* 10}Show Code details{'*'* 10}")
dis.show_code(code)

执行效果如下:

**********Show Disassembly result**********
  2           0 LOAD_CONST               0 ('a')
              2 LOAD_CONST               1 (1)
              4 BUILD_MAP                1
              6 STORE_NAME               0 (my_dict)
              8 LOAD_CONST               2 (None)
             10 RETURN_VALUE
**********Show Code details**********
Name:              <module>
Filename:          <disassembly>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        2
Flags:             NOFREE
Constants:
   0: 'a'
   1: 1
   2: None
Names:
   0: my_dict
3.3. 反编译一个支持任意位置参数的函数

代码如下:

def f(*args):
    nargs = len(args)
    print(nargs, args)

if __name__ == '__main__':
    import dis
    dis.dis(f)

执行的结果:

  2           0 LOAD_GLOBAL              0 (len)
              2 LOAD_FAST                0 (args)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (nargs)

  3           8 LOAD_GLOBAL              1 (print)
             10 LOAD_FAST                1 (nargs)
             12 LOAD_FAST                0 (args)
             14 CALL_FUNCTION            2
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE
3.4. 反编译一个简单的类

示例代码:

class A:
    def __init__(self, name):
        self.name = name

    def twice_name(self):
        return self.name * 2


import dis
import pprint
dis.dis(A)
print('*' * 75)
pprint.pprint(A.__dict__)

执行的结果:

Disassembly of __init__:
  3           0 LOAD_FAST                1 (name)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (name)
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Disassembly of twice_name:
  6           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (name)
              4 LOAD_CONST               1 (2)
              6 BINARY_MULTIPLY
              8 RETURN_VALUE

***************************************************************************
mappingproxy({'__dict__': <attribute '__dict__' of 'A' objects>,
              '__doc__': None,
              '__init__': <function A.__init__ at 0x7f9d5c4af4c0>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'A' objects>,
              'twice_name': <function A.twice_name at 0x7f9d5c4afaf0>})

可以看到: 虽然我们想象中类比较复杂,但是其实类的字节码并不很复杂。 这里明显的分成了两部分, 每一个函数对应其中的一部分。

7.5 闭包

闭包和匿名函数不同,在函数内部定义函数并不常见,知道开始使用匿名函数才会这样,而且只有涉及嵌套函数才会有闭包问题,因此很多人同时知道这两个概念

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

下述例子

假如有一个名为age的函数,它的作用是计算不断增加的系列值的均值;例如整个历史中某个商品的平均收盘价,每天都会增加新价格,因此平均值要考虑至目前为止所有的价格,那么如何实现avg

  1. 使用类实现

    class Average:
        def __init__(self):
            self.series = []
    
        def __call__(self, new_value):
            self.series.append(new_value)
            return sum(self.series) / len(self.series)
    
    
    if __name__ == '__main__':
        a = Average()
        print(a(3))
        print(a(4))
        print(a(5))
    
  2. 使用高阶函数

    def make_average():
        serise = []
    
        def average(n):
            serise.append(n)
            return sum(serise) / len(serise)
    
        return average
    
    
    if __name__ == '__main__':
        avg = make_average()
        print(avg(3))
        print(avg(4))
        print(avg(5))
    
  3. 两个实例的共通之处

    • 调用Average()make_average()都会得到一个可调用对象ava,他会更新历史值,然后计算当前均值.
    • 两个例子中,历史值都是储存在列表中,self.serise是实例属性,serisemake_averager的局部变量
  4. 【python技巧060】形象理解闭包,玩转闭包_哔哩哔哩_bilibili讲的很详细

  5. 函数avg.__closure__可以查看闭包函数中包含的对象

  6. 综上,闭包是一种函数,他会保留定义函数时存在的自由变量的绑定(一种说法:闭包=环境+控制,环境指闭包函数携带的上层函数中的局部变量,控制指闭包函数本身所起的作用)

7.6 nonlocal声明

前面实现make_averager的方法,我们把所有值都储存在历史列表中,然后每次调用averager时计算sum求和,更好的实现方法是只存储目前的总值以及元素个数,然后在计算平均值

下述例子

def make_averager():
    total = 0
    count = 0

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


if __name__ == '__main__':
    a = make_averager()
    print(a(3))
    print(a(4))
    print(a(5))

但是这个方法有一个缺陷,执行代码时会报错cannot access local variable 'count' where it is not associated with a value(无法访问与值无关的局部变量“count”)

在上一节中没有发现这个问题,因为我们只是调用了serise.append()并计算,但是对于数字,或其他不可变类型时,只能读不能更新,如果尝试重新绑定,其实会隐式创建局部变量count,这样count就不再是自由变量了,而不再保存在闭包中

为了解决这个问题,python3中引入了nonlocal声明,它的作用是把变量标记为自由变量,即使在函数中为变量赋新值,也会变成自由变量,此时只需要将本节例子中averager的两个自由变量声明为nonlocal即可

def make_averager():
    total = 0
    count = 0

    def averager(n):
        nonlocal count, total
        count += 1
        total += n
        return total / count
    return averager


if __name__ == '__main__':
    a = make_averager()
    print(a(3))
    print(a(4))
    print(a(5))

7.7 实现一个简单的装饰器

下述例子实现一个装饰器,用于打印被装饰函数的运行时间,及传入的参数和调用的结果打印出来

import time


def clock(fn):
    def clocked(*args):
        t0 = time.perf_counter()
        result = fn(*args)
        elapsed = time.perf_counter() - t0
        name = fn.__name__
        arg_str = ','.join(repr(arg) for arg in args)
        print(f'[函数{name},执行时间:{elapsed},入参:{arg_str},return:{result}]')
        return result
    return clocked


@clock
def fn(n):
    print('running fn')
    time.sleep(n)
    return f'函数执行{n}秒'

if __name__ == '__main__':
    fn(2)
running fn
函数fn,执行时间:2.000215399999888,入参:2,return:函数执行2秒

这是装饰器的典型行为:把被装饰的函数替换成新函数,二者接受相同的参数,此时执行fn.__name__的结果是clocked,并且不支持关键字参数

新的例子通过使用functools.wraps装饰器,把相关的属性(__name__,__doc__)从fn复制到clocked,并且支持了关键字参数

import functools
import time


def clock(fn):
    @functools.wraps(fn)  # 用来将fn的属性传递给clocked
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = fn(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = fn.__name__
        arg_list = []
        
        # 获取args和kwargs的参数
        if args:  
            arg_list.append(','.join(repr(arg) for arg in args))
        if kwargs:
            pair = [f'{key}={value}' for key, value in kwargs.items()]
            arg_list.append(','.join(repr(arg) for arg in pair))
        if not (args and kwargs):
            arg_list.append('(no params)')
        arg_str = ','.join(arg_list)
        
        print(f'函数:{name:{6}},执行时间:{elapsed:{6}.{4}},入参:{arg_str:{6}},return:{result}')
        return result
    return clocked


@clock
def fn(n=1):
    print('running fn')
    time.sleep(n)
    return f'函数执行{n}秒'

if __name__ == '__main__':
    fn()
    print(fn.__name__)

7.8 标准库中的装饰器

python中内置了三个用于装饰方法的函数,property(19.2),classmethod(9.4),staticmethod(9.4)

另外常见的装饰器是functools.wraps,functools.lru_cache,functools.singledispatch

1.使用functools.lru_cache做备忘
from functools import lru_cache

from utils.clock import clock


@lru_cache()
@clock
def fib(n):
    if n < 2:
        return n
    return fib(n - 2) + fib(n - 1)


if __name__ == '__main__':
    print(fib(50))
  1. 使用lru_cache装饰器可以大量的节省函数执行时间,因为它把计算过的数值存在缓存中,再次调用时可以直接获取结果而非计算
  2. 除了优化递归算法外,lru_cache还在web中获取信息的应用中发挥巨大的作用
  3. lru_cache接受两个可选的参数functools.lru_cache(maxsize=128, typed=False),maxsize指定缓存多少个调用结果,缓存满了之后,旧的结果会被丢弃,为了获得最佳性能,值应为2的幂,typed如果设为True把不同类型的到的结果分开保存,即把整形和浮点型分开
  4. 因为lru_cache使用字典储存结果,所以被它装饰的函数,所有入参必须时可散列(哈希)的
2.单分派泛函数

暂时没看懂

7.9 叠放装饰器

多个装饰器装饰一个函数时,他们的执行顺序如下述例子

def dec_1(fn):
    print('1-1')
    return fn

def dec_2(fn):
    print('2-1')
    return fn


@dec_1
@dec_2
def fn():
    print('running fn')


if __name__ == '__main__':
    fn()

两个装饰器执行时fn等同于dec_1(dec_2(fn))

7.10 参数化装饰器

之前的例子中,装饰器都是把被装饰的函数作为入参,那么怎么给装饰器传递其他参数呢:答案时创建一个装饰器工厂函数,把参数传给他,返回一个装饰器,再把新的装饰器应用到要装饰的函数上

下述例子

registry = set()


def register(fn):
    print('running register')
    registry.add(fn)
    return fn

def new_register(active=True):
    def register(fn):
        print('running register', f'parmas:{active}')
        if active:
            registry.add(fn)
        else:
            registry.discard(fn)
        return fn
    return register


@new_register(active=True)
def fn_1():
    print(f'running:{fn_1.__name__}, registry:{registry}')

@new_register(active=False)
def fn_2():
    print(f'running:{fn_2.__name__}, registry:{registry}')

@new_register(active=True)
def fn_3():
    print(f'running:{fn_3.__name__}, registry:{registry}')


if __name__ == '__main__':
    fn_1()
    fn_2()
    fn_3()

注意
该装饰器使用时必须带()

第四部分 面向对象惯用法

第八章 对象引用,可变性和垃圾回收

8.1 变量不是盒子

  1. 讲对象赋值给变量时,变量不是一个装对象的盒子,而更像是给对象贴的一个标签
  2. s='str'说成把s分配给str更合适…

8.2 标识,相等性和别名

下述例子

a = [1, 2, 3]
b = a
print(a is b)
print(id(a))
print(id(b))
b.append(4)
print(a)
c = [1, 2, 3, 4]
print(c is a)
True
1432396912832
1432396912832
[1, 2, 3, 4]
False
  1. ba的别名
  2. 两个变量的内存地址一致
  3. 此时向b添加元素,a中一样被添加
  4. 此时定义一个新变量c值与a相同,但是他们的内存不一样,c也不是a
1. 在==is之间选择
  • ==比较两个对象的值是否相同
  • is比较两个对象是否是一个对象
  • is运算速度更快,因为它不能重载,所以python不用寻找并调用特殊方法,而是直接比较两个整数id,而==是语法糖,就等同于a.__eq__(b)
2. 元组的相对不可变性

元组与多数python集合一样,保存的是对象的引用,如果引用的元素是可变的,即使元组本身不可变,元素依然可变,也就是说元组的不可变性其实是指tuple数据结构的物理内容不可变,与引用的对象无关

x = (1, 2, [3, 4], 5)
try:
    x[2] += [6, 7]
finally:
    print(x)
(1, 2, [3, 4, 6, 7], 5)
Traceback (most recent call last):
  File "D:\dev\fluent_python\4.第四部分 面向对象惯用法\8章 对象引用,可变性和垃圾回收\8.2 标识,相等性和别名.py", line 20, in <module>
    x[2] += [6, 7]
    ~^^^
TypeError: 'tuple' object does not support item assignment
3. 默认做浅复制

切片是浅复制

c = [1, [11, 12], 3, 4]
d = c[:2]
print(d)  # [1, [11, 12]]
c[1][1] = 13
print(c)  # [1, [11, 13], 3, 4]
print(d)  # [1, [11, 13]]
4. 深拷贝和浅拷贝

8.4 函数的参数作为引用时

python唯一支持的参数传递模式是共享传参,多数面向对象语言都采用这一模式

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

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

8.5 del和垃圾回收

  1. del删除名称,而不是对象,del命令可能会导致对象被当做垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时,重新绑定也可能导致对象的引用数量归零,导致对象被销毁
  2. 有一个__del__魔法方法,他不会销毁实例,不应该在代码中调用,而是在即将销毁实例时,python解释器调用该方法,给实例最后的机会,释放外部资源

8.6 弱引用

弱引用不会增加对象的引用数量,弱引用不会方案所指对象被当作垃圾回收

弱引用指向的对象存在时,获取对象,否则获取None

第九章 符合python风格的对象

9.1 对象表示形式

  1. repr()以便于开发者理解的方式返回对象的字符串表示形式,对应__repr__魔法方法
  2. str()以便于用户理解的方式返回对象的字符串表示形式,对应__str__魔法方法

9.2 再谈向量类

查看utils文件夹的example.py文件

9.3 备选构造方法

9.4 classmethodstaticmethod

  1. classmethod的第一个参数为类本身(cls),正如实例方法的第一个参数为对象本身(self);
  2. staticmethod第一个参数不需要传入clsself,故staticmethod中是无法访问类和对象的数据的。

9.5 格式化显示

参考文档:
https://docs.python.org/zh-cn/3/reference/lexical_analysis.html#formatted-string-literals
http://zetcode.com/python/fstring/

3.6 新版功能.

格式字符串字面值 或称 f-string 是标注了 'f''F' 前缀的字符串字面值。这种字符串可包含替换字段,即以 {} 标注的表达式。其他字符串字面值只是常量,格式字符串字面值则是可在运行时求值的表达式。

除非字面值标记为原始字符串,否则,与在普通字符串字面值中一样,转义序列也会被解码。解码后,用于字符串内容的语法如下:

f_string          ::=  (literal_char | "{{" | "}}" | replacement_field)*
replacement_field ::=  "{" f_expression ["="] ["!" conversion] [":" format_spec] "}"
f_expression      ::=  (conditional_expression | "*" or_expr)
                         ("," conditional_expression | "," "*" or_expr)* [","]
                       | yield_expression
conversion        ::=  "s" | "r" | "a"
format_spec       ::=  (literal_char | NULL | replacement_field)*
literal_char      ::=  <any code point except "{", "}" or NULL>

双花括号 '{{''}}' 被替换为单花括号,花括号外的字符串仍按字面值处理。单左花括号 '{' 标记以 Python 表达式开头的替换字段。在表达式后加等于号 '=',可在求值后,同时显示表达式文本及其结果(用于调试)。 随后是用叹号 '!' 标记的转换字段。还可以在冒号 ':' 后附加格式说明符。替换字段以右花括号 '}' 为结尾。

格式字符串字面值中,表达式的处理与圆括号中的常规 Python 表达式基本一样,但也有一些不同的地方。不允许使用空表达式;lambda 和赋值表达式 := 必须显式用圆括号标注;替换表达式可以包含换行(例如,三引号字符串中),但不能包含注释;在格式字符串字面值语境内,按从左至右的顺序,为每个表达式求值。

在 3.7 版更改: Python 3.7 以前, 因为实现的问题,不允许在格式字符串字面值表达式中使用 await 表达式与包含 async for 子句的推导式。

表达式里含等号 '=' 时,输出内容包括表达式文本、'=' 、求值结果。输出内容可以保留表达式中左花括号 '{' 后,及 '=' 后的空格。没有指定格式时,'=' 默认调用表达式的 repr()。指定了格式时,默认调用表达式的 str(),除非声明了转换字段 '!r'

3.8 新版功能: 等号 '='

指定了转换符时,表达式求值的结果会先转换,再格式化。转换符 '!s' 调用 str() 转换求值结果,'!r' 调用 repr()'!a' 调用 ascii()

输出结果的格式化使用 format() 协议。格式说明符传入表达式或转换结果的 __format__() 方法。省略格式说明符,则传入空字符串。然后,格式化结果包含在整个字符串的最终值里。

Top-level format specifiers may include nested replacement fields. These nested fields may include their own conversion fields and format specifiers, but may not include more deeply nested replacement fields. The format specifier mini-language is the same as that used by the str.format() method.

格式化字符串字面值可以拼接,但是一个替换字段不能拆分到多个字面值。

格式字符串字面值示例如下:

>>>
>>> name = "Fred"
>>> f"He said his name is {name!r}."
"He said his name is 'Fred'."
>>> f"He said his name is {repr(name)}."  # repr() is equivalent to !r
"He said his name is 'Fred'."
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal("12.34567")
>>> f"result: {value:{width}.{precision}}"  # nested fields
'result:      12.35'
>>> today = datetime(year=2017, month=1, day=27)
>>> f"{today:%B %d, %Y}"  # using date format specifier
'January 27, 2017'
>>> f"{today=:%B %d, %Y}" # using date format specifier and debugging
'today=January 27, 2017'
>>> number = 1024
>>> f"{number:#0x}"  # using integer format specifier
'0x400'
>>> foo = "bar"
>>> f"{ foo = }" # preserves whitespace
" foo = 'bar'"
>>> line = "The mill's closed"
>>> f"{line = }"
'line = "The mill\'s closed"'
>>> f"{line = :20}"
"line = The mill's closed   "
>>> f"{line = !r:20}"
'line = "The mill\'s closed" '

与常规字符串字面值的语法一样,替换字段中的字符不能与外层格式字符串字面值的引号冲突:

f"abc {a["x"]} def"    # error: outer string literal ended prematurely
f"abc {a['x']} def"    # workaround: use different quoting

格式表达式中不能有反斜杠,否则会报错:

f"newline: {ord('\n')}"  # raises SyntaxError

要使用反斜杠转义的值,则需创建临时变量。

>>> newline = ord('\n')
>>> f"newline: {newline}"
'newline: 10'

即便未包含表达式,格式字符串字面值也不能用作文档字符串。

>>> def foo():
...     f"Not a docstring"
...
>>> foo.__doc__ is None
True

参阅 PEP 498,了解格式字符串字面值的提案,以及与格式字符串机制相关的 str.format()

9.6 可散列的Vector2d

vector2d:向量模块

9.7 python的私有属性和受保护的属性

class Demo:
    def __init__(self):
        self.__name = 'tom'
        self._age = 12

    def get_name(self):
        return self.__name



if __name__ == '__main__':
    d = Demo()
    print(d.get_name())  # tom
    print(d.__dict__)  # {'_Demo__name': 'tom', '_age': 12}
    print(d._Demo__name)  # tom
    print(d._age)  # 12
  1. 在类中实例属性前添加__来创建私有属性
  2. 在类中实例属性前添加_来创建受保护的属性
  3. python解释器会对私有属性进行重命名,而不会处理受保护的属性
  4. 约定俗成:不在类外部调用受保护的属性

9.8 使用__slots__类属性节省空间

class Demo:
    def __init__(self, a, b):
        self.a = a
        self.b = b

class OtherDemo:
    __slots__ = ('a', 'b')

    def __init__(self, a, b):
        self.a = a
        self.b = b


if __name__ == '__main__':
    print(Demo(1, 2).__dict__)  # {'a': 1, 'b': 2}
    print(OtherDemo(1, 2).__slots__)  # ('a', 'b')
    print(Demo(1, 2).a)  # 1
    print(OtherDemo(1,3).b)  # 3
  1. 使用__slots__能够显著节省内存
  2. 每个子类都要定义__slots__,因为解释器会忽略继承的__slots__属性
  3. 实例只能拥有__slots__列出的属性,除非把__dict__假如__slots__(这样会失去节省内存的功效)
  4. 如果不把__weakref__加入__slots__,实例就不能作为弱引用的目标

9.9 覆盖类属性

python有个很独特的特性,类属性可用于为实例属性提供默认值,这个默认值可以在实例中被修改,但是修改的只是那个实例中的属性,默认的类属性不会改变.

类属性可以被子类继承

class Demo:
    name = 'tom'
    def __init__(self, age):
        self._age = age
class OtherDemo(Demo):
    pass


if __name__ == '__main__':
    d = Demo(19)
    f = Demo(20)
    g = OtherDemo(21)
    f.name = 'lee'
    g.name = 'ju'
    print(d.__dict__)  # {'_age': 19}
    print(f.__dict__)  # {'_age': 20, 'name': 'lee'}
    print(g.__dict__)  # {'_age': 21, 'name': 'ju'}

第十章 序列的修改,散列和切片

本章将对utils文件夹中的example.pyVector2d类进行拓展,进一步实现如下功能

  1. 基本的序列协议:__len____getitem__
  2. 正确表述拥有很多元素的实例
  3. 适当的切片支持,用于生成新的Vector2d实例
  4. 综合各个元素的值计算散列值
  5. 自定义的格式语言扩展

此外还将通过__getattr__方法实现属性的动态存取,以此取代Vector2d使用的只读特性

10.1 Vector类:用户定义的序列类型

本章将使用组合模式实现Vector,而不是继承.向量的分量储存在浮点数数组中,而且还将实现不可变扁平序列所需的方法

要确保尽可能兼容Vector2d除非无意义的兼容

10.2 Vector第一版,兼容Vector2d

10.3 协议和鸭子类型

  1. 协议:在面向对象编程中,协议是非正式的接口,只在文档中定义,在代码中不定义
  2. python的序列协议只需要实现__len____getitem__两个方法,无需是谁的子类
  3. 下面的例子,FrenchDeck类实现了序列协议,不过代码中并没有声明这点,因为他的行为像序列
  4. 协议是非正式的,没有强制力的,因此如果你知道类的具体使用场景,通常只需要实现一个协议的部分

下述例子为1-1中的纸牌类

class Card(NamedTuple):
    rank : str
    suit : str


class FrenchDeck:
    ranks = [str(i) for i in range(2, 11)] + ['J', 'Q', 'K', 'A']
    suits = ['黑', '红', '花', '片']

    def __init__(self):
        self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits] + [Card('黑', 'Jocker'), Card('红', 'Jocker')]

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

    def __getitem__(self, item):
        return self._cards[item]
    
    
if __name__ == '__main__':
    f = FrenchDeck()
    print(len(f))
    for i in f:
        print(i)
Card(rank='2', suit='黑')
Card(rank='2', suit='红')
Card(rank='2', suit='花')
...

10.4 实现可切片的序列

第十一章 接口:从协议到抽象基类

抽象类和元类一样,适用于构建框架的工具

鸭子类型:如果一只鸟走路像鸭子,游泳像鸭子,叫声像鸭子,那这只鸟就可以被称为鸭子(对象的类型无关紧要,只要实现了特定的协议即可)

11.1 python文化中的接口和协议

  1. python除了抽象基类,每个类都有接口:类实现或者继承的公开属性(方法或数据属性),包括魔法方法
  2. 按照定义,受保护的属性和私有属性不在接口中(因为他们不是公开的,即便只是通过命名约定来实现)
  3. 实用的补充:对象公开方法的子集,让对象再系统中扮演特定的关于接口,python文档中的’文件类对象’和’可迭代对象’就是这个意思,这种说法指的不是特定的类
  4. 接口是实现特定角色的放法合集,这样的理解就是所谓的协议,协议与继承没关系,一个类可能实现多个接口,从而让实例扮演多个角色
  5. 协议是接口(非正式),协议不能想正式的接口那样施加限制,一个类可能只实现部分接口,这是允许的
  6. 序列协议是python最基础的协议之一,即便对象只实现了那个协议最基本的一部分,解释器也会负责的处理

11.2 python喜欢序列

python数据模型的哲学是尽量支持基本协议

下图展示定义为抽象基类的Sequence正式接口

image-20230421164703689

下述例子

class Foo:
    def __getitem__(self, item):
        return range(0, 30, 10)[item]


if __name__ == '__main__':
    f = Foo()
    print(10 in f)
    for i in f:
        print(i)

虽然Foo类没有继承abc.Sequence而只是实现了序列协议的一个方法__getitem__这样足够访问元素,迭代和in运算了

综上:鉴于序列协议的重要性,如果没有__iter____contains__方法,python会调用__getitem__方法,设法让迭代和in运算符可用

11.3 使用猴子补丁在运算时实现协议

下述例子实现对纸牌类的洗牌

  1. 通过继承,子类增加__setitem__方法

    from random import shuffle
    
    from utils.class_frenchdeck import FrenchDeck
    
    class FrenchDeck_11_3(FrenchDeck):
        def __setitem__(self, key, value):
            self._cards[key] = value
    
            
    if __name__ == '__main__':
        f = FrenchDeck_11_3()
        shuffle(f)
        print(f[:5])
    
  2. 使用猴子补丁

    什么是猴子补丁:在运行时修改类或模块,而不改动源码,但是补丁代码要和打补丁的程序耦合的十分紧密,而且往往要处理隐秘和没有文档的部分

    from random import shuffle
    
    from utils.class_frenchdeck import FrenchDeck
    
    
    def set_card(deck, position, card):
        deck._cards[position] = card
    
    
    if __name__ == '__main__':
        deck = FrenchDeck()
        FrenchDeck.__setitem__ = set_card
        shuffle(deck)
        print(deck[:5])
    

    使用猴子补丁的关键是,set_card函数要知道deck对象有一个名为_cards的属性,而且_cards的值必须是可变序列

    需要注意的是:我们可以在类的外部定义一个函数,如set_card,他的入参deck,position,card和类中的__setitem__方法入参self, key, value起到相同的效果

    这个说法是为了说明,在python中,每个方法说到底就是普通函数,类中方法的第一个入参命名为self只是一种约定

11.4 水禽和抽象基类

不要在生产代码中定义抽象基类(或元类)

白鹅类型:在鸭子类型的基础上增加,指:只要cls是抽象基类,即cls的元素是abc.ABCMeta就可以使用isinstance(obj, cls)

collections.abc有很多有用的抽象类,与具体类相比,抽象基类有很多理论上的有点,以及使用优势:可以使用register类方法在终端用户的代码中吧某个类声明为一个抽象基类的虚拟子类(被注册的类必须满足抽象基类方法名称和签名的要求,最重要的是满足底层语义契约,但是开发那个类时不用了解抽象基类,更不用继承抽象基类).这打破了严格的强耦合,与面向对象编程人员掌握的知识有很大出入,因此使用继承时要小心

有时为了让抽象基类识别子类,甚至不用注册

from collections import abc


class Struggle:
    def __len__(self):return  23


print(isinstance(Struggle(), abc.Sized))  # True

可以看出,无需注册,abc.Sized 也能把Struggle 识别为自己的子类,只要实现了特殊方法__len__ 即可(要使用正确的句法和语义实现,前者要求没有参数,后者要求返回一个非负整数,指明对象的长度;如果不使用规定的句法和语义实现特殊方法,如__len__,会导致非常严重的问题)。
最后我想说的是:如果实现的类体现了numberscollections.abc 或其他框架中抽象基类的概念,要么继承相应的抽象基类(必要时),要么把类注册到相应的抽象基类中。开始开发程序时,不要使用提供注册功能的库或框架,要自己动手注册;如果必
须检查参数的类型(这是最常见的),例如检查是不是“序列”,那就这样做:

isinstance(the_arg, collections.abc.Sequence)

此外,不要在生产代码中定义抽象基类(或元类)

11.5 定义抽象类的子类

from utils.class_frenchdeck import FrenchDeck


class FrenchDeck_11_5(FrenchDeck):
    def __setitem__(self, key, value):
        self._cards[key] = value

    def __delitem__(self, key):
        del self._cards[key]

    def __insert__(self, position, value):
        self._cards.insert(position, value)

➊ 为了支持洗牌,只需实现__setitem__ 方法。
➋ 但是继承MutableSequence 的类必须实现__delitem__ 方法,这是MutableSequence 类的一个抽象方法。
➌ 此外,还要实现insert 方法,这是MutableSequence 类的第三个抽象方法。导入时(加载并编译frenchdeck2.py 模块时),Python 不会检查抽象方法的实现,在运行时实例化FrenchDeck2 类时才会真正检查。因此,如果没有正确实现某个抽象方法,Python 会抛出TypeError 异常,并把错误消息设为"Can't instantiate abstract class FrenchDeck2 with abstract methods `__delitem__`, insert"。正是这个原因,即便FrenchDeck2 类不需要__delitem__ 和insert 提供的行为,也要实现,因为MutableSequence 抽象基类需要它们。

11.6 标准库中的抽象基类

大多是抽象基类定义在collections.abc模块中

1. collections.abc模块中的抽象基类

image-20230422103525477

  1. Iterable,Container,Sized

    各个集合应该继承这三个抽象基类,或说至少实现兼容的协议

    • Iterable通过__iter__方法支持迭代
    • Container通过__contains__方法支持in运算
    • Sized通过__len__方法支持len()函数
  2. Sequence,Mapping,Set

    三个主要的不可变集合类型,各自都有可变的子类

    • MutableSequence

      image-20230422104153683

    • MutableMapping

      image-20230422104111502

    • MutableSet

      image-20230422104134958

  3. MappingView

    映射方法.items(),.keys(),.values()返回的对象分别是ItemsView,KeysView,ValuesView的实例,前两个类还从Set类继承了丰富的接口

  4. Callable Hashable

    这两个抽象基类与集合没有太大的关系,只不过因为collections.abc 是标准库中定义抽象基类的第一个模块,而它们又太重要了,因此才把它们放到collections.abc 模块中。

  5. Iterator

    Iterable的子类

2. 抽象基类的数字塔numbers

numbers包定义的是’数字塔’:各个抽象基类的层次结构是现行的,其中Number是位于顶端的超类

  • Number
  • Complex
  • Real
  • Rational
  • Integral

因此,如果想检查一个数是不是整数,可以使用isinstance(x, numbers.Integral),这样代码就能接受intbool(int 的子类),或者外部库使用numbers 抽象基类注册的其他类型。为了满足检查的需要,你或者你的API 的用户始终可以把兼容的类型注册为numbers.Integral 的虚拟子类。

与之类似,如果一个值可能是浮点数类型,可以使用isinstance(x, numbers.Real) 检查。这样代码就能接受boolintfloatfractions.Fraction,或者外部库(如NumPy,它做了相应的注册)提供的非复数类型。

decimal.Decimal 没有注册为numbers.Real 的虚拟子类,这有点奇怪。没注册的原因是,如果你的程序需要Decimal 的精度,要防止与其他低精度数字类型混淆,尤其是浮点数。

11.7 定义并使用一个抽象基类

下述例子

在网站或移动应用中显示随即广告,但是在整个广告清单轮转一遍前,不重复显示广告

构建一个广告管理框架,名为ADAM,支持用户提供随机挑选的无重复类,为了让ADAM的用户明确理解’随机挑选的无重复’组件是什么意思,我们将定义一个抽象基类Tombola,他有四个方法,其中两个是抽象方法

  • .load()把元素放入容器
  • .pick()从容其中随机拿出一个元素,返回选中的元素

另外两个具体方法

  • .loaded()如果容器至少有一个元素,返回True
  • .inspect()返回一个有序元组,由容器中的现有元素构成,不会修改容器的内容(内部顺序不保留)

下图展示Tombola抽象基类和三个具体实现(一个抽象基类和三个子类的UML类图,根据UML的约定,Tombola抽象基类和他的抽象方法是用斜体,虚线箭头用于表示接口实现,这里它表示TombolistTombola的虚拟子类,因为Tombola是注册的)

image-20230422144347361

import abc


class Tombola(abc.ABC):  # 1
    @abc.abstractmethod
    def load(self, iterable):  # 2
        """从可迭代对象中添加元素"""

    @abc.abstractmethod
    def pick(self):  # 3
        """随机删除元素,并将它返回
        如果实例为空,这个方法应抛出`LookupError`
        """

    def loaded(self):  # 4
        """如果至少有一个元素,返回True,否则返回False"""
        return bool(self.inspect())  # 5

    def inspect(self):
        """返回一个有序元组,由当前元素构成"""
        items = []
        while True:  # 6
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)  # 7
        return tuple(sorted(items))
  1. 自己定义的抽象基类要继承abc.ABC
  2. 抽象方法使用@abc.abstractmethod装饰器标记,而且定义体中通常只有文档字符串
  3. 根据文档字符串,如果没有元素可选,应该抛出LookupError
  4. 抽象基类可以包含具体方法
  5. 抽象基类中的具体方法只能依赖抽象基类定义的接口(即只能使用使用抽象基类中的其他具体方法,抽象方法或特性)
  6. 我们不知道具体子类如何储存元素,不过为了的到inspect的结构,我们可以不断调用.pick方法把Tombola清空
  7. 然后再使用.load()把所有元素放回去

异常层次结构

BaseException 所有内置异常的基类
 ├── BaseExceptionGroup
 ├── GeneratorExit 当一个 generator 或 coroutine 被关闭时将被引发
 ├── KeyboardInterrupt 当用户按下中断键 (通常为 Control-C 或 Delete) 时将被引发
 ├── SystemExit 此异常由 sys.exit() 函数引发
 └── Exception 所有内置的非系统退出类异常都派生自此类。 所有用户自定义异常也应当派生自此类
      ├── ArithmeticError 此基类用于派生针对各种算术类错误而引发的内置异常
      │    ├── FloatingPointError 目前未被使用
      │    ├── OverflowError 当算术运算的结果大到无法表示时将被引发
      │    └── ZeroDivisionError 当除法或取余运算的第二个参数为零时将被引发
      ├── AssertionError 当 assert 语句失败时将被引发。
      ├── AttributeError 当属性引用或赋值失败时将被引发
      ├── BufferError 当与 缓冲区 相关的操作无法执行时将被引发
      ├── EOFError 当 input() 函数未读取任何数据即达到文件结束条件 (EOF) 时将被引发
      ├── ExceptionGroup [BaseExceptionGroup]
      ├── ImportError 当 import 语句尝试加载模块遇到麻烦时将被引发
      │    └── ModuleNotFoundError ImportError 的子类,当一个模块无法被定位时将由 import 引发
      ├── LookupError 此基类用于派生当映射或序列所使用的键或索引无效时引发的异常
      │    ├── IndexError 当序列抽取超出范围时将被引发
      │    └── KeyError 当在现有键集合中找不到指定的映射(字典)键时将被引发
      ├── MemoryError 当一个操作耗尽内存但情况仍可(通过删除一些对象)进行挽救时将被引发
      ├── NameError 当某个局部或全局名称未找到时将被引发
      │    └── UnboundLocalError 当在函数或方法中对某个局部变量进行引用,但该变量并未绑定任何值时将被引发
      ├── OSError 此异常在一个系统函数返回系统相关的错误时将被引发
      │    ├── BlockingIOError 当一个操作将会在设置为非阻塞操作的对象(例如套接字)上发生阻塞时将被引发
      │    ├── ChildProcessError 当一个子进程上的操作失败时将被引发
      │    ├── ConnectionError 与连接相关问题的基类
      │    │    ├── BrokenPipeError 当试图写入一个管道而该管道的另一端已关闭,或者试图写入一个套接字而该套接字已关闭写入时将被引发
      │    │    ├── ConnectionAbortedError 当一个连接尝试被对端中止时将被引发
      │    │    ├── ConnectionRefusedError 当一个连接尝试被对端拒绝时将被引发
      │    │    └── ConnectionResetError 当一个连接尝试被对端重置时将被引发
      │    ├── FileExistsError 当试图创建一个已存在的文件或目录时将被引发
      │    ├── FileNotFoundError 将所请求的文件或目录不存在时将被引发
      │    ├── InterruptedError 当系统调用被输入信号中断时将被引发
      │    ├── IsADirectoryError 当请求对一个目录执行文件操作 (例如 os.remove()) 时将被引发
      │    ├── NotADirectoryError 当请求对一个非目录执行目录操作 (例如 os.listdir()) 时将被引发
      │    ├── PermissionError 当在没有足够访问权限的情况下运行操作时引发
      │    ├── ProcessLookupError 当给定的进程不存在时将被引发
      │    └── TimeoutError 当一个系统函数在系统层级发生超时的情况下将被引发
      ├── ReferenceError 此异常将在使用 weakref.proxy() 函数所创建的弱引用来访问该引用的某个已被作为垃圾回收的属性时被引发
      ├── RuntimeError 当检测到一个不归属于任何其他类别的错误时将被引发
      │    ├── NotImplementedError 此异常派生自 RuntimeError。 在用户自定义的基类中,抽象方法应当在其要求所派生类重载该方法,或是在其要求所开发的类提示具体实现尚待添加时引发此异常。
      │    └── RecursionError 此异常派生自 RuntimeError。 它会在解释器检测发现超过最大递归深度时被引发。
      ├── StopAsyncIteration 必须由一个 asynchronous iterator 对象的 __anext__() 方法来引发以停止迭代操作
      ├── StopIteration 由内置函数 next() 和 iterator 的 __next__() 方法所引发,用来表示该迭代器不能产生下一项。
      ├── SyntaxError 当解析器遇到语法错误时引发
      │    └── IndentationError 与不正确的缩进相关的语法错误的基类
      │         └── TabError 当缩进包含对制表符和空格符不一致的使用时将被引发
      ├── SystemError 当解释器发现内部错误,但情况看起来尚未严重到要放弃所有希望时将被引发
      ├── TypeError 当一个操作或函数被应用于类型不适当的对象时将被引发。
      ├── ValueError 当操作或函数接收到具有正确类型但值不适合的参数,并且情况不能用更精确的异常例如 IndexError 来描述时将被引发。
      │    └── UnicodeError 当发生与 Unicode 相关的编码或解码错误时将被引发
      │         ├── UnicodeDecodeError 当在解码过程中发生与 Unicode 相关的错误时将被引发
      │         ├── UnicodeEncodeError 当在编码过程中发生与 Unicode 相关的错误时将被引发
      │         └── UnicodeTranslateError 在转写过程中发生与 Unicode 相关的错误时将被引发
      └── Warning 警告类别的基类
           ├── BytesWarning 与 bytes 和 bytearray 相关的警告的基类
           ├── DeprecationWarning 如果所发出的警告是针对其他 Python 开发者的,则以此作为与已弃用特性相关警告的基类
           ├── EncodingWarning 与编码格式相关的警告的基类
           ├── FutureWarning 如果所发出的警告是针对以 Python 所编写应用的最终用户的,则以此作为与已弃用特性相关警告的基类
           ├── ImportWarning 与在模块导入中可能的错误相关的警告的基类
           ├── PendingDeprecationWarning 对于已过时并预计在未来弃用,但目前尚未弃用的特性相关警告的基类
           ├── ResourceWarning 资源使用相关警告的基类
           ├── RuntimeWarning 与模糊的运行时行为相关的警告的基类
           ├── SyntaxWarning 与模糊的语法相关的警告的基类
           ├── UnicodeWarning 与 Unicode 相关的警告的基类
           └── UserWarning 用户代码所产生警告的基类
1. 抽象基类句法详解

尝试创建一个有缺陷的子类

class Fake(Tombola):
    def pick(self):
        return 13


if __name__ == '__main__':
    f = Fake()  # TypeError: Can't instantiate abstract class Fake with abstract method load

尝试实例化时抛出TypeError错误,python认为Fake时抽象类,因为他没有实现load方法,这是Tombola抽象基类声明的抽象方法之一

声明抽象类最简单的方法时继承abc.ABC或其他抽象基类

元类将在21章讲解,现在我们暂且把元类理解为一种特殊的类,同样也把抽象基类理解为一种特殊的类,例如常规的类不会检查子类,因此这是抽象基类的特殊行为;

2.定义Tombola抽象基类的子类

定义好Tombola抽象基类之后,我们要开发两个具体子类,满足Tombola规定的接口,这两个子类的类图如11-4,图中还有将在下节讨论的虚拟子类

image-20230424215126361

下述例子

BingoCage类时在示例5-8的基础上修改的,使用了更好的随机发生器.BingoCage实现了所需的抽象方法loadpick,从Tombola中继承了loaded方法,覆盖了inspect方法,还增加了__call__方法

class BingoCage(Tombola):  # ➊ 明确指定BingoCage 类扩展Tombola 类。
    def __init__(self, items):
        self._randomize = random.SystemRandom()  # ➋ 假设我们将在线上游戏中使用这个,生成“适合用于加密”的随机字节序列
        self._items = []
        self.load(items)  # ➌ 委托.load(...) 方法实现初始加载。

    def load(self, items):
        self._items.extend(items)
        self._randomize.shuffle(self._items)  # ➍ 没有使用random.shuffle() 函数,而是使用SystemRandom 实例的.shuffle() 方法。

    def pick(self):  # ➎ pick 方法的实现方式与示例5-8 一样。
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingoCage')

    def __call__(self):  # ➏ __call__ 也跟示例5-8 中的一样。它没必要满足Tombola 接口,添加额外的方法没有问题。
        return self.pick()  # 书中的例子没有return,返回值都是None


if __name__ == '__main__':
    # f = Fake()
    b = BingoCage([1, 2, 3, 4])
    print(b.__dict__)
    print(b())
    print(b())
    print(b())
    print(b())
3. Tombola 虚拟子类

白俄类型得一个基本特性,即便不继承,也有办法把一个类注册为抽象基类得虚拟子类.这样做时,我们保证注册得类 忠实的实现了抽象基类定义的接口,而python会相信我们,从而不做检查,但是如果我们说谎了,那么常规运行时异常会把我们捕获

注册虚拟子类得方式是在抽象基类上调用register方法,这么做之后,注册的类会变成抽象基类得虚拟子类,而且issubclassisinstance等函数都能识别,但是注册得类不会从抽象基类中继承任何方法喝属性

虚拟子类不会继承注册得抽象基类,而且任何时候都不会检查他是否符合抽象基类得接口,即使在实例化时也不检查,为了避免运行错误,虚拟子类要实现所需得全部方法

register方法通常作为普通函数调用,不过也可以作为装饰器使用,在下属例子中,我们使用装饰器语法实现了Tombolist类,这是Tombola得虚拟子类

image-20230425103304878

@Tombola.register
class Tombolist(list):
    def pick(self):
        if self:
            position = random.randrange(len(self))
            return self.pop(position)
        else:
            raise

        load = list.extend

        def loaded(self):
            return bool(self)

        def inspect(self):
            return tuple(sorted(self))
        
# Tombola.register(TomboList)

看不懂

11.8 Tombola 子类得测试方法

11.9 python使用register得方式

11.7中得Tombola.register当作类装饰器使用,在python3.3 之前得版本中不能这样使用,必须在定义类之后像普通函数那样调用,如最后一行注释掉得写法

11.10 鹅得行为可能像鸭子

第十二章 继承和优缺点

12.1 子类化内置类型很麻烦

内置类型(C语言实现)不会调用用户定义的类覆盖得特殊方法

class DoppelDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, [value]*2)  # ➊ DoppelDict.__setitem__ 方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类


if __name__ == '__main__':
    dd = DoppelDict(one=1)  # ➋ 继承自dict 的__init__ 方法显然忽略了我们覆盖的__setitem__ 方法:'one' 的值没有重复。
    print(dd)
    dd["two"] = 2  # ➌ [] 运算符会调用我们覆盖的__setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即[2, 2]。
    print(dd)
    dd.update(three=3)  # ➍ 继承自dict 的update 方法也不使用我们覆盖的__setitem__ 方法:'three' 的值没有重复。
    print(dd)

子类化使用python编写得类,则不受影响

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

12.2 多重继承和方法解析顺序

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

image-20230426204234385

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('D ping-pong', self)

    def pingpong(self):
        self.ping()  # ➊ 第一个调用是self.ping(),运行的是D 类的ping 方法,输出这一行和下一行。
        print('-'*20)
        super().ping()  # ➋ 第二个调用是super().ping(),跳过D 类的ping 方法,找到A 类的ping 方法。
        print('-' * 20)
        self.pong()  # ➌ 第三个调用是self.pong(),根据__mro__,找到的是B 类实现的pong 方法。
        print('-' * 20)
        super().pong()  # ➍ 第四个调用是super().pong(),也根据__mro__,找到B 类实现的pong 方法。
        print('-' * 20)
        C.pong(self)  # ➎ 第五个调用是C.pong(self),忽略__mro__,找到的是C 类实现的pong 方法。


if __name__ == '__main__':
    d = D()
    d.pingpong()
A ping <__main__.D object at 0x000001C420E99D90>  ➊ 第一个调用是self.ping(),运行的是D 类的ping 方法,输出这一行和下一行。
D ping-pong <__main__.D object at 0x000001C420E99D90>
--------------------								D中的super()指向A
A ping <__main__.D object at 0x000001C420E99D90>  ➋ 第二个调用是super().ping(),跳过D 类的ping 方法,找到A 类的ping 方法。
--------------------							    D中的super()指向A
B pong <__main__.D object at 0x000001C420E99D90>  ➌ 第三个调用是self.pong(),根据__mro__,找到的是B 类实现的pong 方法。
--------------------								D没有实现pong(),这里是继承自B的pong()
B pong <__main__.D object at 0x000001C420E99D90>  ➍ 第四个调用是super().pong(),也根据__mro__,找到B 类实现的pong 方法。
--------------------
C pong <__main__.D object at 0x000001C420E99D90>  ➎ 第五个调用是C.pong(self),忽略__mro__,找到的是C 类实现的pong 方法。

通过D.__mro__来查看继承属性为(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

12.3 多重继承的真实应用

多重继承能发挥积极作用,《设计模式:可复用面向对象软件的基础》一书中的适配器模式用的就是多重继承,因此使用多重继承肯定没错(虽然其他的设计模式都是单继承)

在python标准库中,最常使用多重继承的是collections.abc包.这没什么问题,毕竟连java都支持接口的多重继承,而抽象基类就是接口声明,只不过它可以提供具体方法的实现

在标准库中,GUI工具包Tkinter把多重继承用到了极致(了解)

image-20230426211816711

12.4 处理多重继承

继承有很多用途,而多重继承增加了可选方案和复杂度.使用多重继承容易得出令人费解和脆弱的设计,下面是避免把类图搅乱的一些建议

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

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

    • 继承接口,创建子类,实现是什么关系
    • 继承实现,通过重用避免代码重复

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

  2. 使用抽象基类显示表示接口

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

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

  4. 在名称中明确指明混入

    因为在python中没有把类声明为混入的正规方法, 所以强烈推荐在名称中加入mixin后缀

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

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

    抽象基类有一个局限是混入没有的:抽象基类中的具体方法只是一种便利措施,因为这些方法所做的一切,用户调用抽象基类中的其它方法也能做到

  6. 不要子类化多个具体类

    具体类可以没有,或者最多只有一个具体超类,也就是说,具体类的超类中除了这一个具体超类之外,其余的都是抽象基类或混入,例如,在下述代码中,如果Alpha是具体类,那么BetaGamma必须是抽象基类或混入

    class MyConcreteClass(Alpha, Beta, Gamma):
        """这是一个具体类,可以实例化"""
        pass
    
  7. 为用户提供聚合类

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

  8. 优先使用对象组合,而不是类继承

第十三章 正确重载运算符

运算符重载得作用是让用户自定义的对象使用中辍运算符或一元运算符.说的宽泛一些,在python中,函数调用(),属性访问.,和元素访问/,切片[]也是运算符,本章只讨论一元运算符和中缀运算符.

接下来的几节,我们讨论

  • python如何处理中辍运算符中不同类型得操作数
  • 使用鸭子类型或显式类型检查处理不同类型得操作数
  • 中辍运算符如何表明自己无法处理操作数
  • 众多比较运算符得特殊行为
  • 增量赋值运算符得默认处理方式和重载方式

13.1 运算符重载基础

python得限制:

  • 不能重载内置类型得运算符
  • 不能新建运算符,只能重载现有的
  • 某些运算符不能重载(is,and,or,not)

13.2 一元运算符

  • __neg__一元取负算术运算符如果x=2那么 -x==-2
  • __pos__一元取正算术运算符x==+x
  • __invert__对整数按位取反,~x==-(x+1)

13.3 重载向量加法运算符

13.4 重载标量乘法运算符

13.5 众多比较运算符

13.6 增量赋值运算符

第五部分 控制流程

第十四章 可迭代对象,迭代器和生成器

所有生成器都是迭代器,因为生成器完全实现了迭代器接口,迭代器用于从集合中去除元素,生成器用于凭空生成元素

在python中所有集合都可以迭代,在python语言内部,迭代器用于支持

  • for循环
  • 构建和扩展集合类型
  • 逐行遍历文本文件
  • 列表推导,字典推导和集合推导
  • 元组拆包
  • 调用函数时,使用*拆包实参

本章涵盖一下话题

  • 语言内部使用iter()内置函数处理可迭代对象得方式
  • 如何使用python实现经典的迭代器模式
  • 详细说明生成器函数得工作原理
  • 如何使用生成器函数或生成器表达式代替经典得迭代器
  • 如何使用标准库中通用得生成器函数
  • 如何使用yield from语句合并生成器
  • 案例分析,在一个数据库转换工具中使用生成器函数处理大型数据集
  • 为什么生成器和协程看似相同,实则差别很大,不能混淆

14.1 Sentence类第一版:单词序列

定义一个Sentence类,通过索引从文本中提取单词

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 f'Sentence {reprlib.repr(self.text)}'

if __name__ == '__main__':
    s = Sentence('"The time has come," the Walrus said,')
    for i in s:
        print(i)

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

解释器需要迭代对象x时,会自动调用iter(x)

内置的iter函数有以下作用

  1. 检查对象是否实现了__iter__方法,如果实现了,就调用它获取一个迭代器
  2. 如果没有实现__iter__但实现了__getitem__方法,python会创建一个迭代器,创世按顺序获取元素
  3. 如果尝试失败,python会抛出TypeError异常

任何python序列都可以迭代的原因,他们都实现了__getitem__方法,其实,标准的序列也都是实现了__iter__方法,因此你也应该这么做

检查对象是否可迭代的最准确方法:调用iter(x)方法,如果不可迭代,在处理TypeError异常,这比使用isinstance(x, abc.Iterable)更准确,因为前者会考虑__getitem__方法

14.2 可迭代对象与迭代器的对比

可迭代对象:

  1. 使用iter内置函数可以获取迭代器的对象

  2. 如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的,序列都可以迭代

  3. 实现了__getitem__方法,而且其参数时从零开始的索引,这种对象也可以迭代

  4. 可迭代对象和迭代器之间的关系:python从可迭代对象中获取迭代器

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

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

    • __iter__返回self,以便在应该使用可迭代对象的地方使用迭代器

    • Iterator抽象基类实现__iter__方法的方式是返回实例本身,这样,在需要可迭代对象的地方可以使用迭代器

迭代器:

  1. 实现了无参数的__next__方法, 返回序列中的下一个元素,如果没有元素了,抛出StopIteration异常,python中的迭代器还实现了__iter__方法,因此迭代器也可以迭代

14.3 Sentence类第二版:典型的迭代器

import re

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

class Sentence:  # 可迭代对象
    def __init__(self, text):
        self.text = text
        self.words = RE_WORD.findall(text)

    def __iter__(self):  # ➊ 与前一版相比,这里只多了一个__iter__ 方法。这一版没有__getitem__ 方法,为的是明确表明这个类可以迭代,因为实现了__iter__ 方法。
        return SentenceIterator(self.words)  # ➋ 根据可迭代协议,__iter__ 方法实例化并返回一个迭代器

class SentenceIterator:  # 迭代器
    def __init__(self, words):
        self.words = words  # ➌ SentenceIterator 实例引用单词列表。
        self.index = 0  # ➍ self.index 用于确定下一个要获取的单词。

    def __next__(self):
        try:
            word = self.words[self.index]  # ➎ 获取self.index 索引位上的单词。
        except IndexError:
            raise StopIteration()  # ➏ 如果self.index 索引位上没有单词,那么抛出StopIteration 异常。

        self.index += 1  # ➐ 递增self.index 的值。
        return word  # ➑ 返回单词。

    def __iter__(self):  # ➒ 实现self.__iter__ 方法。
        return self

Sentence编程迭代器是坏主意

可迭代对象需要实现__iter__方法,每次调用返回一个新的迭代器,迭代器需要实现__iter____next__方法用来返回迭代器本身和单个元素

因此迭代器可以迭代,可迭代对象不一定是迭代器

迭代器必须能维持本身的内部状态,每次调用都新建一个独立的迭代器是个好习惯

14.4 Sentence类第三版:生成器函数

import re

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

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

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

该类中的__iter__方法实现了生成器,而上一个例子中,__iter__方法创建并返回一个迭代器

1. 生成器函数的工作原理

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

下面以一个特别简单的函数说明生成器的行为

def gen():  # ➊ 只要Python 函数中包含关键字yield,该函数就是生成器函数。
    yield 1  # ➋ 生成器函数的定义体中通常都有循环,不过这不是必要条件;这里我重复使用3 次yield。
    yield 2
    yield 3


if __name__ == '__main__':
    print(gen)
    print(gen())
    for i in gen():  # ➎ 生成器是迭代器,会生成传给yield 关键字的表达式的值。
        print(i)

    g = gen()  # ➏ 为了仔细检查,我们把生成器对象赋值给g。
    print(next(g))  # ➐ 因为g 是迭代器,所以调用next(g) 会获取yield 生成的下一个元素。
    print(next(g))
    print(next(g))
    print(next(g))
Traceback (most recent call last):
  File "D:\dev\fluent_python\5 第五部分 控制流程\14章 可迭代对象,迭代器和生成器\14.4 Sentence类第三版.py", line 39, in <module>
    next(g)
StopIteration
<function gen at 0x000002253886CEA0>  # ➌ 仔细看,gen 是函数对象。
<generator object gen at 0x00000225386AB3D0>  # ➍ 但是调用时,gen() 返回一个生成器对象。
1
2
3
1
2
3

14.5 Sentence类第四版:惰性实现

现有的Sentence类会在__init__方法中一次性的读取整个text

如果使用re.finditer函数来读取text,则可以返回一个生成器,这样可以节省大量内存

import re

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

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

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

14.6 Sentence类第五版:生成器表达式

import re

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

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

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

14.7 何时使用生成器表达式

14.8 等差数列生成器

14.9 标准库中的生成器函数

1. 用于过滤的生成器函数
模块函数说明
itertoolscompress(it, selector_it)并行处理两个可迭代的对象;如果selector_it 中的元素是真值,产出it 中对应的元素
itertoolsdropwhile(predicate, it)处理it,跳过predicate 的计算结果为真值的元素,然后产出剩下的各个元素(不再进一步检查)
filter(predicate, it)把it 中的各个元素传给predicate,如果predicate(item)返回真值,那么产出对应的元素;如果predicate 是None,那么只产出真值元素
itertoolsfilterfalse(predicate, it)与filter 函数的作用类似,不过predicate 的逻辑是相反的:predicate 返回假值时产出对应的元素
itertoolsislice(it, stop)islice(it,start, stop, step=1)产出it 的切片,作用类似于s[:stop] 或s[start:stop:step],不过it 可以是任何可迭代的对象,而且这个函数实现的是惰性操作
itertoolstakewhile(predicate, it)predicate 返回真值时产出对应的元素,然后立即停止,不再继续检查
2. 用于映射的生成器函数

在输入的单个可迭代对象(map starmap 函数处理多个可迭代的对象)中的各个元素上做计算,然后返回结果

模块函数说明
itertoolsaccumulate(it, [func])产出累积的总和;如果提供了func,那么把前两个元素传给它,然后把计算结果和下一个元素传给它,以此类推,最后产出结果
(内置)enumerate(iterable, start=0)产出由两个元素组成的元组,结构是(index, item),其中index 从start 开始计数,item 则从iterable 中获取
(内置)map(func, it1, [it2, ..., itN])把it 中的各个元素传给func,产出结果;如果传入N 个可迭代的对象,那么func 必须能接受N 个参数,而且要并行处理各个可迭代的对象
itertoolsstarmap(func, it) 把it把it 中的各个元素传给func,产出结果;输入的可迭代对象应该产出可迭代的元素iit,然后以func(*iit) 这种形式调用func
3. 用于合并的生成器函数

这些函数都从输入的多个可迭代对象中产出元素。

模块函数说明
itertools chain(it1, ..., itN)先产出it1 中的所有元素,然后产出it2 中的所有元素,以此类推,无缝连接在一起
itertools chain.from_iterable(it)产出it 生成的各个可迭代对象中的元素,一个接一个,无缝连接在一起;it 应该产出可迭代的元素,例如可迭代的对象列表
itertools product(it1, ...,<br/>itN, repeat=1)计算笛卡儿积:从输入的各个可迭代对象中获取元素,合并成由N个元素组成的元组,与嵌套的for 循环效果一样;repeat 指明重复处理多少次输入的可迭代对象
(内置)zip(it1, ..., itN)并行从输入的各个可迭代对象中获取元素,产出由N 个元素组成的元组,只要有一个可迭代的对象到头了,就默默地停止
itertools zip_longest(it1, ...,<br/>itN, fillvalue=None)并行从输入的各个可迭代对象中获取元素,产出由N 个元素组成的元组,等到最长的可迭代对象到头后才停止,空缺的值使用
fillvalue 填充

4. 把输入的各个元素扩展成多个输出元素的生成器函数

模块函数说明
itertoolscombinations(it,<br/>out_len)把it 产出的out_len 个元素组合在一起,然后产出
itertoolscombinations_with_re<br/>placement(it, out_len)把it 产出的out_len 个元素组合在一起,然后产出,包含相同元素的组合
itertoolscount(start=0, step=1)从start 开始不断产出数字,按step 指定的步幅增加
itertoolscycle(it)从it 中产出各个元素,存储各个元素的副本,然后按顺序重复不断地产出各个元素
itertoolspermutations(it,<br/>out_len=None)把out_len 个it 产出的元素排列在一起,然后产出这些排列;out_len 的默认值等于len(list(it))
itertoolsrepeat(item, [times])重复不断地产出指定的元素,除非提供times,指定次数
5. 用于重新排列元素的生成器函数
模块函数说明
itertoolsgroupby(it,<br/>key=None)产出由两个元素组成的元素,形式为(key, group),其中key 是分组标准,group 是生成器,用于产出分组里的元素
(内置)reversed(seq)从后向前, 倒序产出seq 中的元素;seq 必须是序列, 或者是实现了reversed__ 特殊方法的对象
itertoolstee(it, n=2)产出一个由n 个生成器组成的元组,每个生成器用于单独产出输入的可迭代对象中的元素

14.10 yield from

如果生成器函数需要产出另一个生成器生成的值,传统的解决方式是使用嵌套的for循环

def chain(*iterable):
    for i in iterable:
        print(i)
        for j in i:
            yield j


if __name__ == '__main__':
    s = 'abc'
    t = tuple(range(3))
    print(list(chain(s, t)))
def chain(*iterable):
    for i in iterable:
        print(i)
        yield from i


if __name__ == '__main__':
    s = 'abc'
    t = tuple(range(3))
    print(list(chain(s, t)))

yield from替代一层for循环

14.11 可迭代的归约函数

归约函数:接受一个可迭代的对象,然后返回单个结果的函数

模块函数说明
(内置)all(it)it 中的所有元素都为真值时返回True,否则返回False;all([]) 返回True
(内置)any(it)只要it 中有元素为真值就返回True,否则返回False;any([]) 返回False
(内置)max(it, [key=,default=])返回it 中值最大的元素;*key 是排序函数,与sorted 函数中的一样;如果可迭代的对象为空,返回default
(内置)max(it, [key=,default=])返回it 中值最小的元素;#key 是排序函数,与sorted 函数中的一样;如果可迭代的对象为空,返回default
functoolsreduce(func, it,<br/>[initial])把前两个元素传给func,然后把计算结果和第三个元素传给func,以此类推,返回最后的结果;如果提供了initial,把它当作第一个元素传入
(内置)sum(it, start=0)it 中所有元素的总和,如果提供可选的start,会把它加上(计算浮点数的加法时,可以使用math.fsum 函数提高精度)

14.12 深入分析iter函数

iter函数可以传两个参数:第一个参数必须是可调用对象,用于不断调用,产出各个值,第二个值是标记值,当前面产出的值等于标记值时,停止迭代(书上所说的产出值等于标记值时会抛异常的行为,在python3.11未发生,原因未知)

import random


def d6():
    return random.randint(1, 6)

d = iter(d6, 1)
try:
    for i in d:
        print(i)
except Exception as e:
    print(e)

14.13 案例分析:在数据库转换工具中使用生成器

14.14 把生成器当成协程

send()方法:与__next__()方法一样,可以使生成器前进到下一个yield语句,不过send()方法还允许使用生成器的客户把数据发给自己,即不管传给send()什么参数,那个参数都能成为生成器函数定义体中对应的yield表达式的值,也就是说send()方法允许在客户代码和生成器之间双向交换数据,而__next__方法只允许从生成器中获取值

这样使用的话, 生成器本身就变成了协程,在16章讲解

第十五章 上下文管理器和else

本章讨论一些流控制特性,主要有以下

  • with语句和上下文管理器
  • for,while,try语句的else子句

with语句会设置一个临时的上下文,交给上下文管理器对象控制,并且负责清理上下文,这么做能避免错误并减少样板代码,因此API更安全,而且更易于使用,除了自动关闭文件之外,with块还有很多用途

15.1 先做这个,再做那个:if语句之外的else块

else子句的行为如下:

  • for

    仅当for循环运行完毕时(没有被break提前终止),才运行else

  • while

    仅当while循环因为条件为假值而退出时(没有被break提前终止),才运行

  • try

    仅当try块中没有异常抛出,时才运行,(else子句抛出的一场不会有前面的except子句处理)

在所有情况下,如果一场或者return,break,continue语句导致控制权跳到了复合语句主块之外,else子句也会被跳过

在if语句中,else表示要么运行这个,要么运行那个

而在其他的语句中,else表示运行完这个,然后运行那个

15.2 上下文管理器和with块

with语句的目的是简化try/finally模式,这种模式用于保证一段代码运行完毕后执行某项操作,即便那段代码由于异常,return等提前终止,也会执行指定的操作,finally子句中的代码通常用于释放重要的资源,或者还原临时变更的状态

上下文管理器协议包含__enter____exit__两个方法,with语句开始运行时,会在上下文管理器对象上调用__enter__方法,with语句运行结束后,会在上下文管理器对象上调用__exit__方法,以此扮演finally子句的角色

class LookingGlass:
    def __enter__(self):  # ➊ 除了self 之外,Python 调用__enter__ 方法时不传入其他参数。
        import sys
        self.original_write = sys.stdout.write  # ➋ 把原来的sys.stdout.write 方法保存在一个实例属性中,供后面使用。
        sys.stdout.write = self.reverse_write  # ➌ 为sys.stdout.write 打猴子补丁,替换成自己编写的方法。
        return 'JABBERWOCKY'  # ➍ 返回'JABBERWOCKY' 字符串,这样才有内容存入目标变量what。

    def reverse_write(self, text):  # ➎ 这是用于取代sys.stdout.write 的方法,把text 参数的内容反转,然后调用原来的实现。
        self.original_write(text[::-1])

    def __exit__(self, exc_type, exc_value, traceback):  # ➏ 如果一切正常,Python 调用__exit__ 方法时传入的参数是None, None, None;如果抛出了异常,这三个参数是异常数据,如下所述。
        import sys  # ➐ 重复导入模块不会消耗很多资源,因为Python 会缓存导入的模块。
        sys.stdout.write = self.original_write  # ➑ 还原成原来的sys.stdout.write 方法。
        if exc_type is ZeroDivisionError:  # ➒ 如果有异常,而且是ZeroDivisionError 类型,打印一个消息……
            print('Please DO NOT divide by zero!')
            return True  # ➓ ……然后返回True,告诉解释器,异常已经处理了。如果__exit__ 方法返回None,或者True 之外的值,with 块中的任何异常都会向上冒泡。

传给__exit__的三个参数分别如下

  • exc_type: 异常类
  • exc_value:异常实例
  • traceback:traceback对象

15.3 contextlib模块中的实用工具

  • closing

    如果对象提供了close()方法,但是没有实现__enter____exit__协议,那么可以使用这个函数构建上下文管理器

  • supperss

    构建临时忽略指定异常的上下文管理器

  • @contextmanager

    把简单的生成器函数变成上下文管理器

  • ContextDecorator

    这是个基类,用于定义基于类的上下文管理器,这种上下文管理器也能用于装饰函数,在受管理的上下文中运行整个函数

  • ExitStack

    这个上下文管理器能进入多个上下文管理器,with块结束时,ExitStack按照后进先出的顺序调用栈中各个上下文管理器的__exit__方法,如果事先不知道with块要进入多少个上下文管理器,可以使用这个类,例如同时打开任意一个文件里表中的所有文件

15.4 使用@contextmanager

@contextmanager装饰器能减少创建上下文管理器的样板代码量,因为不用编写一个完整的类,定义__enter____exit__方法 ,而只需实现一个yield语句的生成器

@contextlib.contextmanager  # ➊ 应用contextmanager 装饰器
def looking_glass():
    original = sys.stdout.write  # ➋ 贮存原来的sys.stdout.write 方法。

    def reverse_writer(text):  # ➌ 定义自定义的reverse_write 函数;在闭包中可以访问original_write。
        original(text[::-1])

    sys.stdout.write = reverse_writer  # ➍ 把sys.stdout.write 替换成reverse_write。

    yield 'JABBERWOCKY'  # ➎ 产出一个值,这个值会绑定到with 语句中as 子句的目标变量上。执行with 块中的代码时,这个函数会在这一点暂停。
    sys.stdout.write = original  # ➏ 控制权一旦跳出with 块,继续执行yield 语句之后的代码;这里是恢复成原来的sys.stdout.write 方法。


if __name__ == '__main__':
    with looking_glass() as what:
        print(what)
    print(what)

上述代码有个问题,如果with模块中抛出了异常,python解释器会将其捕获,从而中止函数,所以需要在yield语句处添加try方法

第十六章 协程

本章首先要介绍生成器如何变成协程,然后再进入核心内容,本章涵盖一下话题

  • 生成器作为协程使用时的行为和状态
  • 使用装饰器自动预激协程
  • 调用方如何使用生成器对象的close()throw()方法控制协程
  • 协程终止时如何返回值
  • yield from新语句的用途和语义
  • 使用案例–使用写成管理仿真系统中的并发活动

16.1 生成器如何进化成协程

PEP 342:生成器的调用方可以使用.send()方法发送数据,发送的数据会成为生成器函数中yeild表达式的值,因此生成器可以作为协程使用,协程是指一个过程,这个过程与调用方协作,产出由调用方提供的值

PEP 342:除了.send()外,添加.throw().close()方法, 前者作用是让调用方抛出异常,在生成中处理,后者的作用时终止生成器

PEP 380:改动生成器:

  • 现在生成可以返回一个值,以前,如果生成中给 return语句提供值,会抛出异常
  • 新引入yield from语句,使用它可以把复杂的生成器构建成小型的嵌套生成器,省去了之前把生成器的工作委托给子生成器所需的样板代码

16.2 用作协程的生成器的基本行为

简单例子

def simple_coroutine():  # ➊ 协程使用生成器函数定义:定义体中有yield 关键字。
    print('-> coroutine started')
    x = yield  # ➋ yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是None——这个值是隐式指定的,因为yield 关键字右边没有表达式。
    print('-> coroutine received', x)

if __name__ == '__main__':
    my_coro = simple_coroutine()
    print(my_coro)  # ➌ 与创建生成器的方式一样,调用函数得到生成器对象。
    next(my_coro)  # ➍ 首先要调用next(...) 函数,因为生成器还没启动,没在yield 语句处暂停,所以一开始无法发送数据。
    my_coro.send(1)  # ➎ 调用这个方法后,协程定义体中的yield 表达式会计算出42;现在,协程会恢复,一直运行到下一个yield 表达式,或者终止。

协程的四个状态

  1. ‘GEN_CREATED’

    等待开始执行

  2. ‘GEN_RUNNING’

    解释器正在执行

  3. ‘GEN_SUSPENDED’

    在yield表达式处暂停

  4. ‘GEN_CLOSED’

    执行结束

状态可使用inspect.getgeneratorstate(my_coro)查看

因为send方法的参数会成为暂停的yield表达式的值,所以仅当协程处于暂停状态时才能调用send方法,如果协程没激活(GEN_CREATED状态),执行send方法报错TypeError: can't send non-None value to a just-started generator,所以需要使用next激活协程,或者执行my_coro.send(None)激活协程

书中另一个例子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmQqWKAH-1682955587324)(null)]

16.3 示例:使用协程计算移动平均值

def averager():
    total = 0
    count = 0
    average = None

    while True:  # ➊ 这个无限循环表明,只要调用方不断把值发给这个协程,它就会一直接收值,然后生成结果。仅当调用方在协程上调用.close() 方法,或者没有对协程的引用而被垃圾回收程序回收时,这个协程才会终止。
        term = yield average  # ➋ 这里的yield 表达式用于暂停执行协程,把结果发给调用方;还用于接收调用方后面发给协程的值,恢复无限循环。
        total += term
        count += 1
        average = total / count

if __name__ == '__main__':
    coro = averager()  # ➊ 创建协程对象。
    print(next(coro))  # ➋ 调用next 函数,预激协程。
    print(coro.send(10))  # ➌ 计算移动平均值:多次调用.send(...) 方法,产出当前的平均值。
    print(coro.send(4))
    print(coro.send(4))

16.4 预激协程的装饰器

如果不预激,协程没什么用,调用coro.send(x)之前,一定要调用next(coro),为了简化协程的用法,有时会使用一个预激装饰器

def coroutine(func):
    @functools.wraps(func)
    def primer(*args, **kwargs):  # ➊ 把被装饰的生成器函数替换成这里的primer 函数;调用primer 函数时,返回预激后的生成器。
        gen = func(*args, **kwargs)  # ➋ 调用被装饰的函数,获取生成器对象。
        next(gen)  # ➌ 预激生成器。
        return gen  # ➍ 返回生成器。
    return primer


@coroutine  # ➎ 把装饰器应用到averager 函数上。
def averager():
    total = 0
    count = 0
    average = 0
    while True:
        num = yield average
        total += num
        count += 1
        average = total / count


if __name__ == '__main__':
    coro = averager()  # ➊ 调用averager() 函数创建一个生成器对象,在coroutine 装饰器的primer 函数中已经预激了这个生成器。
    print(coro.send(10))  # ➌ 可以立即开始把值发给coro_avg——这正是coroutine 装饰器的目的。
    print(coro.send(4))
    print(coro.send(4))

16.5 终止协程和异常处理

  1. generator.throw(exc_type[, exc_value[, traceback]])

    致使生成器在暂停的yield表达式处抛出指定的异常,如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用gendeator.throw方法得到的返回值,如果生成器没有处理抛出的一场,异常会向上冒泡,传到调用方的上下文中

  2. generator.close()

    致使生成器在暂停的yield表达式处抛出 GeneratorExit异常,如果生成器没有处理这个异常,或者抛出StopIteration异常,调用方不会报错,如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常,生成器抛出的其他异常会向上冒泡,传给调用方.

16.6 让协程返回值

下述例子:协程不再每次都返回值,而是在最后返回值

class Result(NamedTuple):
    count: int
    average: int | None
def averager():
    total = 0
    count = 0
    average = None
    while True:
        term = yield average
        if term is None:
            break
        total += term
        count += 1
        average = total / count

    return Result(count=count, average=average)


if __name__ == '__main__':
    a = averager()
    next(a)
    a.send(10)
    a.send(4)
    a.send(4)
    a.send(6)
    print(a.send(None))  # StopIteration: Result(count=4, average=6.0)
        # print(a.send(None))
    try:
        a.send(None)
    except StopIteration as e:
        print(e.value)  # Result(count=4, average=6.0)

代码最后可以获得结果,但是是以异常的方式,可以通过捕获异常并获取其value的方式获得协程的返回值

16.7 使用yield from

yield fromyield的作用多很多

  1. yield from可以用于简化for循环中的yield表达式

    def gen():
        for c in 'abc':
            yield c
        for i in range(10):
            yield i
    	
    # 可以改写成下面的代码
    def gen():
        yield from 'abc'
        yield from range(10)
    
  2. yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,这样二者可以直接发送和产生值,还可以直接传入异常.而不用在位于中间的协程中添加大量处理异常的样板代码

若想使用yield from结构,需要大幅改动代码,为了说明改动的部分,PEP 380添加了一些术语

  1. 委派生成器

    包含yield from <iterable>表达式的生成器函数

  2. 子生成器

    yield from表达式中的部分获取的生成器

  3. 调用方

    调用委派生成器的客户端代码

image-20230501095001102

16.8 yield from的意义

第十七章 使用future处理并发

第十八章 使用asyncio包处理并发

第六部分 元编程

第十九章 动态属性和特性

在python中,数据的属性和处理数据的方法统称为属性,其中方法只是可调用的属性,除二者之外,我们还可以串讲特性,在不改变类接口的前提下,使用存取方法,修改数据属性,这与统一访问原则相符

不管服务是由储存还是计算实现的,一个模块提供的所有服务都应该通过统一的方式使用

除了特性外,python还提供了丰富的API,用于控制属性的访问权限,以及实现动态属性

  • 使用点号访问属性时(obj.attr),python解释器会调用特殊的方法计算属性

19.1 使用动态属性转换数据

{
  "Schedule": {
    "conferences": [
      {
        "serial": 115
      }
    ],
    "events": [
      {
        "serial": 34505,
        "name": "Why Schools Don´t Use Open Source to Teach Programming",
        "event_type": "40-minute conference session",
        "time_start": "2014-07-23 11:30:00",
        "time_stop": "2014-07-23 12:10:00",
        "venue_serial": 1462,
        "description": "Aside from the fact that high school programming...",
        "website_url": "http://oscon.com/oscon2014/public/schedule/detail/34505",
        "speakers": [
          157509
        ],
        "categories": [
          "Education"
        ]
      }
    ],
    "speakers": [
      {
        "serial": 157509,
        "name": "Robert Lefkowitz",
        "photo": null,
        "url": "http://sharewave.com/",
        "position": "CTO",
        "affiliation": "Sharewave",
        "twitter": "sharewaveteam",
        "bio": "Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup..."
      }
    ],
    "venues": [
      {
        "serial": 1462,
        "name": "F151",
        "category": "Conference Venues"
      }
    ]
  }
}

对json数据进行处理

import json


def load():
    file = '../../utils/osconfeed.json'
    with open(file, encoding='utf-8') as f:
        return json.load(f)


if __name__ == '__main__':
    feed = load()
    for key, value in sorted(feed['Schedule'].items()):
        print(key, '|', value)
        print('-' * 20)
    print(feed['Schedule']['speakers'][-1]['name'])
conferences | [{'serial': 115}]
--------------------
events | [{'serial': 34505, 'name': 'Why Schools Don´t Use Open Source to Teach Programming', 'event_type': '40-minute conference session', 'time_start': '2014-07-23 11:30:00', 'time_stop': '2014-07-23 12:10:00', 'venue_serial': 1462, 'description': 'Aside from the fact that high school programming...', 'website_url': 'http://oscon.com/oscon2014/public/schedule/detail/34505', 'speakers': [157509], 'categories': ['Education']}]
--------------------
speakers | [{'serial': 157509, 'name': 'Robert Lefkowitz', 'photo': None, 'url': 'http://sharewave.com/', 'position': 'CTO', 'affiliation': 'Sharewave', 'twitter': 'sharewaveteam', 'bio': 'Robert ´r0ml´ Lefkowitz is the CTO at Sharewave, a startup...'}]
--------------------
venues | [{'serial': 1462, 'name': 'F151', 'category': 'Conference Venues'}]
--------------------
Robert Lefkowitz
1. 使用动态属性访问JSON类数据

首先构建一个FrozenJSON方法

class FrozenJson:
    def __init__(self, mapping):
        self.__data = dict(mapping)  # ➊ 使用mapping 参数构建一个字典。这么做有两个目的:(1) 确保传入的是字典(或者是能转换成字典的对象);(2) 安全起见,创建一个副本。

    def __getattr__(self, name):  # ➋ 仅当没有指定名称(name)的属性时才调用__getattr__ 方法。
        if hasattr(self.__data, name):
            return getattr(self.__data, name)  # ➌ 如果name 是实例属性__data 的属性,返回那个属性。调用keys 等方法就是通过这种方式处理的。
        else:
            return FrozenJson.build(self.__data[name])  # ➍ 否则,从self.__data 中获取name 键对应的元素,返回调用FrozenJSON.build() 方法得到的结果。

    @classmethod
    def build(cls, obj):  # ➎ 这是一个备选构造方法,@classmethod 装饰器经常这么用。
        if isinstance(obj, abc.Mapping):  # ➏ 如果obj 是映射,那就构建一个FrozenJSON 对象
            return cls(obj)
        elif isinstance(obj, abc.MutableSequence):  # ➐ 如果是MutableSequence 对象,必然是列表,6 因此,我们把obj 中的每个元素递归地传给.build() 方法,构建一个列表。
            return [cls.build(item) for item in obj]
        else:  # ➑ 如果既不是字典也不是列表,那么原封不动地返回元素。
            return obj
import json

from utils.frozen_json import FrozenJSON


def load():
    file = '../../utils/osconfeed.json'
    with open(file, encoding='utf-8') as f:
        return json.load(f)


if __name__ == '__main__':
    feed = load()
    feed = FrozenJSON(feed)
    print(feed.Schedule.speakers[-1].name)

通过__getattr__方法,如果存在该属性,获取对应属性,如果不存在该属性,通过构建的build方法获取属性,

2. 处理无效属性名

FrozenJSON类存在一个缺陷,如果对应属性为python关键字,则会报错

grad = FrozenJSON({'name': 'Jim Bo', 'class': 1982})
print(grad.class)  # SyntaxError: invalid syntax

可以通过在构建FrozenJSON类时,在init方法中将属性后添加_,使之与python关键字区分开来完成

3. 使用__new__方法以灵活的方式创建对象

我们通常把__init__方法成为构造方法,这是从其他语言借鉴过来的术语,其实用于构建实例的时特殊方法__new__,这是个类方法,必须返回一个实例,返回的实例作为第一个参数(self)传给__init__,因为调用__init__方法时要传入实例,而且禁止返回值,所以__init__方法其实是初始化方法,真正的构造方法时__init__,我们几乎不需要自己编写__new__方法,因为从object类继承的实例已经足够了

4. 使用shelve模块调整OSCON数据源的结构
5. 使用特性获取链接的记录

19.2 使用特性验证属性

使用@property装饰器可以实现只读特性,本节要创建一个可读写的特性

1. LineItem类第一版:表示订单中的商品的类

假设有一个销售散装有机食物的电商应用,客户可以按重量订购坚果,干果,杂粮,在这个系统中,每个订单中都有一系列商品,而每个商品都可以使用下面的类表示

class LineItem:
    def __init__(self, desc, weight, price):
        self.desc = desc
        self.weight = weight
        self.price = price

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

这个代码有个问题,当输入的重量是负值时,金额小计为负值

2. 1. LineItem类第二版:能验证值的特性
class LineItem_2:
    def __init__(self, desc, weight, price):
        self.desc = desc
        self.weight = weight  # ➊ 这里已经使用特性的设值方法了,确保所创建实例的weight 属性不能为负值。
        self.price = price

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

    @property  # ➋ @property 装饰读值方法。
    def weight(self):  # ➌ 实现特性的方法,其名称都与公开属性的名称一样——weight。
        return self.__weight  # ➍ 真正的值存储在私有属性__weight 中。

    @weight.setter  # ➎ 被装饰的读值方法有个.setter 属性,这个属性也是装饰器;这个装饰器把读值方法和设值方法绑定在一起。
    def weight(self, value):
        if value > 0:
            self.__weight = value  # ➏ 如果值大于零,设置私有属性__weight。
        else:
            raise ValueError('Value must > 0')  # ➐ 否则,抛出ValueError 异常。

19.3 特性全解析

虽然内置的property经常作为装饰器使用,但他其实一个类,在python中,函数和类通常可以互换,因为二者都是可调用对象,而且没有实例化对象的new运算符,所以调用构造方法与调用工厂函数没有区别,此外,只要能返回新的可调用对象,代替被装饰的函数,二者都可以用作装饰器

property构造方法的完整签名如下

property(fget=None, fset=None, doc=None)

所有参数都是可选的,如果没有把函数传给某个参数,那么得到的特性对象就不允许执行相应的操作

1. 特性会覆盖实例属性

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

如果实例和所属的类有同名数据属性,那么实例属性会覆盖(或称遮盖)类属性,至少通过那个实例读取属性时是这样

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值