流畅的Python学习

流畅的Python笔记

流畅的Python笔记

1 Python数据模型

2 数据结构

2.1 内置序列类型

2.2 列表推导与生成器表达式

2.3 元组

2.4 切片

2.5 序列对象上的+与*

2.6 sorted函数与list的sort方法

2.7 其他可选类型

2.7.1 array

2.7.2 memoryview

2.7.3 deque以及其他形式的队列

3 字典与集合

3.1 可hash

3.2 常见方法

3.3 其他dict

3.4 集合

3.5 dict与set的底层实现

4 文本与字节序列

4.1 文本与字节

4.2 编解码问题

4.3 Unicode字符比较时的规范化

4.4.1 大小写折叠

4.4.2 极端方式

4.5 Unicode文本排序

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

5 一等函数

5.1 函数基础知识

5.2 函数参数

5.3 函数注解

5.4 函数式编程

6 使用函数实现的设计模式

7 函数装饰器和闭包

7.1 装饰器

7.2 变量作用域规则

7.3 闭包

7.4 标准库中的装饰器

8 Python的面向对象

8.1 对象标识,相等性和别名

8.2 浅复制与深复制

8.3 参数与引用

8.4 垃圾回收与引用计数

9 Python风格的对象

10 序列的修改/散列与切片

11 接口

11.1 猴子补丁

11.2 标准库中的抽象基类

11.2.1 集合基类

11.2.2 数字基类

11.3 定义并使用抽象基类

12 继承与多重继承

12.1 内置类型的派生

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

13 重载运算符

14 可迭代对象与生成器

14.1 可迭代对象与迭代器

14.2 生成器

14.3 标准库中生成器

15 上下文管理器

15.1 else语句

15.2 with与上下文管理器

15.3 contextlib

16 协程

16.1 将生成器转化为协程

16.2 预激协程装饰器

16.3 让协程返回值

16.4 使用yield from

17 物理并发

17.1 并发

18 使用asyncio包

18.1 线程与协程对比

19 动态属性与特性

19.1 动态获取属性

19.2 深入特性

19.5 特性工厂函数

19.6 处理属性的相关属性与函数

19.6.1 特殊属性

19.6.2 内置函数

19.6.3 特殊方法

20 属性描述符

20.1 特性管理类

20.2 覆盖型与非覆盖型描述符对比

20.3 方法都是描述符

20.4 描述符最佳实践

21 类元编程

21.1 类工厂函数

21.2 定制描述符的类装饰器

21.3 导入时与运行时比较

21.4 元类基础

21.5 定制描述符的元类

21.6 元类特殊方法prepare

21.7 类作为对象

1 Python数据模型

这是流畅的Python第一章的扑克牌示例,使用collection中的类,并介绍了类的特殊方法

通过实现__getitem__可以使得类可以支持索引和迭代。通过实现__len__可以使得类可以计算长度。

下面的示例,使用了namedtuple类来创建新的类型Card.

from collections import namedtuple
from random import choice

Card = namedtuple('Card', ['Rank', 'Suite'])

class Deck:
    ranks = [item for item in range(2, 11)] + list('AJQK')
    suites = ['spades', 'diamonds' , 'clubs' , 'hearts']

    def __init__(self):
        self.cards = [Card(rank, suite) for rank in self.ranks for suite in self.suites]


    def __getitem__(self, idx):
        return self.cards[idx]


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


if __name__ == '__main__':
    deck = Deck()
    print(len(deck))
    print(choice(deck))

    print(deck[0:10])

#     for card in deck:
#         print card

    print(Card('Q', 'hearts') in deck)
52
Card(Rank='K', Suite='diamonds')
[Card(Rank=2, Suite='spades'), Card(Rank=2, Suite='diamonds'), Card(Rank=2, Suite='clubs'), Card(Rank=2, Suite='hearts'), Card(Rank=3, Suite='spades'), Card(Rank=3, Suite='diamonds'), Card(Rank=3, Suite='clubs'), Card(Rank=3, Suite='hearts'), Card(Rank=4, Suite='spades'), Card(Rank=4, Suite='diamonds')]
True

这是向量类

import math

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

    def __add__(self, a):
        return vector(self. x + a.x, self.y + a.y)

    def __abs__(self):
        return math.sqrt(self.x*self.x + self.y*self.y)

    def __repr__(self):
        return "Vector({}, {})".format(self.x, self.y)

    def __mul__(self, scalar):
        return vector(self.x * scalar, self.y * scalar)

if __name__ == '__main__':
    v1 = vector(3, 4)
    v2 = vector(1, 2)
    print v1+v2
    print abs(v1)

    print v1 * 3
    pass
Vector(4, 6)
5.0
Vector(9, 12)

2 数据结构

2.1 内置序列类型

Python标准库用C实现了丰富的序列类型:

类型名称
容器序列list, tuple, collections.deques
扁平序列str, bytes, bytearray, memoryview, array.array

容器序列存放的是它们包含的对象的引用,扁平序列存放的是值而非引用。

类型名称
可变序列list, collections.deques, bytearray, memoryview, array.array
不可变序列str, bytes, tuple

2.2 列表推导与生成器表达式

列表推导可以用来创建新的列表,并提高代码的可读性。列表推导的概念可以推广到集合推导,字典推导。

使用()可以生成generator对象,以供后续的遍历。

cards = [item for item in range(1,13) if item%2==0 ]
print cards

cards = (item for item in range(1,13))
print cards

dicts = {k:v for (k,v) in enumerate('ab')}
print dicts

sets = {x for x in range(1,6)}
print sets
[2, 4, 6, 8, 10, 12]
<generator object <genexpr> at 0x7f644fd8b9b0>
{0: 'a', 1: 'b'}
set([1, 2, 3, 4, 5])

2.3 元组

元组不仅仅是不可变的列表,它可以用于存储不需要字段名的记录。对记录的各字段通过位置索引进行访问,或利用元组拆包获得各字段的值。

拆包操作并不局限于元组这种数据类型,任意可迭代的数据类型都可以被拆包。

拆包里面可以使用*var来接收后续的若干个元素的列表,在python2中只能在函数传递形参时拆包,在python3中则不再有这种限制。

namdtuple是一种类型创建器,可以生成一个新的类型,新类型实例化的对象拥有命名的字段,可以通过字段访问属性值。

from collections import namedtuple

bush = ('Bush', 88, 'male', 'U.S.')
name, age, sex, country = bush

print(bush)
print(name, age, sex, country)
print("%s is %d years old, sexy is %s and come from the %s"%bush)

a, b, *rest, c = range(5)
print(a, b, rest, c)

Person = namedtuple('Person', ['name', 'age', 'sex', 'country'])
p = Person(*bush)
print(p)
('Bush', 88, 'male', 'U.S.')
Bush 88 male U.S.
Bush is 88 years old, sexy is male and come from the U.S.
0 1 [2, 3] 4
Person(name='Bush', age=88, sex='male', country='U.S.')

2.4 切片

切片操作是通过切片对象来操作的,[]操作符可以生成切片对象,还可以使用逗号分隔的多个索引或切片,外部库Numpy就用了这个特性。

省略(…)可以作为切片的一部分,用于多维数组切片的快捷方式。如果x是4维的,则x[i,…]就是x[i , : , : , :]的缩写。

如果把切片放在赋值语句的左边,或用于del操作,则可以对序列进行嫁接,切除或就地修改。

s="bicycle"
op = slice(0, len(s), 3)
print(s[op])
print(s[::3])

lst = list(range(0, 6))
lst[0:3] = [100,200]
print(lst)

bye
bye
[100, 200, 3, 4, 5]

2.5 序列对象上的+与*

序列相加被实现为拼接两个序列,序列乘整数n被实现为连续重复n遍。这两个操作都重新返回一个新的对象。

但是就地操作+=和*=则会修改原始的对象。id(obj)可以查看一个对象的内在地址,可以观察对象是否为同一个。

对于tuple,若其中存在可修改的元素,对其进行修改时,虽然会抛出异常,但修改仍在异常抛出前完成了,在某些情况下可能会出现问题,因此要避免在tuple中存储可修改的元素。

a = [1, 2, 3]
b = [4,5,6]

print(4*a)
print(a+b)

print(id(a))
print(id(b))

a += b
a *= 2

print(id(a))
print(id(b))

t = (1, 2, [30, 40])
# t[2] += [50, 60]
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3]
[1, 2, 3, 4, 5, 6]
140242249326280
140242249323080
140242249326280
140242249323080

2.6 sorted函数与list的sort方法

sort方法会进行就地的排序并返回None,sorted则返回一个新的列表。

内置模块bisect提供了对有序列表进行二分查找与插入的功能。

import bisect

lst = list(range(0, 10, 2))
print(lst)

position = bisect.bisect(lst, 4)
print(position)

bisect.insort(lst, 7)
print(lst)
[0, 2, 4, 6, 8]
3
[0, 2, 4, 6, 7, 8]

