說明
本項目基于Luciano Ramalho的《Fluent Python》,屬于個人練習。
如需關聯源碼與關聯圖片見原文git地址、本文git地址
Section1 Python 的數據類型
雙下方法
len(self):#len()
getitem(self,mark):#items[i]
init(self):#item = Item()
repr(self):#print(item)
abs(self):#abs(item)
bool(self):#if item:
add(self ,other):#item + other
mul(self, scalar):#item * scalar
序列
列表推導
[i for i in range(10)]
生成器表達式
(i for i in range(10))
拆包
a,b = b,a
切片
l[::-2]
l[:5] = []
序列的增量賦值
m = l*5
m += [1,2]
一個奇怪的現象
#一個奇怪的現象
t = (1,2,[10,20])
print(id(t))
t[2].extend([30,40])
# t[2] += [50,60]
# t[2] = t[2] + [50,60]
#這樣會引發(fā)異常,元組不可變?
print(id(t))
print(t)
#元組可變?
sorted(lists)與list.sort()
區(qū)別sorted會新建列表作為返回值
而list.sort只是對當前l(fā)ist進行操作
已排序搜索和插入
#比較有趣的用法
def grade(score,breakpoints = [60,70,80,90],grades = 'FDCBA'):
i = bisect.bisect_right(breakpoints,score)
return grades[i]
print([grade(score) for score in [33,60,62,69,80,100]])
#已排序隊列的插入
bisect.insort_left([],10)
數組
from array import array
from random import random
floats = array('d',(random()*100 for i in range(10**7)))
print(floats[-1])
#注意哦,數組里面存的是數字的字節(jié)表達
fp = open('floats.bin','wb')
floats.tofile(fp)
fp.close()
內存視圖memoryview
泛化和去數學化的NumPy數組,實現在數據結構之間共享內存。用以處理大型數據集合
注意共享內存的意思是對memoryview的操作不會產生新的對象!?。∵@也是其高效的原因
NumPy 與 SciPy
from time import perf_counter as pc
t0 = pc()
floats = numpy.loadtxt('data.txt')
t1 = pc()
floats /= 3
t2 = pc()
numpy.save('floats-10M',floats)
t3 = pc()
floats2 = numpy.load('floats-10M.npy','r+')
t4 = pc()
floats2 *=363
t5 = pc()
{'t0': 0.851668039, 't1': 153.021685987, 't2': 153.038698158, 't3': 153.4188989, 't4': 153.420616467, 't5': 153.455011833}
讀取txt和二進制文件簡直天壤之別,當然這有NumPy優(yōu)化內存映射機制
隊列
from collections import deque
dq = deque(range(10),maxlen = 10)
dq.rotate(-4)
print(dq)
dq.pop()
print(dq)
dq.extend([11,12,13])
print(dq)
queue
import queue
#用以同步線程安全,滿員時會等待銷毀后再執(zhí)行
multiprocessing
import multiprocessing
#用以同步進程管理,同queue
asyncio
import asyncio
#是用來編寫 并發(fā) 代碼的庫,使用 async/await 語法
heapq
import heapq
#將可變序列當做堆隊列處理
基本數據類型對象
int,float,bool,complex,str(字符串),list,dict(字典),set,tuple
字典dict
字典推導
{country : code for code,country in [(1,'China'),(2,'EN')]}
setdefault
index.setdefault(word,[]).append(location)
word不存在賦值[],然后接著執(zhí)行
missing
dd = defaultdict(list)
mm = defaultdict(lambda : '<missing>')
這個方法只對getitem()有效
update()
將可迭代對象批量更新進去,作用同merge
OrderedDict
有序的鍵
ChainMap
組合多個字典
Counter
鍵帶計數器
UserDict
標準的dict用來自定義dict
MappingProxyType
映射只讀實例
d_proxy = MappingProxyType({})
集合
集合去重無序
支持很多中綴運算符 |合集 &交集 -差集
集合可以類比數學運算符
& | - ^ in <= < >= >
散列表
[圖片上傳失敗...(image-6115e3-1567675097223)]
而dict和set都是可散列的,簡單點說,他們的鍵都是不可重復的,原因可以看如下
def __hash__(self):
return hash(id(self))
def __eq__(self, other):
if isinstance(other, self.__class__):
return hash(id(self))==hash(id(other))
else:
return False
由此eq的定義我們也能知道為什么可哈希集合?==?
散列表的問題在于內存開銷巨大
另外在散列表中新增會導致重新分配內存。同時迭代與更新散列表是一件危險的事
字符
str對象中獲得的元素都是Unicode字符
何為字符:
以4~6個十六進制數表示碼位
字符的具體表述取決于其編碼,編碼是在碼位和字節(jié)序列轉換用的算法
編碼規(guī)范申明
#!/usr/bin/python
# -*- coding:utf-8 -*-
編碼解碼
Unicode字符 encode 編碼 字節(jié)序列
decode 解碼
字節(jié)序標記byte-order mark
編碼的前置標識,用來標識編碼方式,以及識別編碼
處理文本文件
- 盡早解碼成字符串
- 處理過程中盡量不進行編碼和解碼
- 編碼輸出文本
!open file時始終明確編碼,因為open文件時使用的是默認系統(tǒng)編碼
!推舉:chardet進行編碼測試
編碼默認值
在open文件時:系統(tǒng)默認編碼
在輸出輸入控制臺時:控制臺的編碼
而python3系統(tǒng)內部:使用utf-8
為了比較Unicode字符
from unicodedata import normalize
'''
NFC 最小碼位都成等價字符
NFD 拆解成基字符的組合字符
這兩個會把兼容字符分解成兼容分解字符,
會曲解原意但有利于將單字符解析成有意義的多字符
NFKC
NFKD
'''
def nfc_equal(str1,str2):
retrun normalize('NFC',str1) == normalize('NFC',str2)
大小寫折疊
str.casefold()
類似str.lower(),但是其規(guī)范了一些特殊符號的大小寫問題
通過上述兩個函數進行Unicode規(guī)范化和大小寫折疊都是符合Unicode標準的
去掉變音符號
import string
import unicodedata
def shave_marks(txt):
'''
去掉變音符號
'''
norm_txt = unicodedata.normalize('NFD',txt)
shaved = ''.join(c for c in norm_txt if not unicodedata.combining(c))
#combining返回規(guī)范組合類,無組合類則為0
return unicodedata.normalize('NFC',shaved)
轉換
str.maketrans轉換對照表,用于str.translate
Unicode排序算法實現
pyuca.Collator.sort_key
雙模式API
import re,os
為了處理str和bytes不同情況
Section2 將函數視為對象
一等函數
doc()
幫助文檔
將函數視為對象
def factorial(n):
"""return factorial n!"""
return 1 if n<2 else n * factorial(n-1)
print(factorial.__doc__)
fact = factorial
print(list(map(fact,range(11))))
高階函數
如上文的map,正在被生成器表達式替代
匿名函數
lambda
sorted(fruits,key = lambda word : word[::-1])
可調用對象
用戶定義的函數
def/lambda
內置函數
len/time.strftime
內置方法
dict.get
方法
類
new()
類的實例
call()
生成器函數
yield
可以通過callable(obj)確認對象是否可以調用
只要任何對象實現call()即可表現的像函數
函數內省
dir(class) #探知屬性
dict() #注解
annotations() #參數和返回值的注解
call() #實現()運算符
defaults #形式參數的默認值
code() #編譯成字節(jié)碼的函數元數據和函數定義體
參數
定位參數、關鍵字參數、僅限關鍵字參數
調用函數前進行參數驗證
from inspect import signature
signature 的 bind 方法
函數注解
def clip(text:str,max_len:'int > 0' = 10) -> str:
"""
最長max_len后截斷空格
"""
函數式編程
operator#標準運算符替代函數
'''
operator.mul(a,b)
#return a * b
'''
'''
g = operator.itemgetter(*index)
g(obj) === obj[index] if len(index) == 1 else (obj(a) for a in index)
#return obj[index] if len(index) == 1 else (obj(a) for a in index)
#多于一個返回元組
'''
'''
g = operator.attrgetter(*name)
g(obj) === obj[name] if len(name) == 1 else (obj.a for a in name)
#return obj[name] if len(name) == 1 else (obj.a for a in name)
#可以根據.深入嵌套對象,獲得指定元素
'''
'''
g = operator.methodcaller(name,*args,**kwargs)
g(b) === b.name(*args,**kwargs)
#返回一個可調用對象!
'''
| 運算 | Syntax | Function | |
|---|---|---|---|
| Addition | a + b | add(a, b) | |
| Concatenation | seq1 + seq2 | concat(seq1, seq2) | |
| Containment Test | obj in seq | contains(seq, obj) | |
| Division | a / b | truediv(a, b) | |
| Division | a // b | floordiv(a, b) | |
| Bitwise And | a & b | and_(a, b) | |
| Bitwise Exclusive Or | a ^ b | xor(a, b) | |
| Bitwise Inversion | ~ a | invert(a) | |
| Bitwise Or | a | b | or_(a, b) |
| Exponentiation | a ** b | pow(a, b) | |
| Identity | a 是 b | is_(a, b) | |
| Identity | a 不是 b | is_not(a, b) | |
| Indexed Assignment | obj[k] = v | setitem(obj, k, v) | |
| Indexed Deletion | del obj[k] | delitem(obj, k) | |
| Indexing | obj[k] | getitem(obj, k) | |
| Left Shift | a << b | lshift(a, b) | |
| Modulo | a % b | mod(a, b) | |
| Multiplication | a * b | mul(a, b) | |
| Matrix Multiplication | a @ b | matmul(a, b) | |
| Negation (Arithmetic) | - a | neg(a) | |
| Negation (Logical) | not a | not_(a) | |
| Positive | + a | pos(a) | |
| Right Shift | a >> b | rshift(a, b) | |
| Slice Assignment | seq[i:j] = values | setitem(seq, slice(i, j), values) | |
| Slice Deletion | del seq[i:j] | delitem(seq, slice(i, j)) | |
| Slicing | seq[i:j] | getitem(seq, slice(i, j)) | |
| String Formatting | s % obj | mod(s, obj) | |
| Subtraction | a - b | sub(a, b) | |
| Truth Test | obj | truth(obj) | |
| Ordering | a < b | lt(a, b) | |
| Ordering | a <= b | le(a, b) | |
| Equality | a == b | eq(a, b) | |
| Difference | a != b | ne(a, b) | |
| Ordering | a >= b | ge(a, b) | |
| Ordering | a > b | gt(a, b) |
functools
'''
functools.reduce
#將可迭代對象第一二個參數傳入,return和第三個作為下一個參數,依次往復
'''
'''
functools.partial
#用于部分應用一個函數,即創(chuàng)建一個新的可調用對象,但把函數部分參數固定。這個方法其實在于有些方法可能有些固定參數,比如傳入'NFC'格式化字符串,可以提前傳入以創(chuàng)建nfc函數
from operator import mul
from functools import partial
triple = partial(mul,3)
list(map(triple,range(1,10)))
'''
設計模式
迭代 策略模式
定義一系列算法,把他們一一封裝起來,并使得他們可以互相替換。本模式使得算法可以獨立于使用它的客戶而變化
[圖片上傳失敗...(image-45abe1-1567675097223)]
迭代 命令模式
抽象命令、繼承抽象命令 實例化命令接受者 實現具體命令、定義命令接受者 接受者具體實現方法、定義命令調用者
最后客戶端實例化命令接受者、命令調用者,通過命令接受者實例化命令,命令調用者再執(zhí)行具體命令。
[圖片上傳失敗...(image-9f4f4-1567675097223)]
這兩者都因為Python把函數作為一等對象,所以我們可以不給調用者\算法一個實際的對象,直接給函數就好。
對接口編程,而不是對實例編程
優(yōu)先實現對象組合,而不是類繼承
Section3 decorator&closure
what
裝飾器相當于用裝飾器調用被裝飾的函數
同時裝飾器在函數定義/導入以后立刻運行
用裝飾器優(yōu)化策略模式
promos = []
def promotion(promo_func):
promos.append(promo_func)
return promo_func
@promotion
def fidelity(order):
pass
#在導入時即可填充promos
變量作用域
當需要讓解釋器將函數體內變量作為全局變量
需要在函數體內標記global
閉包
一般情況下,如果一個函數結束,函數的內部所有東西都會釋放掉,還給內存,局部變量都會消失。但是如果滿足閉包的條件,外函數在結束的時候發(fā)現有自己的臨時變量將來會在內部函數中用到,就把這個臨時變量綁定給了內部函數,然后自己再結束。
這種臨時變量叫做自由變量,整個外函數則形成了閉包。
這里有一個問題要解決:不可變類型不會自覺變成自由變量,需要(因為不可變變量會自覺隱式創(chuàng)建局部變量)
nonlocal申明
在內部函數內部聲明nonlocal,說明不是局部變量
裝飾器:輸出函數運行時間
import time
def clock(func):
def clocked(*args):
t0 = time.perf_counter()
result = func(*args)
elapsed = time.perf_counter() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed,name,arg_str,result))
return result
return clocked
@clock
def snooze(seconds):
time.sleep(seconds)
@clock
def factorial(n):
return 1 if n<2 else n * factorial(n-1)
print('*'*40 , 'Calling snooze(.123)')
snooze(.123)
print('*'*40 , 'Calling factorial(6)')
print('6! = ',factorial(6))
動態(tài)的給一個對象增加一些額外的責任
迭代
import functools
def clock2(func):
@functools.wraps(func)
def clocked(*args,**kwargs):
t0 = time.time()
result = func(*args,**kwargs)
elapsed = time.time() - t0
name = func.__name__
arg_lst = []
if args:
arg_lst.append(','.join(repr(arg) for arg in args))
if kwargs:
pairs = ['%s=%r' % (k,w) for k,w in sorted(kwargs.items())]
arg_lst.append(', '.join(pairs))
arg_str = ', '.append(arg_lst)
print('[%0.8fs] %s(%s) -> %r' % (elapsed,name,arg_str,result))
return clocked
functools.wraps將func中name&doc屬性復刻到clocked中(保留原函數的屬性,裝飾器畢竟會將原函數替換為內函數),協(xié)助構建行為良好的裝飾器
標準庫的裝飾器
functools.lru_cache()
備忘功能,可以將耗時的函數結果保存下來,以防止傳入相同的參數重復計算
最常見的參考就是斐波那契了
@functools.lru_cache()
@clock
def fibonacci(n):
if n < 2 :
return n
return fibonacci(n-2) + fibonacci(n-1)
print(fibonacci(30))
參數maxsize指定緩存多少個結果,參數typed會區(qū)分不同參數類型,比如1和1.0
單分派泛函數functools.singledispatch
因為python不支持重載,所以帶來了它
根據第一個參數的類型選擇專門的函數
疊放裝飾器
參數化裝飾器
工廠函數。怎么讓裝飾器接受其他參數呢?那就讓裝飾器裝飾裝飾器
定義一個裝飾器工廠函數,返回裝飾器
參數化clock
更新并返回表示當前本地符號表的字典。 在函數代碼塊但不是類代碼塊中調用 locals() 時將返回自由變量。 請注意在模塊層級上,locals() 和 globals() 是同一個字典。
Section4 面向對象
對象在賦值之前就創(chuàng)建了
標識,相等性,別名
每個變量都有標識、類型、值,對象一旦建立,他的標識就不會變,is比較對象的標識,id()返回對象標識的整數標識
元組的相對不可變性
即元組的數據結構的物理內容不可變(即保存的引用不可變)
,但是引用的可變對象的值還是可變的
默認做淺復制
利用構造方法和切片創(chuàng)建的多數內置可變集合都是淺復制
即復制了外部容器,但是內部的引用還是不變的。
深復制和淺復制
import copy
同時通過copy()&deepcopy()定義copy和deepcopy的行為
python只支持共享傳參!
即函數內部的形式參數是實參的中各個引用的副本(淺復制哦)
避免將可變類型作為參數發(fā)的默認值
來看多圖警告:
[圖片上傳失敗...(image-679ea-1567675097224)]
[圖片上傳失敗...(image-831c5b-1567675097224)]
問題在于python中存在init.default寄存默認值,可變變量作為默認值在加載模塊式就已經確定了,直接用,危險危險!
比較好的辦法就是用利用構造方法和切片創(chuàng)建的多數內置可變集合進行淺復制代替等號這種直接賦值的方法
del和對象回收
對象絕對不會自行銷毀;然而無法獲得對象時,可能會被當做垃圾回收。
#演示一個回調函數
import weakref
s1 = {1,2,3}
s2 = s1
def bye():
print('GOODBYE!')
ender = weakref.finalize(s1,bye)
print(ender.alive)
del s1#不會刪除對象
print(ender.alive)
s2 = 'spam'#重新綁定最后一個引用導致{1,2,3}無法獲取,則對象被銷毀
print(ender.alive)
弱引用
其實示例中s1的引用也在ender中,但是為什么還是被回收了?——>弱引用不會增加引用數量
正是因為有引用,對象才會在內存中存在。在緩存中,弱引用保證被緩存引用的對象不因此而始終保存
weekref.ref獲取所指對象
weekref.WeakKeyDictionary實現可變映射
里面的值是對象的弱引用
但是不是所有的對象都可以作為弱引用的目標
一些更加邪道的特性就不多說了,涉及到編輯器的實現
Section5 duck type,python風格的對象
classmethod
定義類方法
staticmethod
定義構造方法
格式化顯示
format()
優(yōu)化處理:
def angle(self):
return math.atan2(self.y,self.x)
def __format__(self, format_spec = ''):
#優(yōu)化下:可以解析以p結尾的格式化語句,以轉化為極坐標系
if format_spec.endswith('p'):
format_spec = format_spec[:-1]
coords = (abs(self),self.angle())
outer_fmt = '<{},{}>'
else:
coords = self
outer_fmt = '({},{})'
components = (format(c,format_spec) for c in coords)
return outer_fmt.format(*components)
可散列
只讀特性
使用兩個前導下劃線把屬性標記為私有
def __init__(self, x=0, y=0):
self.__x = float(x)
self.__y = float(y)
@property#將讀值屬性標記為特性
def x(self):#讀值屬性與公開屬性同名
return self.__x#直接返回__x
@property
def y(self):
return self.__y
eq()
hash()
推薦使用異或混合各分量的散列值
要點其實就是可散列要求是鍵不可變、相同的鍵要hash相同、再實現hash,以上即三點所要求的的
私有屬性,受保護的屬性
python不支持直接定義私有屬性,通過將python的屬性定義為有兩個前導下劃線的屬性名,python會自覺的將此屬性歸類到實例的dict中,并且前面還會加上_type(self).__name__例如如下:Vector__x
以此防止子類覆蓋私有屬性:簡單點說就是沒有私有屬性這個概念,而是投機取巧,只能預防
所以其實是可以通過_Vector__x去修改私有屬性的值
所以其實在python中大家約定俗成的使用單個前導下劃線去自行約束自己的行為,雖然編譯器不會去輔助你,但你可以輔助自己
slots屬性
在類中定義slots后,實例將不會有除了slots中的其他東西
但是slots確實是一種優(yōu)化手法,有利于節(jié)省內存
覆蓋類屬性
類屬性實例可以直接調用修改,這樣就變成實例自己的屬性了
但是更好的方法是
繼承子類
簡單點說就是前面的問題,用子類去覆蓋父類的屬性
精彩直至
[圖片上傳失敗...(image-ca6fe4-1567675097224)]
Section6 序列的修改、散列和切片
之前就有涉獵,python中創(chuàng)建完善的序列不需要使用繼承,只需要符合序列協(xié)議的方法。這有點像建立在低級語言與編譯器交互的邏輯上,但是卻是使用的非正式的接口
序列主要的兩個方法就是len()與getitem()
所以,因為行為想序列,所以我們才說他是序列,這就是python中所謂的鴨子模型
可切片的序列
切片的實現原理
dir(slice)
help(indices)
S.indices(len) ——> (start,stop,stride)
這也是切片為嘛要知道len的原因
實現切片返回Vector對象
def __getitem__(self,index):
'''
這個內容審查可以參閱:
class Myseq:
def __getitem__(self,index):
return index
從而查看index的行為
'''
cls = type(self)
if isinstance(index,slice):
return cls(self._components[index])#切片是返回slice
elif isinstance(index,numbers.Integral):
return self._components[index]
else:
msg = '{cls.__name__} indices must be integers'
raise TypeError(msg.format(cls=cls))
動態(tài)存取屬性
屬性查找失敗時,解釋器會調用getattr方法
所以傳說中的只讀屬性可以通過一種另類的方法來體現
問題:setattr
但是上述的問題,這種不存在的屬性,影響了賦值的體現,即看得到,能賦值,但是沒有效果(不存在的值會被記錄為此對象類屬性)
所以我們需要setattr避免異常表現
def __setattr__(self,name,value):
cls = type(self)
if len(name) == 1:
if name in cls.shortname_names:
error = 'Readonly attribute {attr_name!r}'
elif name.islower():
error = "Can't set attributes 'a' to 'z' in {cls_name!r}"
else:
error = ''
if error:
msg = error.format(cls_name = cls.__name__,attr_name = name)
raise AttributeError(msg)
super().__setattr__(name,value)
所以基本上getattr定義了,也要setattr以保證行為一致
散列
一樣,求異或
Section7 接口,從協(xié)議到抽象基類
協(xié)議讓python對象在系統(tǒng)中扮演特定的角色
回到我們的撲克類
原先的撲克類是一個不可變序列,因為我們沒有setitem
所以我們可以在運行時補上
def set_card(deck,position,card):
deck._cards[position] = card
FrenchDeck.__setitem__ = set_card
shuffle(deck)
我們將這種模式叫做猴子補丁
由此我們亦可以看出我們的協(xié)議是動態(tài)的,他并不在乎你的類型是啥,只要對象實現了對應的可變序列協(xié)定即可
這是一個大家庭
collections.abc
[圖片上傳失敗...(image-c26ef1-1567675097224)]
- Iterable,Container,Sized
基本所有人要實現的協(xié)議,主要體現在iter()支持迭代,contains()支持in運算符,len()支持len函數
- Sequence,Mapping,Set
不可變集合,Mutablexxx是他們可變的子類
- MappingView
.item(),.keys()和.values()返回的分別是ItemView、KeysView、ValuesView的實例
- Callable,Hashable
為內置函數isinstance提供支持,以一種安全的方法判斷對象能否散列
- Iterator
Iterable的子類
抽象基類的數字塔
import numbers
- Number
- Complex
- Real
- Rational
- Integral
繼承
首先有個問題,內置類型的方法不會調用子類覆蓋的方法,比如dict的子類覆蓋了getitem(),其不會被內置類型的get()方法調用
所以子類化內置類型有各種各樣的問題,所以,請子類化collections模塊
多重繼承的解析順序
類的mro屬性
其記錄了循序的超類解析順序
調用超類的方法
super().ping()#利用super()
A.ping(self)#直接在類上調用方法傳入實例
一些禱告
- 把接口繼承和實現繼承區(qū)分開
- 使用抽象基類顯式表示接口
- 通過混入重用代碼Mixin
- 在名稱中明確指明混入
- 抽象基類可以作為混入,反之不可
- 不要子類化多個具體類
- 為用戶提供聚合類
- 優(yōu)先使用對象組合,而不是類繼承
Section8 運算符重載
一元運算符
-(neg)負運算符
+(pos)正運算符
~(invert)位取反
abs(abs)絕對值
什么是a+b
實現基本的加法運算符
def __add__(self,other):
pairs = itertools.zip_longest(self,other,fillvalue = 0.0)
return Vector(a+b for a,b in pairs)
那么b+a呢?
a+b的檢查順序:檢查a有add,檢查b有radd
那么投機取巧下:
def __radd__(self,other):
return self+other
異常處理
主要是要處理TypeError,讓編譯器嘗試準備反轉運算符再拋出NotImplemented
def __add__(self,other):
try:
pairs = itertools.zip_longest(self,other,fillvalue = 0.0)
return Vector(a+b for a,b in pairs)
except TypeError:
return NotImplemented
什么是a*b
這里我們采用白鵝類型處理
def __mul__(self,scalar):
if isinstance(scalar,numbers.Real):
return Vector(n*scalar for n in self)
else:
return NotImplemented
def __rmul__(self,scalar):
return self * scalar
其他的運算符
注意哦,a+b的檢查順序應用到了很多運算符。比如==,同時還有后備機制(都不行時),同時也有一個問題,要返回NotImplemented才會繼續(xù)檢查下去
[圖片上傳失敗...(image-f40dc-1567675097224)]
這其實一個問題,程序員討厭驚喜
Section9 控制流程
迭代器,生成器,牛皮。一定程度避免了直接生成結果導致資源的極度浪費
單詞序列
序列可以迭代的原因
當解釋器需要迭代對象時,會自動調用iter()
而內置的iter行為如下
1.檢查對象是否實現了iter,若果實現就調用并返回一個迭代器
2.如果沒有,就調用getitem從零開始自行迭代
3.如果還是不行,那就返回TypeError
這種查詢方式就是經典的鴨子類型,如果換成白鵝類型,如下:
只檢查iter的實現,因為Iterable實現了subclasshook,默認包攬所有實現iter就不用大家去繼承抽象基類Iterable了
而實際Iterator的實現由兩部分組成:next(),iter()
next返回下一個對象,如果沒有則返回StopIteration異常
iter返回迭代器返回本身
可迭代對象
明確可迭代對象返回一個迭代器的設計理念,而不是即是又是
用生成器函數代替返回一個迭代器的設計理念
生成器函數
只要python函數體有yield,那么他就是生成器函數,調用它時會返回一個生成器對象
生成器和之前提到的概念一致
def gen_123():
yield 1
yield 2
yield 3
> gen_123
<function gen_123 at 0x0000000003A29D90>
> gen_123()
<generator object gen_123 at 0x0000000003A69840>
生成器其實是截斷函數的執(zhí)行(第一次執(zhí)行才是開始,不是停在第一個yield),待啟動時,才會繼續(xù)執(zhí)行,這一點和裝飾器差距很大
惰性實現:生成器表達式、生成器函數
即不要急于實現
等差數列生成器
自己的類
from fractions import Fraction
class ArithmeticProgression():
def __init__(self,start,step,end = None):
self.start = start
self.step = step
self.end = end
def __iter__(self):
result = type(self.start +self.step)(self.start)
forever = self.end is None
index = 0
while forever or result < self.end:
yield result
index += 1
result = self.start + self.step * index
ap = ArithmeticProgression(0,Fraction(2,3))
index = 0
for name in ap:
print(name)
if index > 10 :
break
index += 1
生成器函數
就是把上文class的iter拆出來了,不說話
itertools模塊
標準庫中的生成器
[圖片上傳失敗...(image-9405d4-1567675097224)]
[圖片上傳失敗...(image-6760e3-1567675097224)]
[圖片上傳失敗...(image-6ea4d3-1567675097224)]
[圖片上傳失敗...(image-e4c45b-1567675097224)]
[圖片上傳失敗...(image-c467ce-1567675097224)]
yield from
新的語法糖,用來調用生成器
歸約函數
[圖片上傳失敗...(image-caee59-1567675097224)]
更多有關iter的消息
iter(可調用對象,哨符)
類似截止符的操作。有一個比較有趣的用法
with open('mydata.txt') as fp:
for line in iter(fp.readline,'\n')
process_line(roll)
大佬的操作
Section10 上下文管理器和else
else
if
if為假時
for
循環(huán)完畢時
while
while條件為假時
try
沒有異常時執(zhí)行
發(fā)現一件事,這本書處處都在批判Guide對于語法糖和添加關鍵字,唯恐避之不及的態(tài)度的抱怨
不過確實是,雖然這種方法減少了語法糖,但是,理解難度也高了
上下文管理器
基本結構
enter初始化上下文管理器
exit無論以任何方式退出上下文管理器都會調用
注意不要在exit再次拋出返回的異常,而是應該返回True||False
示例
示例使用了一個修改print語境的上下文
class LookingGlass():
def __enter__(self):
import sys
self.original_write = sys.stdout.write#保存原write方法
sys.stdout.write = self.reverse_write#猴子補丁
return 'JABBERWOCKY'#上下文的返回值
def reverse_write(self,text):#翻轉輸出
self.original_write(text[::-1])
def __exit__(self,exc_type,exc_value,traceback):#exc_type異常類,exc_value.args異常實例,有些參數傳給異常構造方法,traceback對象,在finally中調用sys.exe_info()得到的就是這三個參數
import sys
sys.stdout.write = self.original_write#還原
if exc_type is ZeroDivisionError:
print('Please DO NOT divide by zero')
return True
保證在此上下文中輸出都是反序,結束后再恢復
這里注意print其實就是包裝的sys.stdout對象,將參數傳遞給此對象的write方法
使用@contextmanager裝飾器定義上下文管理器
通過yield把函數切分開來,前半部分是enter,后半部分是exit
其實就是把函數包裝成了包含enter||exit的類
import contextlib
@contextlib.contextmanager
def looking_glass():
import sys
original_write = sys.stdout.write
def reverse_write(text):
original_write(text[::-1])
sys.stdout.write = reverse_write
msg = ''
try:
yield 'JABBERWOCKY'#避免用戶在with模塊瞎玩
except ZeroDivisionError:
msg = 'Please DO NOT divide by zero'
finally:
sys.stdout.write = original_write
if msg:
print(msg)
有一點不一樣,用此裝飾器需要顯式拋出異常
Section11 協(xié)程
yield之前在生成器和上下文管理器中講過,其實yield雖然在這些不同的情況下用法迥異,但是yield其實就是流程控制語句,yield會把時間片交還出去,等待有人來喊他繼續(xù)執(zhí)行
協(xié)程
在生成器API中加入了.send(value)方法,生成器的調用方式可以用.send(...)發(fā)送數據,發(fā)送數據會成為生成器函數中yield表達式的值。
同時也有.throw(...)調用方拋出異常交給生成器,.close()終止生成器
演示
def simple_coroutine():
print('-> coroutine started!')
x = yield
print('-> coroutine received :',x)
#使用inspect.getgeneratorstate(...)確定協(xié)程的狀態(tài)
my_coro = simple_coroutine()#'GEN_CREATED'等待開始執(zhí)行
next(my_coro)#'GEN_RUNNING'解釋器正在執(zhí)行,也可以send(None)督促其執(zhí)行
#'GEN_SUSPENDED'在yield處暫停
my_coro.send(42)#'GEN_RUNNING'解釋器正在執(zhí)行
#'GEN_CLOSED'執(zhí)行結束
有個小問題,如果是x = yield a ,第一個next會發(fā)生什么?
要解決這個問題得明確兩件事。當函數中有yield時,他就不是一般的函數了,平時我們寫的simple_coroutine()并不是執(zhí)行函數,而是創(chuàng)建了生成器對象。當我們調用next(...)或者.send(...)時才會開始執(zhí)行,且是執(zhí)行到第一個yield ...(等號運算符是先執(zhí)行右邊再執(zhí)行左邊),對于x = yield a來說,賦值是沒有發(fā)生的,只是發(fā)生了yield返回了a的值
預激協(xié)程裝飾器
from functools import wraps
def coroutine(func):
"""裝飾器:向前執(zhí)行到第一個yield表達式,預先激活'func'"""
@wraps(func)
def primer(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return primer
終止協(xié)程和異常處理
當協(xié)程遇到無法處理的異常時,他會將異常向上拋出給next(...)或者.send(...)的調用方,協(xié)程立刻終止(即無法處理的錯誤下方的邏輯不會處理),再次調用協(xié)程只會受到StopIteration。
以上即基本法,所以當有些我們不希望協(xié)程終止的異常發(fā)生時||異常發(fā)生需要清理工作,需要try...catch...else...finally,去正常的結束協(xié)程
協(xié)程的返回值
協(xié)程正常結束時會,會返回StopIteration,似乎我們正常的return并不能解決問題,那么:
from collections import namedtuple
Results = namedtuple('Result','count average')
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Results(count,average)
avg = averager()
next(avg)
avg.send(12)
avg.send(32)
avg.send(None)
Traceback (most recent call last):
StopIteration: Result(count=2, average=22.0)
很明顯可以看到一個方法:捕獲StopIteration異常,StopIteration異常攜帶了結果
try:
avg.send(None)
excet StopIteration as exc:
result = exc.value
這種方法并不違規(guī),是PEP標準,后面yield from則就是用的這種方式
yield from
前面說過,yield from x會把控制權交給x,具體的來說應該是iter(x),但是這樣看來,似乎很沒有意義?
所以yield from 實際的主要作用是打開雙向的通道,把最外層調用方與最內層的子生成器連接起來。這樣兩者可以直接發(fā)送和產出值,還可以直接傳入異常,而不用在處于中間的協(xié)程添加大量的異常處理代碼。
這句話并不違背yield from x會把控制權交給x的說法,甚至不違背yield是流程控制語句的特性。yield from同樣是在等待,只不過面對的不再是單獨的對象,而是一個生成器,yield from在調用方和被調用方中間扮演了通信管道的角色
[圖片上傳失敗...(image-933fcb-1567675097224)]
- 調用方
可以直接通過委派生成器獲得子生成器的結果
通過.send(None)通知子生成器結束,同時委派生成器也結束了
- 委派生成器
預激活子生成器
捕獲子生成器的StopIteration異常的返回值,同時終結自己(回想前面的所述的yield賦值的問題,如果子生成器不返回StopIteration引導委派生成器退出的話,yield賦值的左邊永遠不會有值,因為一直沒有能輪到賦值語句執(zhí)行)
- 子生成器
生成器具體邏輯實現
案例分析
在大多數的理論體系中,協(xié)程是用來在單個線程中管理并發(fā)活動的,上述并沒有具體體現出來。接下來通過一個示例:離散事件仿真來說明如何使用協(xié)程代替線程處理并發(fā)
離散事件仿真
離散事件模擬將系統(tǒng)隨時間的變化抽象成一系列的離散時間點上的事件,通過按照事件時間順序處理事件來演進,是一種事件驅動的仿真世界觀。離散事件仿真將系統(tǒng)的變化看做一個事件,因此系統(tǒng)任何的變化都只能是通過處理相應的事件來實現,在兩個相鄰的事件之間,系統(tǒng)狀態(tài)維持前一個事件發(fā)生后的狀態(tài)不變。
示例
Section12 并發(fā)
你覺得并發(fā)要了解到什么程度?
如何派生出一堆獨立的線程,然后用隊列收集結果。
并發(fā)都是因為IO延時
網絡下載的三種風格
依序下載
沒啥好說的
concurrent.futures模塊
ThreadPoolExecutor
線程池
submit(fn, *args, **kwargs)
as_completed#傳入future,并開始執(zhí)行,做完會返回
map(func, *iterables, timeout=None, chunksize=1)#依序產生結果
ProcessPoolExecutor
進程池
asyncio包
優(yōu)化版的下載處理
包介紹tqdm
一個進度條工具,根據可迭代對象的len和iter屬性計算可迭代對象的時間
工具Toxiproxy
實際上是一個代理工具,但是又不是簡單的進行代理(tcp,可以配置策略,toxics 實現延遲,模擬故障
Celery
任務隊列
threading與multprocessing
線程與協(xié)程
python中沒有終止線程的方法,如果要終止,必須給線程發(fā)消息
threading
def superisor():
signal = Signal()
spinner =threading.Thread(target = spin,args = ('thinking!',signal))
print('spinner object:',spinner)
spinner.start()#激活線程
result = slow_function()
signal.go = False
spinner.join()#等待線程終止
return result
新版迭代通過threading.Event()控制
def supervisor(): # <9>
done = threading.Event()
'''
事件對象
這是線程之間通信的最簡單機制之一:一個線程發(fā)出事件信號,而其他線程等待該信號。
一個事件對象管理一個內部標志,調用 set() 方法可將其設置為true,調用 clear() 方法可將其設置為false,調用 wait() 方法將進入阻塞直到標志為true。
class threading.Event
實現事件對象的類。事件對象管理一個內部標志,調用 set() 方法可將其設置為true。調用 clear() 方法可將其設置為false。調用 wait() 方法將進入阻塞直到標志為true。這個標志初始時為false。
'''
spinner = threading.Thread(target=spin,
args=('thinking!', done))
'''
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)?
調用這個構造函數時,必需帶有關鍵字參數。參數如下:
group 應該為 None;為了日后擴展 ThreadGroup 類實現而保留。
target 是用于 run() 方法調用的可調用對象。默認是 None,表示不需要調用任何方法。
name 是線程名稱。默認情況下,由 "Thread-N" 格式構成一個唯一的名稱,其中 N 是小的十進制數。
args 是用于調用目標函數的參數元組。默認是 ()。
kwargs 是用于調用目標函數的關鍵字參數字典。默認是 {}。
如果 daemon 不是 None,線程將被顯式的設置為 守護模式,不管該線程是否是守護模式。如果是 None (默認值),線程將繼承當前線程的守護模式屬性。
'''
print('spinner object:', spinner) # <10>
spinner.start() # <11>
'''
start()
開始線程活動。
它在一個線程里最多只能被調用一次。它安排對象的 run() 方法在一個獨立的控制進程中調用。
如果同一個線程對象中調用這個方法的次數大于一次,會拋出 RuntimeError 。
'''
result = slow_function() # <12>
done.set() # <13>
'''
set()
將內部標志設置為true。所有正在等待這個事件的線程將被喚醒。當標志為true時,調用 wait() 方法的線程不會被被阻塞。
'''
spinner.join() # <14>
'''
> join(timeout=None)
等待,直到線程終結。這會阻塞調用這個方法的線程,直到被調用 join() 的線程終結 -- 不管是正常終結還是拋出未處理異常 -- 或者直到發(fā)生超時,超時選項是可選的。
'''
return result
asyncio
這個鬼迭代的有點快,以官方文檔為實例,快速迭代一遍:
入門示例
>>> import asyncio
>>> async def main():
... print('hello')
... await asyncio.sleep(1)
... print('world')
>>> asyncio.run(main())
hello
world
加載協(xié)程的方法
- asyncio.run() 函數用來運行最高層級的入口點 "main()" 函數 (參見上面的示例。)
- asyncio.create_task() 函數用來并發(fā)運行作為 asyncio 任務 的多個協(xié)程。
這里面其實涉及兩個方面
一個是await直接等待協(xié)程方法執(zhí)行結束
另一個是await asyncio.create_task()對象,從而實現并發(fā)
可等待對象的定義
如果一個對象可以在 await 語句中使用,那么它就是 可等待 對象。許多 asyncio API 都被設計為接受可等待對象。
可等待 對象有三種主要類型: 協(xié)程, 任務 和 Future.
- 協(xié)程
協(xié)程函數: 定義形式為 async def 的函數;
協(xié)程對象: 調用 協(xié)程函數 所返回的對象。
當然還有基于生成器的老版協(xié)程
@asyncio.coroutine
def old_style_coroutine():
yield from asyncio.sleep(1)
- 任務
當一個協(xié)程通過 asyncio.create_task() 等函數被打包為一個 任務,該協(xié)程將自動排入日程準備立即運行: - Future
Future 是一種特殊的 低層級 可等待對象,表示一個異步操作的 最終結果。
當一個 Future 對象 被等待,這意味著協(xié)程將保持等待直到該 Future 對象在其他地方操作完畢。
在 asyncio 中需要 Future 對象以便允許通過 async/await 使用基于回調的代碼。
通常情況下 沒有必要 在應用層級的代碼中創(chuàng)建 Future 對象。
Future 對象有時會由庫和某些 asyncio API 暴露給用戶,用作可等待對象:
async def main():
await function_that_returns_a_future_object()
# this is also valid:
await asyncio.gather(
function_that_returns_a_future_object(),
some_python_coroutine()
)
一個很好的返回對象的低層級函數的示例是 loop.run_in_executor()。
方法庫
運行 asyncio 程序
asyncio.run(coro, *, debug=False)
此函數運行傳入的協(xié)程,負責管理 asyncio 事件循環(huán)并 完結異步生成器。
當有其他 asyncio 事件循環(huán)在同一線程中運行時,此函數不能被調用。創(chuàng)建任務
asyncio.create_task(coro)
將 coro 協(xié)程 打包為一個 Task 排入日程準備執(zhí)行。返回 Task 對象。休眠
coroutine asyncio.sleep(delay, result=None, *, loop=None)
阻塞 delay 指定的秒數。
如果指定了 result,則當協(xié)程完成時將其返回給調用者。并發(fā)運行任務
awaitable asyncio.gather(*aws, loop=None, return_exceptions=False)
并發(fā) 運行 aws 序列中的 可等待對象。屏蔽取消操作
awaitable asyncio.shield(aw, *, loop=None)
保護一個 可等待對象 防止其被 取消。超時
coroutine asyncio.wait_for(aw, timeout, *, loop=None)
等待 aw 可等待對象 完成,指定 timeout 秒數后超時。
函數將等待直到目標對象確實被取消,所以總等待時間可能超過 timeout 指定的秒數。簡單等待
coroutine asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
并發(fā)運行 aws 指定的 可等待對象 并阻塞線程直到滿足 return_when 指定的條件。
asyncio.as_completed(aws, *, loop=None, timeout=None)
并發(fā)地運行 aws 集合中的 可等待對象。返回一個 Future 對象的迭代器。返回的每個 Future 對象代表來自剩余可等待對象集合的最早結果。
來自其他線程的日程安排
asyncio.run_coroutine_threadsafe(coro, loop)
向指定事件循環(huán)提交一個協(xié)程。線程安全。
返回一個 concurrent.futures.Future 以等待來自其他 OS 線程的結果。內省
Task 對象
- class asyncio.Task(coro, *, loop=None)
- cancel()
請求取消 Task 對象。 - cancelled()
如果 Task 對象 被取消 則返回 True。 - done()
如果 Task 對象 已完成 則返回 True。 - result()
返回 Task 的結果。 - exception()
返回 Task 對象的異常。
aiohttp
asyncio實現了TCP、UDP、SSL等協(xié)議,aiohttp則是基于asyncio實現的HTTP框架
async with
異步上下文管理器”async with”
異步上下文管理器指的是在enter和exit方法處能夠暫停執(zhí)行的上下文管理器。
主要示例
async def download_many(cc_list):
async with aiohttp.ClientSession() as session: # <8>
res = await asyncio.gather( # <9>
*[asyncio.create_task(download_one(session, cc))
for cc in sorted(cc_list)])
return len(res)
總結一下
其實理論上所有的協(xié)程都可以直接把await或者yield from忽視掉。為什么說await后面接的是可等待對象,就是因為await后面接的對象要是暫停了,控制權就會交回事件循環(huán)手中,再去驅動其他協(xié)程。而為嘛使用了asyncio后我們不再用next(...)或.send(...)了,因為協(xié)程的驅動我們交還給了事件循環(huán)。甚至在3.7中,事件循環(huán)也不顯示調用了。
更進一步的理解 阻塞性調用
[圖片上傳失敗...(image-7c8b48-1567675097224)]
- 將阻塞性操作交給線程
在線程或者進程池中執(zhí)行代碼。
awaitable loop.run_in_executor(executor, func, *args)
安排在指定的執(zhí)行器中調用 func 。
import asyncio
import concurrent.futures
def blocking_io():
# File operations (such as logging) can block the
# event loop: run them in a thread pool.
with open('/dev/urandom', 'rb') as f:
return f.read(100)
def cpu_bound():
# CPU-bound operations will block the event loop:
# in general it is preferable to run them in a
# process pool.
return sum(i * i for i in range(10 ** 7))
async def main():
loop = asyncio.get_running_loop()
## Options:
# 1. Run in the default loop's executor:
result = await loop.run_in_executor(
None, blocking_io)
print('default thread pool', result)
# 2. Run in a custom thread pool:
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(
pool, blocking_io)
print('custom thread pool', result)
# 3. Run in a custom process pool:
with concurrent.futures.ProcessPoolExecutor() as pool:
result = await loop.run_in_executor(
pool, cpu_bound)
print('custom process pool', result)
asyncio.run(main())
再來一個方法asyncio.Semaphore
semaphore = asyncio.Semaphore(concur_req)#限制并發(fā)量的同步裝置
初始化計數器
semaphore = asyncio.Semaphore(concur_req)
計數器減一
semaphore.acquire()
計數器加一
semaphore.release()
當計數器小于等于零則堵塞協(xié)程,或者用async with semaphore:當上下文使用,自動控制
try:
async with semaphore:#限制并發(fā)量
image = await get_flag(session,base_url,cc)
聊個天
回調地獄
為什么會有yield from ,又為什么會有回調呢?其實這是一個很簡單的問題,很遠的前方就說過,要預防阻塞性調用,即各種I/O操作對CPU性能的浪費,CPU默默地角落里哭泣,你又讓他等硬盤寫入,還讓他等不知道多久以后才會有返回的TCP鏈接,CPU心里苦你知道嗎!
為什么認為回調是地獄呢?其實回調不管嵌套不嵌套,第一點難以理解,這還好,最大的問題是閉包,這是一個和垃圾回收機制有關的問題,函數調用會在結束時把其作用域沒有失去關聯的變量全部回收,回調很容易陷入:哎,為嘛取不到變量值?然后設定一大堆全局變量接參的故事。
每次循環(huán)多個請求
線程版很簡單,阻塞線程兩次就好了
協(xié)程也簡單,委托給兩個協(xié)程就好
TCP通信模塊
server端處理模塊
async def handle_queries(reader,writer):
while True:
writer.write(PROMPT)#StreamWriter.write向流中寫入數據
await writer.drain()#StreamWriter.drain等到適當時再恢復對流的寫入
data = await reader.readline()#StreamReader.readline讀一行,其中“l(fā)ine”是以\n。結尾的字節(jié)序列。如果收到EOF \n但未找到,則該方法返回部分讀取的數據。如果收到EOF且內部緩沖區(qū)為空,則返回一個空bytes對象。
try:
query = data.decode().strip()
except UnicodeDecodeError:#處理telnet客戶端decode異常,設定為傳空字符
query = '\x00'
client = writer.get_extra_info('peername')#返回與套接字連接的遠程地址
print('Received from {}: {!r}'.format(client,query))
if query:
if ord(query[:1]) < 32:#收到控制字符,退出
break
lines = list(index.find_description_strs(query))
if lines:
writer.writelines(line.encode() + CRLF for line in lines)#將一個列表(或任何可迭代的)字節(jié)寫入流。
writer.write(index.status(query,len(lines)).encode() + CRLF)
await writer.drain()#刷新輸出緩沖
print('Sent {} results'.format(len(lines)))
print('Close the client socket')
writer.close()
Section13 動態(tài)屬性和特性
動態(tài)屬性訪問JSON類數據
邏輯上很簡單,即json對象轉化為python原生對象時正常都是dict或者list,我們只能用feed['Schedule'][40]['name']之類的方法去訪問dict或者list結構,但是點屬性的方式是否更加nice呢?比如feed.Schedule[40].name這樣的效果。
所以我們會去構造類的屬性獲取方法,getattr(self,name),處理dict或者list結構。
@classmethod
def build(cls,obj):#備選構造方案
if isinstance(obj,abc.Mapping):#映射對象
return cls(obj)
elif isinstance(obj,abc.MutableSequence):#列表對象
return [cls.build(item) for item in obj]
else:
return obj
處理無效屬性名
有一個問題:python保留字導致屬性無法訪問,比如grad.class
當然我們可以這么做:getattr(grad,'class')
還有一個問題:無效標識符導致屬性無法訪問(python3可以根據str.isidentifier()判定str是否為有效標識符)
常見的方法是替換為通用名稱或者拋出異常
使用new方法以靈活的方式創(chuàng)建對象
前面我們是在getattr時,依據不同的值返回不同類型的對象,其實初始化過程中就可以構建對象
def __new__(cls,arg):
if isinstance(arg,abc.Mapping):
return super().__new__(cls)
elif isinstance(arg,abc.MutableSequence):
return [cls(item) for item in arg]
else:
return arg
shelve模塊
俗稱架子,一個類似字典,但是值可以是幾乎任意python對象,鍵是字符串,他的背后由dbm支持。
def load_db(db):
raw_data = osconfeed.load()
warnings.warn('loading '+DB_NAME)
for collection,res_list in raw_data['Schedule'].items():
record_type = collection[:-1]
for record in res_list:
key = '{}.{}'.format(record_type,record['serial'])#定義key值
record['serial'] = key
db[key] = Record(**record)
更近一步構建關系網
# BEGIN SCHEDULE2_RECORD
import warnings
import inspect # <1>
import osconfeed
DB_NAME = 'data/schedule2_db' # <2>
CONFERENCE = 'conference.115'
class Record:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __eq__(self, other): # 判斷屬性是否一致,比的是__dict__和歸屬類
if isinstance(other, Record):
return self.__dict__ == other.__dict__
else:
return NotImplemented
# END SCHEDULE2_RECORD
# BEGIN SCHEDULE2_DBRECORD
class MissingDatabaseError(RuntimeError):
"""需要數據庫時但是沒有指定時拋出.""" # 代替pass語句說明下用途
class DbRecord(Record): # <2>
__db = None # 儲存一個打開的shelve.Shelf數據庫引用
@staticmethod # <4>
def set_db(db):
DbRecord.__db = db # 設置shelve.Shelf數據庫引用
@staticmethod # <6>
def get_db():
return DbRecord.__db #返回shelve.Shelf數據庫引用
@classmethod # <7>
def fetch(cls, ident):
'''獲取傳入鍵對應的值'''
db = cls.get_db()
try:
return db[ident] # 獲取對應的鍵的數據
except TypeError:
if db is None: # <9>
msg = "database not set; call '{}.set_db(my_db)'"#未設置db
raise MissingDatabaseError(msg.format(cls.__name__))#說明未設置數據庫
else: # 非db is None那只能拋出TypeError了
raise
def __repr__(self):
if hasattr(self, 'serial'): # <11>
cls_name = self.__class__.__name__
return '<{} serial={!r}>'.format(cls_name, self.serial)
else:
return super().__repr__() # <12>
# END SCHEDULE2_DBRECORD
# BEGIN SCHEDULE2_EVENT
class Event(DbRecord): # <1>
@property#標記對應函數名的讀值方法
def venue(self):
key = 'venue.{}'.format(self.venue_serial)
return self.__class__.fetch(key) # 使用繼承過來的fetch(舍近求遠的原因:預防存在屬性fetch)
@property
def speakers(self):
if not hasattr(self, '_speaker_objs'): #屬性存在檢查
spkr_serials = self.__dict__['speakers'] # 從__dict__實例中獲取屬性speakers的值
fetch = self.__class__.fetch # <5>
self._speaker_objs = [fetch('speaker.{}'.format(key))
for key in spkr_serials] # 將speaker記錄列表賦值給_speaker_objs
return self._speaker_objs # <7>
def __repr__(self):
if hasattr(self, 'name'): # <8>
cls_name = self.__class__.__name__
return '<{} {!r}>'.format(cls_name, self.name)
else:
return super().__repr__() # <9>
# END SCHEDULE2_EVENT
# BEGIN SCHEDULE2_LOAD
def load_db(db):
raw_data = osconfeed.load()
warnings.warn('loading ' + DB_NAME)
for collection, rec_list in raw_data['Schedule'].items():
record_type = collection[:-1] # <1>
cls_name = record_type.capitalize() # 首字母大寫
cls = globals().get(cls_name, DbRecord) # 從全局對象中獲取名稱對應的對象,找不到就用DbRecord
if inspect.isclass(cls) and issubclass(cls, DbRecord): # 判斷是否是派生類或者子類
factory = cls # <5>
else:
factory = DbRecord # 因為如果叫json里面叫event,已經有Event繼承DbRecord,就可以用class Event
for record in rec_list: # <7>
key = '{}.{}'.format(record_type, record['serial'])
record['serial'] = key
db[key] = factory(**record) # 不過創(chuàng)建的class不太一樣了
# END SCHEDULE2_LOAD
這個關系網任然是先load_db,但是把event單獨歸為DbRecord的子類Event,初始load時,因為有些值(單純的值Event幾個只讀特性值)在db里面fetch不到,所以存在TypeError,實際全部加載完成時,就有了,同時也多了_speaker_objs屬性
蠻復雜,慢慢推理論。
特性驗證屬性
property其實是一個類裝飾器,被裝飾的方法有一個setter屬性,從而綁定讀值和設值方法
一種老版的方法,有點像java的set,get
# BEGIN LINEITEM_V2B
class LineItem:
def __init__(self, description, weight, price):
self.description = description
self.weight = weight
self.price = price
def subtotal(self):
return self.weight * self.price
def get_weight(self): # <1>
return self.__weight
def set_weight(self, value): # <2>
if value > 0:
self.__weight = value
else:
raise ValueError('value must be > 0')
weight = property(get_weight, set_weight) # <3>
# END LINEITEM_V2B
特性會覆蓋實例屬性
前面有個詭異的特例 return self.class.fetch(key) # 使用繼承過來的fetch(舍近求遠的原因:預防存在屬性fetch)
因為特性都是類屬性,如果實例有用相同的屬性,實例屬性就會覆蓋類屬性,(只是數據表現層面上的,實際你用class.obj還是存在的)
- 實例屬性覆蓋類的數據屬性
- 實例屬性不會覆蓋類特性
- 新增的類特性覆蓋現有實例屬性
其實換個說法,當設定特性時,取實例屬性,會因為有同名的特性,導致返回的是特性對象。如果同名的不是特性,那還是取實例實際的屬性
特性可以從doc中獲取說明。裝飾器直接會顯示相關代碼,老式方法要傳入doc=''參數
處理屬性刪除操作
被裝飾的方法還有個deleter
@member.deleter
def member(self):
text = "BLACK KNIGHT (loses {})\n-- {}"
print(text.format(self.members.pop(0)),self.phrases.pop(0))
處理屬性的重要函數和屬性
特殊屬性
- class 對象所屬類的引用
- dict 一個映射,存儲對象和類的可寫屬性
- slots 限制示例能夠有哪些屬性
內置函數
- dir([object]) 列出對象的大多數屬性,無參會列出當前作用域的名稱
- getattr(object,name[,default]) 從對象中獲得name對應的屬性
- hasattr(object,name) 判斷存在與否
- setattr(object,name,value) 設定屬性值
- vars([object]) 返回object的dict
特殊方法
- delattr(self,name) del刪除屬性時觸發(fā)
- dir(self) 調用dir時觸發(fā)
- getattr(self,name) 僅獲取屬性失敗時
- getattribute(self,name) 當不是特殊屬性和方法時,獲取屬性觸發(fā) ,拋出AttributeError調用getattr
- setattr(self,name,value) 設定屬性值時調用
Section14 屬性描述符
創(chuàng)建一個實例,作為另一個類的類屬性(注意哦創(chuàng)建是實例,作為另一個類的屬性)
[圖片上傳失敗...(image-719a15-1567675097224)]
[圖片上傳失敗...(image-cf0e32-1567675097224)]
class Quantity:
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
if value > 0:
setattr(instance, self.storage_name, value)
else:
raise ValueError('value must be > 0')
沒有特別多想說的,一個更好的優(yōu)化方案
import abc
class AutoStorage: # 自動管理儲存屬性的描述符類
__counter = 0
def __init__(self):
cls = self.__class__
prefix = cls.__name__
index = cls.__counter
self.storage_name = '_{}#{}'.format(prefix, index)
cls.__counter += 1
def __get__(self, instance, owner):
if instance is None:
return self
else:
return getattr(instance, self.storage_name)
def __set__(self, instance, value):
setattr(instance, self.storage_name, value) # <2>
class Validated(abc.ABC, AutoStorage): # 抽象類,覆蓋__set__方法
def __set__(self, instance, value):
value = self.validate(instance, value) # <4>
super().__set__(instance, value) # <5>
@abc.abstractmethod
def validate(self, instance, value): # <6>
"""return validated value or raise ValueError"""
class Quantity(Validated): # 實現非零的驗證
"""a number greater than zero"""
def validate(self, instance, value):
if value <= 0:
raise ValueError('value must be > 0')
return value
class NonBlank(Validated): #實現非空字串驗證
"""a string with at least one non-space character"""
def validate(self, instance, value):
value = value.strip()
if len(value) == 0:
raise ValueError('value cannot be empty or blank')
return value # <8>
描述符類型
覆蓋性描述符
因為實現set方法會覆蓋實例屬性的賦值操作,所以叫覆蓋型描述符
而又由于存在get方法,常規(guī)讀操作也會被描述符操作替代
這種方法,描述符會覆蓋屬性,無法用常規(guī)方法訪問
沒有get方法的覆蓋性描述符
因為沒有get,所以讀操作會返回描述符本身
但這種方法,如果通過dict創(chuàng)建同名實例屬性,因為沒有get,讀操作就會返回實例屬性
非覆蓋性描述符
若果有同名實例屬性,那么描述符無法處理那個實例屬性
但是呢
這一切都看的很美好,依附于類的描述符無法控制為類屬性賦值的操作,
所以set無法控制對類屬性的賦值操作!?。?/p>
若想控制設置類屬性的操作,最好的方法就是將描述符依附于類的類上。
同時呢
這時候我們就來看下對于類和對象來說我們定義的方法有什么不同吧
obj.spam 綁定是是方法對象:可調用對象,里面包裝著函數
Managed.spam 獲取的是函數
所以函數體現出來其實就是個非覆蓋性描述符
描述符用法
- 使用特性以保持簡單
內置property其實是覆蓋性描述符,所以創(chuàng)建只讀最好的方法就是用特性 - 只讀描述符必須要有set方法
為防止同名屬性覆蓋描述符,實現只讀時,get、set都應該實現,set拋出AttributeError異常 - 用于驗證的描述符可以只有set方法
賦值驗證最快捷的方法就是在set中檢查,并最后在示例dict屬性中設置 - 僅有get方法的描述符可以實現高效緩存
這種方法用在get比較耗CPU時,之后可以用實例同名屬性緩存結果 - 非特殊方法可以被實例屬性覆蓋
因為特殊方法是基于x.class.repr(x)訪問的,簡單點說是類方法,所以不會被實例屬性覆蓋,但是非特殊方法get就會了。
Section15 類元編程
類工廠函數
使用type構造類
MyClass = type('MyClass',(MySuperClass,MyMixin),{'x':42,'x2':lambda self:self.x * 2})
等同于
class MyClass(MySuperClass, MyMixin):
x = 42
def x2(self):
return self.x * 2
def record_factory(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):
values = ', '.join('{}={!r}'.format(*i) for i in zip(self.__slots__,self))
return '{}({})'.format(self.__class__.__name__,values)
cls_attrs = dict(__slots__ = field_names,#組建類的屬性字典
__init__ = __init__,
__iter__ = __iter__,
__repr__ = __repr__)
return type(cls_name,(object,),cls_attrs)#用type構造方法構造新類
神奇的type的實例是類
導入時和運行時
首先是編譯的問題,不要把文件名命名為內部包名稱,會導致文件編譯后產生的.pyc文件影響其他文件導入內部包
其實是導包問題,會運行頂層代碼
何為頂層代碼:def語句、類的定義體構建類對象、
導入
>>> import evaltime
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha #先運行裝飾類定義體,再運行裝飾器函數
<[9]> ClassFour body
<[14]> evaltime module end
運行
PS E:\py_work\fluent_python\Section15> python .\evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1 #被裝飾器替代掉了方法
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y #但是沒有影響子類
<[14]> evaltime module end
<[4]> ClassOne.__del__ #結束后垃圾回收
元類
元類從type中獲得了構建類的能力
[圖片上傳失敗...(image-c8e17d-1567735785792)]
prepare
知道類的屬性構建的順序
類作為對象
- mro 獲取類的超類元組
- class
- name
- bases 由類的基類組成的元組
- qualname 從模塊的全局作用域到類的點分路徑
- subclasses 返回類的直接子類