2.7 其他可选类型

在一些性能特定的场景下,list并不是最好的选择,下面介绍一些常用的容器结构。

2.7.1 array

类似于C的数组,不仅支持list的有关操作,还支持文件读取与保存的方法。在使用array的时候需要指定其数据类型,例如b表示byte类型,d表示float类型。

array在性能上比list要高很多,因此在存储大量数据的场景下,可以使用array。

2.7.2 memoryview

memeoryview是受numpy启发而在python3中引入的,它可以包装一段内存,让用户在不复制内容的情况下操作同一片内存的不同切片。

2.7.3 deque以及其他形式的队列

deque是一个双端队列,且为线程安全的。

from array import array

floats = array('d', [1.2, 1.3, 1.4, 1.5])

print(floats)

view = memoryview(array('b', [1,2,3,4]))
print(view.cast('H').tolist())
array('d', [1.2, 1.3, 1.4, 1.5])
[513, 1027]

3 字典与集合

3.1 可hash

所谓可hash是指对象在生命周期中其值不会发生改变,这样调用对象上的hash()方法,返回的值也不会改变。python中的不可变类型如str , bytes与数值类型都是可hash的。对于不可变容器类型frozeset也是可hash的。对于tuple,其包含的元素必须是不可变的时其才为可hash的。

x = (1, 2, (3, 4))
print(hash(x))

x = (1, 2, [3, 4])
try:
    print(hash(x))
except Exception as e: 
    print(e)
-2725224101759650258
unhashable type: 'list'

3.2 字典的常见方法

dict, defaultdict与OrderdDict的常用方法如下表所示

方法说明
clear清除所有元素
copy浅复制
fromkeys(iter)将迭代中的元素设置为字典的key
get(key, [default])获取指定key的值
items()返回所有键值对
keys()返回所有key
values()返回所有value
setdefault(k, [default])设置k的值为value而不论k是否存在
pop(k, [default])弹出k对应的value
popitem()随机弹出一个键值对
update(iter)更新一批值

3.3 其他dict

defaultdict提供了字典中不存在k时,读取这个k时自动返回一个初始化值的方法。否则你需要先判断k是否存在,若不存在先初始化,若存在才能读取其值返回。可以让代码实现的更简捷。

OrderdDict提供了按插入顺序遍历其中元素的字典。

ChainMap提供了可以将多个字典串联起来,从而用一行代码查找多个字典的类型,不建议对ChainMap对象进行写操作。

Counter提供了一个统计序列中出现频次的计数器类。

from collections import ChainMap

dict1 = {}
dict2 = {}
dict3 = {'a': 1}

map1 = ChainMap(dict1, dict2, dict3)
print(map1.get('a'))

map1['a'] = 3
print(map1.get('a'))

map1['b'] = 5
print(map1.get('b'))

print(dict1, dict2, dict3)
1
3
5
{'a': 3, 'b': 5} {} {'a': 1}

3.4 集合

集合中的每个元素都是唯一的,因此集合可以用于去重。此外集合还实现了集合运算。

集合运算符作用
I集合并运算,如AIB
&集合交集运算,如A&B
-集合差集运算,如A-B

集合的字面常量使用大括号,如{1, 2, 3},但如果要创建一个空集,就不能用{},必须是set()。

集合操作与比较

方法说明
intersection(iter)与迭代序列的值进行交运算
union(iter)与迭代序列的值进行并运算
difference(iter)与迭代序列的值进行差运算
issubset(iter)s是否为可迭代序列的子集

isupperset(iter)|s是否可迭代序列的超集|
|isdisjoint(s)|两个集合是否不相交|

其他操作

方法说明
s.add(e)向集合中加入元素
s.clear()
s.copy()
s.dicard(e)移除元素e
s.pop()移除一个元素,返回之
s.remove(e)移除元素
import dis

a = {1, 2, 3}
print(type(a))
print(a)

dis.dis('{1,2,3}')
<class 'set'>
{1, 2, 3}
  1           0 LOAD_CONST               0 (1)
              2 LOAD_CONST               1 (2)
              4 LOAD_CONST               2 (3)
              6 BUILD_SET                3
              8 RETURN_VALUE

3.5 dict与set的底层实现

dict与set 底层采用了hash表来实现存储,因此查找算法十分快,但缺点是占用空间比较大。

其在不断的插入过程中如果原有的存储空间不满足要求,算法会重建整个hash表,因此如果在遍历的时候去插入dict,可能会触发底层hash表重建,而重建过程会导致key值的重排,因此可能会跳过而无法访问一部分key值。

4 文本与字节序列

4.1 文本与字节

在python3中,字符串str的内部表示以unicode进行,当需要转换成其他编码时都要进行encode,而其他编码需要decode才可转换为unicode。
python3中bytes和bytearray用于存储字符数组,每个字符都由0-255之间的整数。bytes对像是不可变的,bytearray是可变的。str对象通过指定的编码可以生成bytes对象,str对象上不在有decode方法。可以认为str对象存储的是unicode序列,而bytes中存储的是各种实现编码的字节。因此bytes上支持decode()方法

在处理过程中对于输入,要尽可能早的将输入字节解码为unicode,而在输出时要尽可能晚的将unicode编码成字节流。标准IO库的open与write均遵照上面的原则。

b = u'中国'.encode('utf-8')
print(type(b))
print(type(b[0:1]))
print(b)

ba = bytearray(b)
print(type(ba))
print(type(ba[0:1]))
print(ba)

asc = u'中国'.encode('cp437', errors='replace')
print(asc)

try:
    c = u'中国'.decode('utf-8')
    print(type(c))
except Exception as e:
    print(e)
<class 'bytes'>
<class 'bytes'>
b'\xe4\xb8\xad\xe5\x9b\xbd'
<class 'bytearray'>
<class 'bytearray'>
bytearray(b'\xe4\xb8\xad\xe5\x9b\xbd')
b'??'
'str' object has no attribute 'decode'

4.2 编解码问题

语言提供了UnicodeEncodeError异常(将字符串编码为二进制序列时)与UnicodeDecodeError异常(将二进制序列转换为字符串时)。

如果要编码或解码的字符集中不包含要处理的字符,则会抛出异常。可以指定错误处理函数来防止抛出异常。常用的如errors=’ignore|replace’。ignore会悄悄的忽略错误,replace将无法处理的字符替换为?。

在python3中,源码文件中的字符默认为utf-8编码,而python2默认使用ascii编码,因此python2文件中若出现了非ascii字符,则需要在文件头显式的声明:#coding: utf8

python有个三方库叫chardetect,可以探测文件所使用的编码。

4.3 Unicode字符比较时的规范化

对于一些西欧语言,字符上面还有重音符号,这类字符可以由两种方式表示,其中一种是在后面追加一个字节表示重音符号。

两种方式会导致相同的字符串有不同长度的表示方式。对于这类字符串的比较需要使用unicodedata模块提供的Normalize函数对其进行处理。unicodedata模块是一个unicodedatabase,其中包含了所有unicode字符的定义,值,名字,类别等信息。

Normalize的第一个参数为规范化的方式,有NFC,NFD,NFKC,NFKD。NFC使用最少的码位构成等价的字符串,而NFD将组合字符分解成基本字符和单独的组合字符。而NFKC和NFKD则将组合字符分解为普通多个字符的组合,它们可以用在需要检索或索引的场合,如 42 4 2 会规范为42。

from unicodedata import normalize

s1='café'
s2='cafe\u0301'
print(normalize('NFC',s1))
print(normalize('NFC',s2))

café
café

4.4.1 大小写折叠

先把所有文本变成小写,这个功能由str.casefold()方法支持。其与str.lower()在一些码位上会有不同的结果。

在多数场合下不区分大小写的比较都可以使用NFC规范后接casefold来解决。

4.4.2 极端方式

极端方式是去掉所有变音符号,可以采用下面的函数。其缺陷在对于非拉丁字符只去掉变音符号并不能将它们变成ASCII字符。

import unicodedata

def shave_marks(txt):
    norm_txt = unicodedata.normalize('NFD', txt)
    shaved = ''.join(c for c in norm_txt if c not unicodedata.combining(c))
    return unicodedata.normalize('NFC', shaved)

4.5 Unicode文本排序

Python比较任何类型的序列时,会逐个比较序列的元素。对字符串来说,比较的是码位。对于非ASCII字符来说,这种比较方式不尽人意。非ASCII文本的标准排序方式是使用locale.strxfrm函数,这个函数会把字符串置换成适合所在区域进行比较的形式。因此在使用此函数之前,需要设置合适的区域setlocale(LC_COLLATE, ‘zh_CN.utf8’),同时还需要操作系统支持此选项。另外locale.setlocale()方法是非线程安全的,因此最好在程序开始的地方保证只设置一次。

然而由于不同操作系统对locale支持特性的不同,推荐使用PyPI中的PyUCA库来进行unicode排序。

import pyuca

collate = pyuca.Collator()
fruit = ['苹果', '桔子']
fruit_sorted = sorted(fruit, key=collate.sort_key)
print(fruit_sorted)

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

这些双模式的API根据输入的不同展现出不同的行为。re和os模块中就有这样的函数。

使用re模块,如果使用字节序列,则\d,\w等模式只能匹配ASCII字符。如果是使用字符串模式,则能匹配ASCII之外的数字和字母。字符串正则表达式有个re.ASCII标志,可以让模式只匹配ASCII字符。

使用os模块,如果使用字符串参数,则参数会被sys.getfilesystemcoding()得到的codec进行编码,在操作系统内部则使用相同的codec进行解码。这正是我们想要的行为。为了处理那些不能如此处理的字符,则需要将字节序列参数传递给os模块。

import re
import os

print(type(u'中'))
print(type('中'.encode('utf8')))

open(u'中','w')
open('华'.encode('utf8'), 'w')

a = os.listdir('.')
b = os.listdir(b'.')

print(a)
print(b)
<class 'str'>
<class 'bytes'>
['datalab', '中', '.cache', '.local', '华', '.forever', '.ipython', '.config']
[b'datalab', b'\xe4\xb8\xad', b'.cache', b'.local', b'\xe5\x8d\x8e', b'.forever', b'.ipython', b'.config']

5 一等函数

5.1 函数基础知识

在python中,函数是一等的。函数可以在运行时创建,可以赋值给其他变量,可以作为参数传给函数,能作为函数的返回结果。

内置的filter, map, reduce等都是高阶函数,可以接受函数作为参数。由于python后续引入了列表推导,map, filter的作用可以被替代。在python3中他们返回的是生成器。

匿名函数可以用lambda来创建,但由于语法限制,无法实现复杂的函数。

可以用callable()来判断一个对象是否可调用。

任何python的对象,只要实现了call方法,都可以表现的像函数。

5.2 函数参数

Python提供了灵活的参数处理机制,可以传入列表和字典参数,在调用函数时使用和*展开可迭代对象。

5.3 函数注解

python3的注解可以用来说明参数及返回值。在说明参数时,注解内容存放在:后。注解返回值存放在)->后。

import inspect

def my_add(a, b):
    '''This is Add function'''
    return a + b

print(my_add.__doc__)
print(dir(my_add))

fruits=['banana', 'orange','apple', ]
sorted(fruits, key=lambda word: word[::-1])


class Test:
    def __init__(self, msg):
        self.msg = msg


    def __call__(self):
        print(self.msg)


t = Test('hello world')
t()


def func_args(first, *lst, **kwargs):
    print(first)
    print(lst)
    print(kwargs)


func_args('first', 1, 2, 3, key=1, value=2)


def func_test(a, b, c):
    print(a,b,c)

args = {'a':1, 'b':2, 'c':3}
func_test(**args)

print(func_test.__defaults__)
print(func_test.__kwdefaults__)
print(func_test.__code__)


def annotation(a,b:int,c)->str:
    return 0

sig = inspect.signature(annotation)
print(sig.return_annotation)
for param in sig.parameters.values():
    print(param.annotation)
    print(param.name, param.default)
This is Add function
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
hello world
first
(1, 2, 3)
{'key': 1, 'value': 2}
1 2 3
None
None
<code object func_test at 0x7fec2ea2a4b0, file "<ipython-input-11-f146c66b8ebd>", line 36>
<class 'str'>
<class 'inspect._empty'>
a <class 'inspect._empty'>
<class 'int'>
b <class 'inspect._empty'>
<class 'inspect._empty'>
c <class 'inspect._empty'>

5.4 函数式编程

operator中提供了众多运算符的函数实现,可以方便的用于类似于reduce这类高阶函数中。

operator中还有一类函数,可以替代从序列中取出元素或读取对象属性的lambda表达式。itemgetter接受一个索引用于从序列中索取指定的值,而attrgetter授受一个字符串用于从对象上获取指定的属性值。

最后operator.methodcaller与functools.partial都可以绑定函数的若干参数。

from operator import add, mul, itemgetter, attrgetter, methodcaller
from functools import partial, reduce

numbers = [1,2,3,4,5]
prod = reduce(mul, numbers)

print(prod)


persons=[('Lucy', 23), ('Jim', 13), ('Lily', 18)]
print(sorted(persons, key=itemgetter(1)))

age_name = itemgetter(1, 0)
for item in persons:
    print(age_name(item))


get_doc = attrgetter('__doc__')

print(get_doc(mul))


upper = methodcaller('upper')
print(upper('hello'))

triple = partial(mul, 3)
print(triple(8))
120
[('Jim', 13), ('Lily', 18), ('Lucy', 23)]
(23, 'Lucy')
(13, 'Jim')
(18, 'Lily')
mul(a, b) -- Same as a * b.
HELLO
24

6 使用函数实现的设计模式

from abc import ABC, abstractmethod
from collections import namedtuple


Customer = namedtuple('Customer', (name, fidelity))

class LineItem:
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price

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

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

    def total(self):
        return sum(item.total() for item in self.cart)

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

        return self.total() - discount

    pass

class Promotion(ABC):
    @abstractmethod
    def discount(self, order):
        pass

class FidelityPromotion(Promotion):
    def discount(self, order):
        return order.total()

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

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

7 函数装饰器和闭包

7.1 装饰器

装饰器只是一种语法糖,本质是高阶函数。只需要实现一个高阶函数decorator,并在需要使用的地方,以@decorator放在要包装的函数声明之前即可。

装饰器在被装饰的函数定义之后立即运行。通常是在模块导入的时候。

7.2 变量作用域规则

Python中变量分为全局变量和局部变量。作用域因而分为全局作用域和局部作用域。

当读取一个变量时,会先在局部作用域中查找,再向全局中查找。若局部作用域中定义了和全局作用域相同的变量,则会屏蔽掉全局作用域的定义。

7.3 闭包

闭包是一种函数,它会保留定义它时存在的自由变量的绑定。这样当调用函数时,虽然自由变量的定义作用域不可用了,但是仍能使用那些绑定。

#装饰器示例代码

def my_deco(func, *args, **kwargs):
    def inner(*args, **kwargs):
        print('befor call')
        func(*args, **kwargs)
        print('end call')
    return inner()


@my_deco
def test():
    print('hello world')


test
# 变量作用域示例
a=3

def func():
    a=1
    print(a)

func()
print(a)

# 闭包示例

def make_avg():
    total, count = 0, 0

    def average(num):
        nonlocal total, count
        total += num
        count += 1
        return total / count   
    return average

avg = make_avg()

print(avg(10),avg(11),avg(12))
befor call
hello world
end call
1
3
10.0 10.5 11.0

7.4 标准库中的装饰器

@property
把读值函数映射为类的属性

@classmethod
声明方法为操作类的方法,其第一个参数为所在类的类型

@staticmetod
声明方法为类的静态方法,不需要带类的实例参数

@functools.wraps,其作用是帮忙构建行为良好的装饰器。

@functools.lru_cache(maxsize,typed),它把耗时的函数结果保存起来,避免传入相同参数时重复计算。并将最少使用的条目自动删除。它可以用来消除树形递归时重复计算的问题。

@singledispatch,它用于将装饰的函数变成一个根据第一个参数的类型进行派发的分派器函数,有点OO中的RTTI的味道。可以用于实现函数的重载。

多个装饰器可以按顺序应用到一个函数上,作用是从下向上依次应用这些叠加的装饰器。

from functools import singledispatch

@singledispatch
def my_print(x):
    pass

@my_print.register(str)
def _(s):
    print('string dump', s)

@my_print.register(int)
def _(s):
    print('int dump', s)

@my_print.register(float)
def _(s):
    print('float dump', s)

my_print('hello')
my_print(1)
my_print(0.2)
print('----------------')

def f1(func):
    print('f1 called')
    return func

def f2(func):
    print('f2 called')
    return func

@f1
@f2
def test():
    print('test called')

test()
print('----------------')

def add_args(func):
    def inner(*args, **kwargs):
        print('call test2 wraper', args, kwargs)
        func(*args, **kwargs)
    return inner

@add_args
def test2(*args,**kwargs):
    print('test2 called')

test2()

print('----------------')
def argumented_decorate(prefix):
    def wrapper(func):
        def inner(*args, **kwargs):
            print(prefix, args, kwargs)
            func(*args, **kwargs)
        return inner
    return wrapper

@argumented_decorate(prefix='output:')
def test3(*args, **kwargs):
    print('test3 called')

test3(1,2, a=1, b=2)
string dump hello
int dump 1
float dump 0.2
----------------
f2 called
f1 called
test called
----------------
call test2 wraper () {}
test2 called
----------------
output: (1, 2) {'a': 1, 'b': 2}
test3 called

8 Python的面向对象

8.1 对象标识,相等性和别名

python中变量都是引用,指向内存中变量的地址。
id()方法可以取得一个变量的内在地址。==判断两对象的值是否相等,而不关心二者是否指向同一个对象。is 关键字判断两个变量是滞指向一个对象。

a=1
b=1

print(id(a), id(b), a is b, a==b)

a=[1,2]
b=[1,2]

print(id(a), id(b), a is b, a==b)

a=(1, 2, [3, 4])
b=(1, 2, [3, 4])
print(id(a), id(b), a is b, a==b)

a[-1].append(5)
print(id(a), id(b), a is b, a==b)
10935488 10935488 True True
140610190950984 140610190790088 False True
140610190713264 140610198628824 False True
140610190713264 140610198628824 False False

8.2 浅复制与深复制

对于对象复制,默认是浅复制,即对于内部非值的对象,只复制引用。如果希望二者完全隔离,则需要使用标准库copy的deepcopy进行深复制。

import copy

lst1 = [1, 2, [3,4]]
lst2 = list(lst1)

print(id(lst1), id(lst2), id(lst1[-1]), id(lst2[-1]))
print(lst1==lst2, lst1 is lst2)
print(lst1[-1] is lst2[-1])


lst2 = copy.deepcopy(lst1)

print(id(lst1), id(lst2), id(lst1[-1]), id(lst2[-1]))
print(lst1==lst2, lst1 is lst2)
print(lst1[-1] is lst2[-1])

140632762564808 140632762608520 140632762568392 140632762568392
True False
True
140632762564808 140632762511944 140632762568392 140632762565512
True False
False

8.3 参数与引用

python中函数参数传递的是对象的引用,也就说函数内部的形参是实参的别名而已。因此函数可能会修改参数传入的可变对象,对于值对象,对形参的修改并不会影响外面的实参。

对于修改可变参数的内容,是否会在函数外部造成非 预期的副作用,需要调用方和被调用方之间进行协商以达成一致,否则这点与修改了全局变量造成的后果类似。

另外不要使用可变的数据类型作为参数的默认值,否则会造成难以发现的Bug。

a = 1
b =[1,2,3]
s = 'hello world'


def fun(*args):
    a, b, s = args
    print('enter call: ', id(a), id(b), id(s), a, b, s)
    a = 2
    b.append(4)
    s += '!'
    print('leave call: ', id(a), id(b), id(s), a, b, s)


print('befor call: ', id(a), id(b), id(s), a, b, s)

fun(a,b,s)

print('after call: ',id(a), id(b), id(s), a, b, s)

#Bug caused by default arg with 
def test(input=[]):
    input.append(1)
    print(id(input), input)

test()
test()
befor call:  10935488 140632762607496 140632762607600 1 [1, 2, 3] hello world
enter call:  10935488 140632762607496 140632762607600 1 [1, 2, 3] hello world
leave call:  10935520 140632762607496 140632762772656 2 [1, 2, 3, 4] hello world!
after call:  10935488 140632762607496 140632762607600 1 [1, 2, 3, 4] hello world
140632762607112 [1]
140632762607112 [1, 1]

8.4 垃圾回收与引用计数

del删除的是变量名,而不是变量占用的内存。虚拟机使用引用计数来进行内在的垃圾回收。当对象引用计数为0时,对象被释放。

标准库weakref是弱引用,弱引用并不占用引用个数。它可以用来管理对象的生命周期。

弱引用在缓存应用中很有用,因为我们不想被缓存的引用而始终保存对象。此时可以使用weakref.ref来获取所指对象,使用弱引用访问对象时若对象不存在了,则会返回None。

ref相对比较底层,通常使用WeakSet/WeakKey/ValueDictionary/finalize等高层模块。

WeakValueDictionary类实现的是一种可变映射,其存储对象的弱引用于value中,若引用的对象被回收,则对应的键会自动从字典中删除。其经常用于缓存。
WeakKeyDictionary类类似,其key用于存储弱引用,它可以为相关对象存储额外的属性。
WeakSet中则直接保存对象的弱引用,若对象被回收,则会从集合中自动删除。
上面这三个类不能处理list和dict类,但可以通过继承之来绕过这个问题。

import weakref


a = {1, 2, 3}
b = a

def finalize():
    print('release all')

refer = weakref.finalize(a, finalize)

wk_refer = weakref.ref(a)
print(wk_refer())

del a
print(refer.alive)
del b
print(refer.alive)

print(wk_refer())


class Person:
    def __init__(self):
        pass
    pass

wk_set = weakref.WeakSet()
o1 = Person()
o2 = Person()
wk_set.add(o1)
wk_set.add(o2)

del o1
del o2

print(len(wk_set))
{1, 2, 3}
True
release all
False
None
0

9 Python风格的对象

本章通过实例介绍如何实现符合python标准的类,实现类不需要通过继承来实现,只需要实现一些magic方法即可。

__slots__属性,可以优化对象的内存占用。实例属性将不会存储在耗费内存的__dict__中。

from array import array
import math

class Vector:
    __slots__ = ('__x', '__y')

    typecode = 'd'

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

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

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

    def __iter__(self):
        return (i for i in (self.x, self.y))

    def __str__(self):
        return "({},{})".format(self.x, self.y)

    def __repr__(self):
        return "{}({},{})".format(type(self).__name__, self.x, self.y)

    def __bytes__(self):
        return bytes([ord(self.typecode)]) + bytes(array(self.typecode, self))

    def __eq__(self, v):
        return self.x == v.x and self.y == v.y

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

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

    def __format__(self, fmt_spec=''):
        comp = (format(c, fmt_spec) for c in self)
        return '({}, {})'.format(*comp)


@classmethod
def frombytes(cls, octets):
    typecode = chr(octets[0])
    memv = memoryview(octers[1:]).cast(typecode)
    return cls(*memv)

vect = Vector(3, 4)
print(str(vect))
print(repr(vect))
v2 = eval(repr(vect))
print(vect == v2)

x, y = v2
print(x,y)

print(bytes(v2))
(3,4)
Vector(3,4)
True
3 4
b'd\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@'

10 序列的修改/散列与切片

本意通过实现一个多维向量类,来说明一些方法的使用。

import math
import reprlib
import functools
import operator
from array import array


class VectorN:
    typecode = 'd'

    def __init__(self, iterable):
        self.data = array(self.typecode, iterable)

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

    def __getitem__(self, idx):
        cls = type(self)
        if isinstance(idx, slice):
            return cls(self.data[idx])
        elif isinstalce(idx, numbers.Integral):
            return self.data[idx]
        else:
            raise typeError('idx type error')

    def __iter__(self):
        return iter(self.data)

    def __repr__(self):
        comp = reprlib.repr(self.data)
        comp = comp[comp.find('[') : -1]

        return 'VectorN({})'.format(comp)

    def __str__(self):
        return str(tuple(self.data))

    shortcut_names = 'xyzt'

    def __getattr__(self, name):        
        cls = type(self)

        pos = self.shortcut_names.find(name)

        if pos != -1:
            return self.data[pos]

        raise AttributeError('attr not exist')

    def __hash__(self):
        data = map(hash, self.data)
        return functools.reduce(operator.xor, data)

    def __eq__(self, other):
        for a, b in zip(self, other):
            if a != b:
                return False

        return True

vect = VectorN(range(50, 100))
print(repr(vect))

vect[2:10]
print(hash(vect))
print(vect.x)
VectorN([50.0, 51.0, 52.0, 53.0, 54.0, ...])
1
50.0

11 接口

python初始并没有引入抽象基类,2.6版本才引入。抽象基类的常见用途是实现接口时作为超类使用。

对象的类型无关紧要,只要实现了特定的接口即可。抽象基类的本质是几个特殊方法。

11.1 猴子补丁

我们在第一章的代码是不支持洗牌的,因为我们没有实现__setitem__()方法。为了在不修改源码的情况下使其支持洗牌。

为了补充这个功能,可以实现一个洗牌函数,并将其赋值给Deck类的__setitem__方法。

import random

class Deck:
    def __len__(self):
        return 0

    def __getitem__(self, pos):
        return 0    

deck = Deck()
random.shuffle(deck)

def set_card(deck, pos, card):
    deck._cards[pos] = card


Deck.__setitem__ = set_card
random.shuffle(deck)


class Books:
    def __len__(self):
        return 0

from collections import abc

isinstance(Books(), abc.Sized)
True

11.2 标准库中的抽象基类

11.2.1 集合基类

Iterable , Container 和 Sized

各个集合应该继承这三个抽象基类,__iter__支持迭代, __contains__支持in操作,__len__支持len()函数。

Sequence, Mapping 和 Set
这三个是主要不可变集合类型,各自都有可变的子类。如MutableSet。

MappingView
在Python3中,map的方法如item, keys, values返回的对象分别是ItemView, KeysView, ValuesView

11.2.2 数字基类

numbers包定义了数字的层次。自顶向下分别为Number -> Complex -> Real -> Rational -> Integral

11.3 定义并使用抽象基类

自定义的抽象基类一般只在框架中才有使用场合。

import abc
import random

class Tombola(metaclass=abc.ABCMeta):
    # __metaclass__ = abc.ABCMeta  # for python2
    @abc.abstractmethod
    def load(self, iterable):
        pass

    @abc.abstractmethod
    def pick(self):
        pass

    def loaded(self):        
        return bool(self.inspect())

    def inspect(self):
        items = []
        while True:
            try:
                items.append(self.pick())
            except:
                break
        self.load(items)

        return tuple(sorted(items))


class BingoCage(Tombola):
    def __init__(self, items):
        self._items = items
        self.load(items)

    def pick(self):
        try:
            return self._items.pop()
        except:
            pass

    def load(self, iterable):
        self._items.extend(iterable)
        random.shuffle(self._items)

    def __call__(self):
        return self.pick()


class LotteryBlower(Tombola):
    def __init__(self, iterable):
        self._balls = list(iterable)        

    def load(self, iterable):
        self._balls.extend(iterable)

    def pick(self):
        return 


@Tombola.register    
class TomboList(list):
    def pick(self):
        pass

    def load(self):
        pass 

    def load(self):
        pass

    def inspect(self):
        pass



b = BingoCage([1,2,3])
l = LotteryBlower([1,2,3])
t = TomboList([1,2,3])

issubclass(TomboList, Tombola)
True

12 继承与多重继承

12.1 内置类型的派生

在python2.2之前,如list和dict这类内置类型是不能子类化的。之后的版本才可以进行子类化。但有个重要的事项,内置类型的一些方法不会调用用户定义的特殊覆盖方法,例如覆盖setitem或getitem在initupdate中会被忽略。

因此,直接子类化内置类型,容易出错。用户自己定义的类应该继承自collections模块中的类,如UserDcit,UserList和UserString。

class MyDict(dict):
    def __getitem__(self, key):
        print('user defined get')
        return super().__getitem__(key)

    def __setitem__(self,key,value):
        print('user defined set')
        super().__setitem__(key, value*2)


d = MyDict()
d['a']=1

print('call get', d.get('a'))
print('call []', d['a'])

user defined set
call get 2
user defined get
call [] 2

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

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

为此Python定义了方法解析顺序(MRO)规则,在每个类都有一个__mro__属性,它的值是一个元组,按照方法解析顺序列出各个父类。其顺序不仅和依存图有关,也和类继承时声明的顺序有关。

多重继承本身具有较大的复杂性,因此通常要避免使用多重继承。如果必须要多重继承,也要以继承接口的方式进行。

由于在Python中并没有interface关键字,因此不有办法实现多重接口继承。这样就有了Mixin的方式来模拟接口的继承。将一系列单一责任的行为放入Mixin类作为接口,这个Mixin,表示混入(mix-in),它告诉别人,这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。

class A:
    def say(self):
        print('say from A')

class B(A):
    def lie(self):
        print('lie from B')

class C(A):
    def lie(self):
        print('lie from C')

class D(B, C):
    def say(self):
        print('say from D')
        super().say()

    def said(self):
        self.say()
        super().say()

        self.lie()
        super().lie()
        C.lie(self)

d = D()

d.said()

print(D.__mro__)

class FlyableMixin:
    def fly(self):
        print('I flys')
    pass

class Person:
    def think(self):
        print('I think')


class Superman(Person, FlyableMixin):
    pass

man = Superman()
man.fly()
say from D
say from A
say from A
lie from B
lie from B
lie from C
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
I flys

13 重载运算符

本章以例子介绍若干个运算符的重载方法。python中各种运算符,都可以映射到对应类型上特殊方法。比如+对应__add__, +=对应__iadd__,详细参见下表。

运算符函数名说明
-__neg__
+__pos__
-__sub__
~__invert__
+=__iadd__
*__mul__
/__truediv__
//__floordiv__整除
%__mod__
**__pow__
@__matmul__矩阵乘法
&__and__
|__or__
^__xor__
<<__lshift__
>
__rshift__
==__eq__
!=__ne__
>__gt__
<__lt__

主要有加法,乘法,原位加法的实现。

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

    def __add__(self, other):
        if isinstance(other, Vector):
            raise TypeError()

        return Vector(self.x+other.x, self.y+other.y)

    def __mul__(self, other):
        pass

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

    def __iter__(self):
        return (item for item in (self.x, self.y))

    def __str__(self):
        return str(tuple([*self]))


v1 = Vector(3,4)
v2 = Vector(2,2)
print(v1+v2)

v1+=v2
print(v1)
(5, 6)
(5, 6)

14 可迭代对象与生成器

Python中没有宏,为了抽象出迭代器模式,Python2.2中添加了yield关键字,其可用于构建生成器(generator)。多数时候都把迭代器和生成器作同一概念。

在python3中,range也返回生成器,如果想要显式的列表,则需要使用list(range())这种方式。

14.1 可迭代对象与迭代器

一种类型只要实现了__iter__方法,便可以支持迭代。如果没有实现,但实现了__getitem__则会创建一个迭代器,并尝试按顺序从开始获取元素。

python3.4之后,可以使用iter()来判断一个对象是否可迭代。

标准的迭代器有两个方法:__next__与__iter__。下面的代码示例了实现迭代器Iterator的方法。

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

14.2 生成器

实现迭代功能更适合的方法是使用生成器。

创建生成器的方法是使用关键字:yield。通常需要定义一个函数,从这个函数返回yield创建的生成器。

生成器表达式借助于列表推导机制,返回一个新的生成器,它只是生成器函数的语法糖。

import re
import reprlib
from collections import abc

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

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

    # for iterator pattern 
    # def __iter__(self):
    #      return SentenceIterator(self.words)

    # for generator pattern
    def __iter__(self):
        #for match in  re.finditer('\w+', self.text):
            #yield match.group()

        return ( match.group() for match in re.finditer('\w+', self.text) )

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

    def __next__(self):
        if self.index == len(self.words):
            raise StopIteration()
        word = self.words[self.index]
        self.index += 1

        return word

    def __iter__(self):
        return self

s = Sentence('this is the best book in the field')

s
print(isinstance(s, abc.Iterable))
print(issubclass(Sentence, abc.Iterable))

it = iter(s)
print(next(it))

def gen123():
    yield 1
    yield 2
    yield 3

g = gen123()
for x in g:
    print(x)
True
True
this
1
2
3

14.3 标准库中生成器

itertools.count(start, step)提供了生成无穷数列的生成器。

itertools.compress(iter, selector) 返回selector序列对应为真的iter时的元素组成的生成器

ertool.takewhile(pred, iter) 处理iter序列,保留pred为真的值,一旦遇到假即不向后处理
itertools.dropwhile(pred, it) 处理iter序列,跳过pred为真的值,返回其后的元素

filter(pred, it)

itertools.filterfalse(pred, it)

map(func, iter, [iter2, iter…])

enumerater(iterable, start=0)

import itertools

count = itertools.count(0, 1)
print(next(count))


count = range(1, 10)

sel = itertools.compress(count, [1, 0, 0, 1])
print(list(sel))

def even(x):
    return x % 2 == 1

print(list(filter(even, count)))

sel = itertools.dropwhile(lambda x : x<5, count)
print(list(sel))

sel = itertools.takewhile(lambda x : x<5, count)
print(list(sel))


0
[1, 4]
[1, 3, 5, 7, 9]
[5, 6, 7, 8, 9]
[1, 2, 3, 4]

15 上下文管理器

15.1 else语句

else不仅能用在if中,也能用在for, while 和try语句中。

for…else… 仅当for运行完毕且没有被break中止,才运行else块

while…else… 仅当while运行完毕且没有被break中止,才运行else块

try…else…仅当try块没有异常时才运行else块,且else块中的异常不会被前面的except捕获。

def main():
    for x in range(1,3):
        print(x)
    else:
        print('for finish')


try:   
    main()
except:
    print('exception occur')    
else:
    print('cleaning ...')
1
2
for finish
cleaning ...

15.2 with与上下文管理器

使用with来管理上下文管理器,可以使得资源可以被无感知的正常释放。否则需要手动小心释放资源。

要实现下文管理器,只需要实现__enter__和__exit__()方法。

15.3 contextlib

contexlib提供with上下文的一些工具。

namecomment
closing若对象提供了close()方法,但没实现__enter__, __exit__,则可使用close构建上下文
suppress构建可以忽略指定异常的上下文
@contextmanager此装饰器将简单的生成器变成上下文管理器
prefix = 'global'

class FakeContext:
    def __enter__(self):
        global prefix
        self.prefix = prefix
        prefix = 'In Context'
        return prefix

    def __exit__(self, exc_type, exc_value, track_back):
        global prefix
        prefix = self.prefix        
        return


print(prefix)
with FakeContext() as fake:
    print(prefix)

print(prefix)


import contextlib

@contextlib.contextmanager
def fake_context():    
    global prefix;
    temp = prefix
    prefix = 'In Context'    
    yield prefix
    prefix = temp


with fake_context() as f:
    print(prefix)

print(prefix)
global
In Context
global
In Context
global

16 协程

yield不仅创建了生成器,它也是一种流程控制台工具,通过使用它,可以实现协程。yield的左边如果有等号,则运行时,可以从外部通过send向生成器发送数据,数据将被赋值到yield等号的左值。yield 右边是生成器向外部传递的值,若没有值,则传None。

16.1 将生成器转化为协程

协程指一个协调的过程,此过程与调用方协作,产出由调用方提供的值。

协程有以下四种状态:

statedescription
GEN_CREATED等待开始执行
GEN_RUNNING解释器正在执行
GEN_SUSPENDED在yield表达式处暂停
GEN_CLOSED执行结束

当协程创建之后处于created状态,通过调用next()才能让其执行,并在yield处挂起;第一次调用next称之谓prime(预激);执行next时,yield之后的语句被执行,并将其计算器结果返回出去。
当处于suspend状态时才能调用send向其传递参数,传递的参数将赋值给挂起位置的yield语句左边赋值变量。

如果协程中有异常,则会中止协程,因此需要处理掉可能产生的异常。当然如果确实希望中止协程,则可以使用生成器对象上的throw和close方法显式将异常发送给协程。

from inspect import getgeneratorstate

class UserException(Exception):
    pass

def simple_coroutine(a):
    try:
        while True:
            try:
                x = yield
            except UserException:
                print('user except handled')
    finally:
        print('cleanup')

co = simple_coroutine(12)
next(co)

co.send(10)
co.send(11)

co.throw(UserException)
co.close()

print('--------------------------')


def simple_coroutine2(a):
    print('start: a=', a)
    try: 
        try:
            b = yield a
            print('receved b=', b)
            c = yield a + b
        except :
            print('*** StopException handled, terminated')
    finally:
        print('clearnup exit')

co2 = simple_coroutine2(14)
print(getgeneratorstate(co2))
print('------')

print('call next', next(co2))
print(getgeneratorstate(co2))
print('------')


print('call send', co2.send(28))
print(getgeneratorstate(co2))
print('------')

co2.close()
print(getgeneratorstate(co2))
user except handled
cleanup
--------------------------
GEN_CREATED
------
start: a= 14
call next 14
GEN_SUSPENDED
------
receved b= 28
call send 42
GEN_SUSPENDED
------
*** StopException handled, terminated
clearnup exit
GEN_CLOSED

16.2 预激协程装饰器

协程都必须预激才能使用,为了方便使用,定义一个装饰器,可以使得这个过程更容易编程。

下面的代码以计算移动平均的示例,分别演示使用next激活。以及使用装饰器的方法。

注意my_avg及my_avg2的代码是相同的,只是后者的定义体前使用了装饰器。

# 使用next激活的方式
def my_avg():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield avg
        total += term
        count += 1
        average = total/count

avg=my_avg()

next(avg)

print(avg.send(1))
print(avg.send(2))
print(avg.send(3))


from functools import wraps

#coroutine为激活装饰器
def coroutine(func):
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer

@coroutine
def my_avg2():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

avg = my_avg2()

print(avg.send(1))
print(avg.send(2))
print(avg.send(3))
1.0
1.5
2.0

16.3 让协程返回值

在python3.3之前,如果从生成器函数调用return,则会触发错误。在新版本中可以返回值,代码需要判断输入是否到达终止,并从中退出。

然而要拿到返回值,必须在外层使用try..except捕获StopIteration,以便从中拿到返回值。具体参见下面的代码。

def my_avg():
    total, count = 0.0, 0
    avg = None
    while True:
        num = yield
        if num == None:
            break
        total += num
        count += 1
        avg = total/count
    return (count, avg)

avg = my_avg()
next(avg)

avg.send(1)
avg.send(2)
avg.send(3)

try:
    avg.send(None)
except StopIteration as e:
    print('avg returned:', e)

16.4 使用yield from

yield from最简单的应用是替换使用了for的生成器。

复杂的用法是它可以联通上层生成器和子生成器,构成数据传输的通道。如下例中的yield from avg(),可以将客户端的数据通过上层生成器传递到avg()中进行计算,并在子生成器终止时将计算结果返回给上层生成器,并在客户端中驱动取出这些数据。可以把子生成器看作消费者,而将客户端看作生产者。

# simplify for in generator
def example():
    # for x in 'hello word':
    #    yield x
    yield from 'hello world'

print('yield from o simplify for:',list(example()))


# play as pipeline

data = {
    'grade1': [3,4,5,3,2,1],
    'grade2': [4,4,4,2,2,1,3],
    'grade3': [1,2,3,4,2,2],
}

def my_avg():
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        if term is None:
            break
        total += term
        count += 1
        average = total/count

def client(dt):
    result = {}
    for key, values in dt.items():
        group = grouper(result, key)
        next(group)

        for value in values:
            group.send(value)
        group.send(None)

    print('yield from as a pipeline:',result)


def grouper(result, key):
    while True:
        result[key] = yield from my_avg() # yield from sub geneator
        print('In grouper, yield return:', key)
    pass


client(data)
yield from o simplify for: ['h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']
In grouper, yield return: grade1
In grouper, yield return: grade2
In grouper, yield return: grade3
yield from as a pipeline: {'grade1': None, 'grade2': None, 'grade3': None}

17 物理并发

17.1 并发

在python中实现并发有两种方式,一种是使用线程池,另一种是使用协程。
python3的标准库中的current模块,在concurrent.futures模块中提供了ThreadPoolExecutor封装了线程池操作。而在futures.Future和asyncio.Future提供两个Future类。两个类的作用相同。

ThreadPoolExecutor是很高层的实现,其提供了两种方式来实现并发。一种是逐个提交,一种是批量映射。分别对应submit接口和map接口。submit返回单个future,而map则返回future的生成器,以遍历等待 任务。

下面以并行下载若干网页为例来说明。其中第1个版本的run_many采用顺序下载,第2个版本的使用线程池,第3个版本使用协程,第4个版本使用多进程。

从结果来看,顺序执行时,消耗的时间要多一些,而两种并行在时间上差不太多。

Cpython实现使用PIL,因此对于CPU密集型的实现来说,并发是没有价值的。但对于IO密集型的程序,在执行耗时IO时都会释放PIL,因此这种情况下并发会产生较好的效果。

python3移除了旧的thread模块,代之为新的threading模块。如果ThreadPoolExecutor类对任务不够灵活,threading模块中的组件。

import time
import urllib.request
from concurrent import futures

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://www.google.com/']


def load_url(url, timeout=2):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# run in sequence
def run_many1(lst):
    for x in lst:
        load_url(x)
    return 0

# run in threadpool
def run_many2(lst):
    work_num = len(lst)
    with futures.ThreadPoolExecutor(work_num) as executor:
        res = executor.map(load_url, lst)

    return list(res)

# run in coroutine
def run_many3(lst):
    workers = len(lst)
    with futures.ThreadPoolExecutor(max_workers=workers) as exe:
        to_do = []
        for item in lst:
            future = exe.submit(load_url,item)
            to_do.append(future)

        ret = []
        for future in futures.as_completed(to_do):
            result = future.result()
            ret.append(result)
    return ret

def run_many4(lst):
    with futures.ProcessPoolExecutor() as exe:
        ret = exe.map(load_url, lst)
    return ret

for i in (1,2,3,4):
    func = 'run_many{}(URLS)'.format(i)
    t0 = time.time()
    ret = eval(func)
    diff = time.time() - t0

    print(i, diff)


1 5.84559178352356
2 2.779308557510376
3 2.8296608924865723
4 2.8546900749206543

18 使用asyncio包

asyncio提供了一个事件循环,我们只需要将定义的函数,交给事件循环去执行即可。

18.1 线程与协程对比

下面的代码示例了使用threading模块实现并发,和协程并发的两个方式。

import threading
import itertools
import time
import sys

class Signal:
    flag = True

def spin(msg, signal):
    write, flush = sys.stdout.write, sys.stdout.flush

    for char in itertools.cycle('|/-\\'):
        status = char + ' ' + msg
        write(status)
        flush()

        write('\x08'*len(status))
        time.sleep(0.1)
        if not signal.flag:
            break
    write(' '*len(status) + '\x08'*len(status))


def delay():
    time.sleep(5)
    return 42


def supervisor():
    signal = Signal()
    spinner = threading.Thread(target=spin, args=('thinking', signal))
    spinner.start()
    result = delay()
    signal.flag = False
    spinner.join()
    return result

#print(supervisor())

import asyncio

@asyncio.coroutine
def spin2(msg):
    write, flush = sys.stdout.write, sys.stdout.flush

    for char in itertools.cycle('|/-\\'):
        status = char + ' ' + msg
        write(status)
        flush()

        write('\x08'*len(status))

        try:
            yield from  asyncio.sleep(0.1)
        except asyncio.CancelledError:
            break

    write(' '*len(status) + '\x08'*len(status))

@asyncio.coroutine
def delay2():
    yield from asyncio.sleep(5)
    return 43

@asyncio.coroutine    
def supervisor2():
    spiner = asyncio.async(spin2('thinking!'))

    result = yield from delay2()
    spiner.cancel()



def main():
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(supervisor2())
    loop.close()

main()
| thinking!/ thinking!

/usr/local/lib/python3.6/dist-packages/ipykernel_launcher.py:67: DeprecationWarning: asyncio.async() function is deprecated, use ensure_future()


- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!- thinking!\ thinking!| thinking!/ thinking!           

19 动态属性与特性

19.1 动态获取属性

python中类的数据成员和方法称为属性(attribute),而特性(property)用来描述数据成员的读取和设置接口。

python中类的特殊方法__new__(cls)用法是实际的构造函数,它用于创建对象,并返回对象实例。若返回的对象是本类的实例化,则实例随后传递给__init__进行初始化,否则不会传递给__init__方法。本方法不需要 使用classmthod装饰器。

下面的例子对dict进行了扩展,原始的dict只能按字符串key的方式从中获取key对应的value,无法以属性的方式获取值。通过在包装类上调用__getattr__方法,可以实现对dict的健的按属性名访问。为了防止dict中的键与python的关键字冲突,可以借助keyword模块来判断是否是关键字。

import reprlib
import requests
import keyword
from collections import abc


s = requests.get('https://www.oreilly.com/pub/sc/osconfeed')

# 包装json反序列化返回的dict对象
class HappyJson:
    def __init__(self, mapping):
        self._data = {}

        for key, value in mapping.items():    
            if keyword.iskeyword(key):
                key += '_'
            self._data[key] = value

    def __getattr__(self, key):
        if hasattr(self._data, key):
            return getattr(self._data, key)
        else:
            return HappyJson(self._data[key])

    def __new__(cls, obj):
        if isinstance(obj, abc.Mapping):
            return super().__new__(cls)
        elif isinstance(obj, abc.MutableSequence):
            return [cls(item) for item in obj]
        else:
            return obj

    def __repr__(self):
        return repr(self._data)

obj = s.json()
print(obj['Schedule'].keys())

json = HappyJson(obj)
print(type(json))

speaker = json.Schedule.speakers[2]
print(speaker)
dict_keys(['conferences', 'events', 'speakers', 'venues'])
<class '__main__.HappyJson'>
{'serial': 172532, 'name': 'josh adams', 'photo': 'https://cdn.oreillystatic.com/en/assets/1/eventprovider/1/_%40user_172532.jpg', 'url': 'http://isotope11.com/', 'position': 'CTO', 'affiliation': 'Isotope11', 'twitter': 'knewter', 'bio': '<p>I&#8217;m the <span class="caps">CTO</span> of Isotope11, a mildly successful software development company that focuses on Ruby, JavaScript, and Erlang/Elixir. I&#8217;m also responsible for <a href="http://www.elixirsips.com">http://www.elixirsips.com</a>, a screencast series wherein I walk through elixir as I learn it, and record 2 shortish (5-12 minutes, occasionally longer) videos per week. I&#8217;ve also been a co-author and a technical reviewer for multiple books on the Arduino microprocessor and Ruby, and have had a lot of fun doing robotics with Ruby as well. I&#8217;m currently aiming to bring that robotics fun to Erlang (or Elixir)</p>'}

19.2 深入特性

property虽然是作为装饰器来使用,但其本身是一个类。
其构造方法为
property(fget=None,fset=None,fdel=None,doc=None) p r o p e r t y ( f g e t = N o n e , f s e t = N o n e , f d e l = N o n e , d o c = N o n e )

property装饰的方法可以在__init__方法中调用,因此可以使用property对初始化函数中传递给attribute的值进行校验。

如下示例,对Person的name,age进行property两种不同方法的装饰 ,并对age的值进行的校验。当构造p2时传递了非法的年龄,代码抛出了我们定义的数据异常。

property是类特性,但其能管理实例对象的在运行时的对象属性。因此property不能在运行时通过对象直接覆盖,覆盖会抛出”Can’t set attribute”异常。其他类属性或对象属性都可能在运行时覆盖。

另一种覆盖方法是改写__dict__字典,然而即使改写成功,在调用property时,依旧返回原有的定义。只有在类上覆盖特性,才能生效。

总结就是当访问对象上的attr时,会首先从obj.__class__上去寻找,当没有找到时,才会 obj上去寻找。

class Person:
    country = 'USA'

    def sexy(self):
        return 'Unknown'

    @property
    def blood_type(self):
        return 'A'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def get_age(self):
        return self._age

    def set_age(self, age):
        if age < 0:
            raise Exception('value not allowed')
        self._age = age

    age = property(get_age, set_age, doc='age of person')

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, nam):
        self._name = nam

    def __str__(self):
        return 'Person({}, {})'.format(self._name, self._age)

p = Person('Jim', 32)
print(p)

# age setter for valid
try:
    p2 = Person('Trump', -2)
    print(p2)
except Exception as e:
    print('catch exception: ', e)

# override class attribute and inst attribute
p.country = 'Canda'
p.sexy = 'guy'

print(p.country, Person.country, p.sexy)

# can't override property through inst
# can override property througn cls
try:
    p.blood_type = 'AB'
    print(p.blood_type)
except Exception as e:
    print(e)

print(Person.blood_type)
Person.blood_type = 'FOO'
print(Person.blood_type)
Person(Jim, 32)
catch exception:  value not allowed
Canda USA guy
can't set attribute
<property object at 0x7f15c7b3fc78>
FOO

19.5 特性工厂函数

为每个property都定义getter和setter会导致一些重复的代码,可以使用特性工厂来解决这个问题。

这个特性工厂函数的问题在于创建属性时还需要传递一个字符串的名称,这需要程序员小心为保证其名字不会出现重复。另外如果使用字段名相同的字符串,则在对象的dict和类的dict中,此字段名称因为相同,会造成getattr触发特性解析而循环调用。

def make_property(name):
    def getter(instance):

        #这里不能使用getattr,不然会造成递归调用getattr而溢出
        # return getattr(instance, name)
        return instance.__dict__[name]

    def setter(instance, value):
        instance.__dict__[name] = value

    return property(getter, setter)


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age    

    name = make_property('name')
    age = make_property('age')

p = Person('Bush', 23)
print(p.name, p.age)

print('obj __dict__:', p.__dict__)
print('cls __dict__:',Person.__dict__)
Bush 23
obj __dict__: {'name': 'Bush', 'age': 23}
cls __dict__: {'__module__': '__main__', '__init__': <function Person.__init__ at 0x7f15c7a84268>, 'name': <property object at 0x7f15c8129188>, 'age': <property object at 0x7f15c8129318>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

19.6 处理属性的相关属性与函数

19.6.1 特殊属性

namecomment
__class__对象所属类的引用
__dict__dict,存储对象或类的可写属性
__slots__字符串组成的元组,定义类允许有的属性。若slots中没有__dict__,则类的实例中就不会有__dict__

19.6.2 内置函数

namecomment
dir列出对象的大多数属性
getattr(obj, name, default)获取对象中name字符串对应的属性
hasattr(obj,name)检测对象中存在指定的属性
setattr(obj,name,val)设置对象属性
vars([obj])返回obj的__dict__属性

19.6.3 特殊方法

namecomment
__delattr__(self, name)当调用del删除属性时会调用此方法
__dir__()dir方法会触发此方法的调用
__getattr__(self, name)仅当在obj, Class和超类中找不到属性时才会触发
__getattribute(self,name).操作符与getattr/hasattr会触发此方法,此方法失败后才会调用上面的__getattr__
__setattr__(self,name,val).操作符和setattr会触发此方法

20 属性描述符

20.1 特性管理类

为了管理property,在上一章中我们使用了函数来解决,来节我们用类来解决,类中可以保存传递进行的要管理的属性的名字,但我们的目的是消除传递的字符串参数,使得使用代码更加精简。因此可以通过使用自增ID的方式来跟踪每个创建的property。

在django框架的model的字段就是描述符。

class PropertyField:
    _counter = 0

    def __init__(self):
        cls = self.__class__
        self._id = '{}#{}'.format(cls.__name__ ,cls._counter)
        cls._counter += 1

    def __set__(self, instance, value):
        setattr(instance, self._id, value)
        #instance.__dict__[self._id] = value 

    # owner是托管类如Person的引用,当需要引用时可以传递进来
    def __get__(self, instance, owner):
        if instance is None: # 通过类访问特性时返回特性本身
            return self
        return getattr(instance, self._id)
        # return instance.__dict__[self._id]


class Person:
    name = PropertyField()
    age = PropertyField()

    def __init__(self, name, age):
        self.name = name
        self.age = age


p = Person('Bush', 23)
print(p.name, p.age)

print(Person.name)
print('obj __dict__:', p.__dict__)
print('cls __dict__:',Person.__dict__)


Bush 23
<__main__.PropertyField object at 0x7f15c8141a20>
obj __dict__: {'PropertyField#0': 'Bush', 'PropertyField#1': 23}
cls __dict__: {'__module__': '__main__', 'name': <__main__.PropertyField object at 0x7f15c8141a20>, 'age': <__main__.PropertyField object at 0x7f15c7aa1160>, '__init__': <function Person.__init__ at 0x7f15c7a7f620>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

20.2 覆盖型与非覆盖型描述符对比

所谓覆盖型是指定义了__set__方法的描述符,非覆盖型则只定义了__get__方法。有了__set__便可以修改实例对象的属性。

不管描述符是不是可覆盖,为类属性赋值都是能覆盖描述符的。这是一种猴子补丁技术。

20.3 方法都是描述符

类中定义的方法,因为都有get方法,其相当于描述符。

20.4 描述符最佳实践

  • 使用特性以保持简单
  • 只读描述符必须有__set__方法
  • 用于验证的描述符可以只有__set__方法
  • 仅有__get__方法的描述符可以实现高效缓存
  • 非特殊的方法可以被实例属性遮盖

21 类元编程

类元编程是在运行时创建或定制类的方法。在python中类也是第一类的,无需使用class关键字,在任何时候都可以使用新建类。

元类是类元编程的最高级工具,可以创建具有某种特质的全新类种。当然,除了开发框架,否则不需要编写元类。

21.1 类工厂函数

之前我们见过的namedtuple就是一个类工厂函数,把类名和属性名传递给它,它会创建一个tuple的子类。

下面我们实现一个简单类来实现类似namedtuple的功能。代码最后使用type来返回一个新的类。通常type作为函数来调用,然而type是一个类,作为类时通过传入三个参数可以新建一个类。最后一个参数用于指定新类的属性名和值。

cls=type(cls_name,(base,...),attr_dict) c l s = t y p e ( c l s _ n a m e , ( b a s e , . . . ) , a t t r _ d i c t )

另一种创建类的方法是构造一个类的字符串,并根据传递的参数格式化到字符串中,最后使用内置的exec()来执行类源码字符串。如果要接收不安全的字符串来创建类是十分不推荐的作法。

Dog = type('Dog', (object,), {'name':'doggy', 'age':2})
d = Dog()
print(d.name, d.age)


def make_cls(cls_name, field_names):
    try:
        field_names = field_names.replace(',', ' ').split(' ')
    except AttributeError:
        pass

    field_names = tuple(field_names)

    def __init__(self, *args, **kwargs):
        attrs = dict(zip(self.__slots__, args))
        attrs.update(kwargs)

        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):
        return 'Person({}, {}, {})'.format(*self)


    cls_attrs = {   
        '__slots__': field_names,
        '__init__': __init__,
        '__iter__':__iter__,
        '__repr__':__repr__
    }

    return type(cls_name, (object,), cls_attrs)

Person = make_cls('Person', ('name', 'age', 'sexy'))

p = Person('Bush', 16, 'Male')
print(p, type(p))
doggy 2
Person(Bush, 16, Male) <class '__main__.Person'>

21.2 定制描述符的类装饰器

类装饰器用于在类定义完成后对类进行处理,但它的问题是只能作用于直接装饰的类,若存在继承,则子类可能不会收到装饰的效果。


class PropertyField():

    def __init__(self):
        self._id = 0

    def __set__(self, instance, value):
        if value < 0:
            raise Exception('Value Error')
        instance.__dict__[self._id] = value 

    def __get__(self, instance, key):
        if instance is None:
            return self
        return instance.__dict__[self._id]


def cls_decorator(cls):
    for key, value in cls.__dict__.items():
        if isinstance(value, PropertyField):
            value._id = '{}#{}'.format(type(value).__name__, key)

    return cls


@cls_decorator
class Person:
    age = PropertyField()

    def __init__(self, name, sexy, age):
        self.name = name
        self.sexy = sexy
        self.age = age

    pass

p = Person('Bush', 'Male', 10)
print(p.name, p.age, p.sexy)

print(p.__dict__)
print(Person.__dict__)
Bush 10 Male
{'name': 'Bush', 'sexy': 'Male', 'PropertyField#age': 10}
{'__module__': '__main__', 'age': <__main__.PropertyField object at 0x7f15c7a44a58>, '__init__': <function Person.__init__ at 0x7f15c7c85840>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

21.3 导入时与运行时比较

Python程序元需要区分导入时与运行时,当然二者间也存在着灰色地带。

在导入时,解释器会从上到下一次性解释完.py文件的源码,生成用于执行的字节码。若本地pycahce目录中有最新的.pyc文件,则会跳过生成字节码的步骤。

由于语句几乎都是可执行的,因此导入时程序状态也会改变。尤其是import语句,首次导入时,会运行导入模块中的全部顶层代码,后续的重担导入则使用缓存。由于顶层代码可以做任何事,所以import语句可以触发任何运行时行为。

对于定义的函数和类,导入时解释器会编译它们的定义体,把它们绑定到对应的全局名称上。

21.4 元类基础

在前面笼统说了type是一个类,但其实type是python中所有类的元类,或所有类都是type的实例。

python中内置类及用户定义类,都继承自object类,这些类的metaclass都指向type,即object.__class__是type。

type类有些特殊,其继承自object类,其元类指向自己。同时object类是type的实例。

在标准库中还有其他的元类,如Enum和ABCMeta,但它们都继承自type。

21.5 定制描述符的元类

class PropertyField():

    def __init__(self):
        self._id = 0

    def __set__(self, instance, value):
        instance.__dict__[self._id] = value 

    def __get__(self, instance, key):
        return instance.__dict__[self._id]


class EntityMeta(type):
    def __init__(cls, name, bases, attr_dict):
        super().__init__(name, bases, attr_dict)
        for key, attr in attr_dict.items():
            if isinstance(attr, PropertyField):
                attr._id = '{}#{}'.format(type(attr).__name__, key)

class Entity(metaclass=EntityMeta):
    pass


class Person(Entity):
    name = PropertyField()
    age = PropertyField()

    def __init__(self, name, age):
        self.name = name
        self.age = age
        pass
    pass

p = Person('Bush', 32)

print(p.name, p.age)

print(p.__dict__)
print(Person.__dict__)
Bush 32
{'PropertyField#name': 'Bush', 'PropertyField#age': 32}
{'__module__': '__main__', 'name': <__main__.PropertyField object at 0x7f15c8141780>, 'age': <__main__.PropertyField object at 0x7f15c8141358>, '__init__': <function Person.__init__ at 0x7f15c7a7fe18>, '__doc__': None}

21.6 元类特殊方法__prepare__

有时我们定义类的属性时,希望能按定义的顺序来使用它们,例如一个解析csv文件的类。然而由于__dict__是字典,无法保存顺序。

为了解决顺序的问题,python3引入了特殊方法__prepare__,它只在元类中有用,且必须声明为类方法,,其形式为:

__prepare__(cls, name, bases) __prepare__(cls, name, bases)

第一个参数为元类,name为要构建的类的名称,bases为基类组成的元组。解释器会在调用__init__前调用__prepare__,并将其返回的结果传递给init。

在实际应用中,框架和库会使用元类来执行很多任务,如:

  • 验证属性
  • 一次把装饰器应用到多个方法上
  • 对象关系映射
  • 基于对象的持久存储
  • 序列化对象或转换数据
import collections 

class EntryMeta(type):
    @classmethod
    def __prepare__(cls, name, bases):
        return collections.OrderedDict()

    def __init__(cls, name, bases, attr_dict):
        super().__init__(name, bases, attr_dict)
        cls.field_names = []
        for key, attr in attr_dict.items():
            type_name = type(attr).__name__
            attr._id = '{}#{}'.format(type_name, key)
            cls._field_names.append(key)
        pass

class Entry(metaclass=EntryMeta):
    @classmethod
    def field_names(cls):
        for name in cls._field_names:
            yield name
    pass

21.7 类作为对象

python数据模型为每个类定义了很多属性,常见的属性见下表:

namecomment
cls.__mro__方法查找顺序
cls.__class__实例方法,实例实例化来源类型
cls.__name__类方法,本类型的名字
cls.__bases__由类的基类组成的元组
cls.__qualname__类或函数的限定名称
cls.__subclasses__类的直接子类
cls.mro可供覆盖的方法解析方法顺序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值