改善 Python 程式的 91 個建議(三)
第 4 章 庫
建議 41:使用 argparse 處理命令列引數
Python 標準庫中有幾種關於處理命令列的方案:getopt、optparse、argparse。
現階段最好用的引數處理是argparse:
import argparse parse = argparse.ArgumentParser() parse.add_argument('-o', '--output') parse.add_argument('-v', dest='verbose', action='store_true') args = parser.parse_args()
關於命令列引數,我記得有個第三方庫超好用,好久貼個教程出來。
建議 42:使用 pandas 處理大型 CSV 檔案
CSV 作為一種逗號分隔型值的純文字格式檔案,常用於資料庫資料的匯入匯出,資料分析中記錄的儲存。Python 中的 csv 模組提供了對 CSV 的支援。
列出一些常用的 API:
reader(csvfile[, dialect='excel'][, fmtparam]) # 讀取一個 csv 檔案,返回一個 reader 物件 csv.writer(csvfile, dialect='excel', **fmtparams) # 寫入 csv 檔案 csv.DictWriter(csvfile, fieldnames, restval='', extrasaction='raise', dialect='excel')
當然,處理 CSV 還有更好的選擇,那就是大名鼎鼎的 Pandas,它提供兩種基本的資料結構:Series 和 DataFrame。這裡有個 Pandas 的教程,值得一看。
建議 43:一般情況下使用 ElementTree 解析 XML
給一個較好的學習教程,下面直接看例子吧:
count = 0 for event, elem in ET.iterparse('test.xml'): if event == 'end': if elem.tag == 'userid': count += 1 elem.clear() print(count)
建議 44:理解模組 pickle 優劣
pickle 是較為通用的序列化模組,其中兩個主要的函式dump()和load()分別用來進行物件的序列化和反序列化:
-
pickle.dump(obj, file[, protocol])
-
load(file)
In [1]: import pickle In [2]: data = {'name': 'Python', 'type': 'Language', 'version': '3.5.2'} In [3]: with open('pickle.dat', 'wb') as fp: ...: pickle.dump(data, fp) ...: In [4]: with open('pickle.dat', 'rb') as fp: ...: out = pickle.load(fp) ...: print(out) ...: {'version': '3.5.2', 'name': 'Python', 'type': 'Language'}
它還有個C語言的實現 cPickle,效能較好。但 pickle 限制較多:比如不能保證原子性操作,存在安全問題,跨語言相容性不好等。
建議 45:序列化的另一個不錯的選擇 JSON
這個應該不用多做介紹了吧,書中講得比較淺,又來放連結(逃...
建議 46:使用 traceback 獲取棧資訊
當發生異常,開發人員往往需要看到現場資訊,trackback 模組可以滿足這個需求,先列幾個常用的:
traceback.print_exc() # 列印錯誤型別、值和具體的trace資訊 traceback.print_exception(type, value, traceback[, limit[, file]]) # 前三個引數的值可以從sys.exc_info() raceback.print_exc([limit[, file]]) # 同上,不需要傳入那麼多引數 traceback.format_exc([limit]) # 同 print_exc(),返回的是字串 traceback.extract_stack([file, [, limit]]) # 從當前棧中提取 trace 資訊
traceback 模組獲取異常相關的資料是通過sys.exc_info()得到的,該函式返回異常型別type、異常value、呼叫和堆疊資訊traceback組成的元組。
同時 inspect 模組也提供了獲取 traceback 物件的介面。
建議 47:使用 logging 記錄日誌資訊
僅僅將資訊輸出到控制檯是遠遠不夠的,更為常見的是使用日誌儲存程式執行過程中的相關資訊,如執行時間、描述資訊以及錯誤或者異常發生時候的特定上下文資訊。Python 提供 logging 模組提供了日誌功能,將日誌分為 5 個級別:
Level使用情形DEBUG詳細的資訊,在追蹤問題的時候使用INFO正常的資訊WARNING一些不可預見的問題發生,或者將要發生,如磁碟空間低等,但不影響程式的執行ERROR由於某些嚴重的問題,程式中的一些功能受到影響CRITICAL嚴重的錯誤,或者程式本身不能夠繼續執行
之前完成過一個個人部落格,總算對日誌訊息有了一定的瞭解。總的來說,日誌訊息是給程式設計師看的,在開發中,我們需要看到程式執行時的方方面面的情況,這時候給日誌分級就派上用場,其實日誌訊息是由我們來決定它屬於哪一種型別。
logging.basicConfig([**kwargs]) 提供對日誌系統的基本配置:
格式描述filename指定 FileHandler 的檔名,而不是預設的 StreamHandlerfilemode開啟檔案的模式,同 open 函式中的同名引數,預設為 'a'format輸出格式字串datefmt日期格式level設定根 logger 的日誌級別stream指定 StreamHandler。這個引數若與 filename 衝突,忽略 stream
下面結合 traceback 和 logging 來記錄程式執行過程中的異常:
import traceback import sys import logging gList = ["a", "b", "c", "d", "e", "f", "g"] logging.basicConfig( # 配置日誌的輸出方式及格式 level = logging.DEBUG, filename = "log.txt", filemode = "w", format = "%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s % (message)s", ) def f(): gList[5] logging.info("[INFO]:calling method g() in f()") # 記錄正常的資訊 return g() def g(): logging.info("[INFO]:calling method h() in g()") return h() def h(): logging.info("[INFO]:Delete element in gList in h()") del gList[2] logging.info("[INFO]:calling method i() in h()") return i() def i(): logging.info("[INFO]:Append element i to gList in i()") gList.append("i") print(gList[7]) if __name__ == "__main__": logging.debug("Information during calling f():") try: f() except IndexError as ex: print("Sorry, Exception occured, you accessed an element out of range") # traceback.print_exc() ty, tv, tb = sys.exc_info() logging.error("[ERROR]: Sorry, Exception occured, you accessed an element out of range") # 記錄異常錯誤訊息 logging.critical("object info:%s" % ex) logging.critical("Error Type:{0}, Error Information:{1}".format(ty, tv)) # 記錄異常的型別和對應的值 logging.critical("".join(traceback.format_tb(tb))) # 記錄具體的 trace 資訊 sys.exit(1)
logging 模組讓我們可以很方便地控制日誌資訊,如loggging.disable()傳入一個日誌級別會禁用該級別或比級別更低的日誌訊息,預設是全部禁用。大致我們常用的日誌記錄就這些了。
建議 48:使用 threading 模組編寫多執行緒程式
之前學習廖老師的 Python3 教程的時候,關於執行緒有句話記得特別清楚:
多執行緒的併發在Python中就是一個美麗的夢。
由於 GIL 的存在,讓 Python 多執行緒程式設計在多核處理器中無法發揮優勢,但在一些使用場景下使用多執行緒仍然比較好,如等待外部資源返回,或建立反應靈活的使用者介面,或多使用者程式等。
Python3 提供了兩個模組:_thread和threading。_thread提供了底層的多執行緒支援,使用比較複雜,下面我們重點說說threading。
Python 多執行緒支援用兩種方式來建立執行緒:一種通過繼承 Thread 類,重寫它的run()方法;另一種是建立一個 threading.Thread 物件,在它的初始化函式__init__()中將可呼叫物件作為引數傳入。
threading模組中不僅有 Lock 指令鎖,RLock 可重入指令鎖,還支援條件變數 Condition、訊號量 Semaphore、BoundedSemaphore 以及 Event 事件等。
下面有一個比較經典的例子來理解多執行緒:
import threading from time import ctime,sleep def music(func): for i in range(2): print("I was listening to %s. %s" % (func,ctime())) sleep(1) # 程式休眠 1 秒 def move(func): for i in range(2): print("I was at the %s! %s" % (func,ctime())) sleep(5) threads = [] t1 = threading.Thread(target=music,args=('愛情買賣',)) threads.append(t1) t2 = threading.Thread(target=move,args=('阿凡達',)) threads.append(t2) if __name__ == '__main__': for t in threads: t.setDaemon(True) # 宣告執行緒為守護執行緒 t.start() #3 print("all over %s" % ctime())
以下是執行結果:
I was listening to 愛情買賣. Tue Apr 4 17:57:02 2017 I was at the 阿凡達! Tue Apr 4 17:57:02 2017 all over Tue Apr 4 17:57:02 2017
分析:threading 模組支援執行緒守護,我們可以通過setDaemon()來設定執行緒的daemon屬性,當其屬性為True時,表明主執行緒的退出可以不用等待子執行緒完成,反之,daemon屬性為False時所有的非守護執行緒結束後主執行緒才會結束,那執行結果為:
I was listening to 愛情買賣. Tue Apr 4 18:05:26 2017 I was at the 阿凡達! Tue Apr 4 18:05:26 2017 all over Tue Apr 4 18:05:26 2017 I was listening to 愛情買賣. Tue Apr 4 18:05:27 2017 I was at the 阿凡達! Tue Apr 4 18:05:31 2017
繼續修改程式碼,當我們在#3處加入t.join(),此方法能夠阻塞當前上下文環境,直到呼叫該方法的執行緒終止或到達指定的 timeout,此時在執行程式:
I was listening to 愛情買賣. Tue Apr 4 18:08:15 2017 I was at the 阿凡達! Tue Apr 4 18:08:15 2017 I was listening to 愛情買賣. Tue Apr 4 18:08:16 2017 I was at the 阿凡達! Tue Apr 4 18:08:20 2017 all over Tue Apr 4 18:08:25 2017
當我們把music函式的休眠時間改為 4 秒,再次執行程式:
I was listening to 愛情買賣. Tue Apr 4 18:11:16 2017 I was at the 阿凡達! Tue Apr 4 18:11:16 2017 I was listening to 愛情買賣. Tue Apr 4 18:11:20 2017 I was at the 阿凡達! Tue Apr 4 18:11:21 2017 all over Tue Apr 4 18:11:26 2017
此時我們就可以發現多執行緒的威力了,music雖然增加了 3 秒,然而總的執行時間仍然為 10 秒。
建議 49:使用 Queue 使多執行緒程式設計更加安全
執行緒間的同步和互斥,執行緒間資料的共享等這些都是涉及執行緒安全要考慮的問題。縱然 Python 中提供了眾多的同步和互斥機制,如 mutex、condition、event 等,但同步和互斥本身就不是一個容易的話題,稍有不慎就會陷入死鎖狀態或者威脅執行緒安全。
如何保證執行緒安全呢?我們先來看看 Python 中的 Queue 模組:
-
Queue.Queue(maxsize):先進先出,maxsize 為佇列大小,其值為非正數的時候為無限迴圈佇列
-
Queue.LifoQueue(maxsize):後進先出,相當於棧
-
Queue.PriorityQueue(maxsize):優先順序佇列
以上佇列所支援的方法:
-
Queue.qsize():返回近似的佇列大小。當該值 > 0 的時候並不保證併發執行的時候 get() 方法不被阻塞,同樣,對於 put() 方法有效。
-
Queue.empty():佇列為空的時候返回 True,否則返回 False
-
Queue.full():當設定了佇列大小的情況下,如果佇列滿則返回 True,否則返回 False
-
Queue.put(item[, block[, timeout]]):往佇列中新增元素 item,block 設定為 False 的時候,如果佇列滿則丟擲 Full 異常。如果 block 設定為 True,timeout 為 None 的時候則會一直等待直到有空位置,否則會根據 timeout 的設定超時後丟擲 Full 異常
-
Queue.put_nowait(item):等於 put(item, False).block 設定為 False 的時候,如果佇列空則丟擲 Empty 異常。如果 block 設定為 True、timeout 為 None 的時候則會一直等到有元素可用,否則會根據 timeout 的設定超時後丟擲 Empty 異常
-
Queue.get([block[, timeout]]):從佇列中刪除元素並返回該元素的值
-
Queue.get_nowait():等價於 get(False)
-
Queue.task_done():傳送訊號表明入列任務已經完成,經常在消費者執行緒中用到
-
Queue.join():阻塞直至佇列中所有的元素處理完畢
首先 Queue 中的佇列和 collections.deque 所表示的佇列並不一樣,前者用於不同執行緒之間的通訊,內部實現了執行緒的鎖機制,後者是資料結構上的概念,支援 in 方法。
Queue 模組實現了多個生產者多個消費者的佇列,當多執行緒之間需要資訊保安的交換的時候特別有用,因此這個模組實現了所需要的鎖原語,為 Python 多執行緒程式設計提供了有力的支援,它是執行緒安全的。
先來看一個簡單的例子:
import os import Queue import threading import urllib2 class DownloadThread(threading.Thead): def __init__(self, queue): threading.Thread.__init__(self) self.queue = queue def run(self): while True: url = self.queue.get() print('{0} begin download {1}...'.format(self.name, url)) self.download_file(url) self.queque.task_done() print('{0} download completed!!!'.format(self.name)) def download_file(self, url): urlhandler = urllib2.urlopen(url) fname = os.path.basename(url) + '.html' with open(fname, 'wb') as f: while True: chunk = urlhandler.read(1024) if not chunk: break f.write(chunk) if __name__ == '__main__': urls = ['http://wiki.python.org/moin/WebProgramming', 'https://www.createspace.com/3611970', 'http://wiki.python.org/moin/Documentation' ] queue = Queue.Queue() for i range(5): t = DownloadThread(queue) t.setDaemon(True) t.start() for url in urls: queue.put(url) queue.join()
第 5 章 設計模式
建議 50:利用模組實現單例模式
滿足單例模式的 3 個需求:
-
只能有一個例項
-
必須自行建立這個例項
-
必須自行向整個系統提供這個例項
下面我們使用 Python 實現一個帶鎖的單例:
class Singleton(object): objs = {} objs_locker = threading.Lock() def __new__(cls, *args, **kw): if cls in cls.objs: return cls.objs(cls) cls.objs_locker.acquire() try: if cls in cls.objs: return cls.objs(cls) cls.objs[cls] = object.__new__(cls) finally: cls.objs_locker.release()
當然這種方案也存在問題:
-
如果 Singleton 的子類過載了__new__(),會覆蓋或干擾 Singleton 類中__new__()的執行
-
如果子類有__init__(),那麼每次例項化該 Singleton 的時候,__init__()都會被呼叫,這顯然是不應該的
雖然以上問題都有解決方案,但讓單例的實現不夠 Pythonic。我們可以重新審視 Python 的語法元素,發現模組採用的其實是天然的單例的實現方式:
-
所有的變數都會繫結到模組
-
模組只初始化一次
-
import 機制是執行緒安全的,保證了在併發狀態下模組也只是一個例項
# World.py import Sun def run(): while True: Sun.rise() Sun.set() # main.py import World World.run()
感覺這是最炫酷的單例模式。
建議 51:用 mixin 模式讓程式更加靈活
模板方法模式就是在一個方法中定義一個演算法的骨架,並將一些實現步驟延遲到子類中。模板方法可以使子類在不改變演算法結構的情況下,重新定義演算法中的某些步驟。
來看一個例子:
class People(object): def make_tea(self): teapot = self.get_teapot() teapot.put_in_tea() teapot.put_in_water() return teapot
顯然get_teapot()方法並不需要預先定義,也就是說我們的基類不需要預先申明抽象方法,子類只需要繼承 People 類並實現get_teapot(),這給除錯程式碼帶來了便利。但我們又想到如果一個子類 StreetPeople 描述的是正走在街上的人,那這個類將不會實現get_teapot(),一呼叫make_tea()就會產生找不到get_teapot()的 AttributeError,所以此時程式設計師應該立馬想到,隨著需求的增多,越來越多的 People 子類會選擇不喝茶而喝咖啡,或者是抽雪茄之類的,按照以上的思路,我們的程式碼只會變得越發難以維護。
所以我們希望能夠動態生成不同的例項:
class UseSimpleTeapot(object): def get_teapot(self): return SimpleTeapot() class UseKungfuTeapot(object): def get_teapot(self): return KungfuTeapot() class OfficePeople(People, UseSimpleTeapot): pass class HomePeople(People, UseSimpleTeapot): pass class Boss(People, UseKungfuTeapot): pass def simple_tea_people(): people = People() people.__base__ += (UseSimpleTeapot,) return people def coffee_people(): people = People() people.__base__ += (UseCoffeepot,) def tea_and_coffee_people(): people = People() people.__base__ += (UseSimpleTeapot, UserCoffeepot,) return people def boss(): people = People() people.__base__ += (KungfuTeapot, UseCoffeepot, ) return people
以上程式碼的原理在於每個類都有一個__bases__屬性,它是一個元組,用來存放所有的基類,作為動態語言,Python 中的基類可以在執行中可以動態改變。所以當我們向其中增加新的基類時,這個類就擁有了新的方法,這就是混入mixin。
利用這個技術我們可以在不修改程式碼的情況下就可以完成需求:
import mixins # 把員工需求定義在 Mixin 中放在 mixins 模組 def staff(): people = People() bases = [] for i in config.checked(): bases.append(getattr(maxins, i)) people.__base__ += tuple(bases) return people
建議 52:用釋出訂閱模式實現鬆耦合
釋出訂閱模式是一種程式設計模式,訊息的傳送者不會傳送其訊息給特定的接收者,而是將釋出的訊息分為不同的類別直接釋出,並不關注訂閱者是誰。而訂閱者可以對一個或多個類別感興趣,且只接收感興趣的訊息,並且不關注是哪個釋出者釋出的訊息。要實現這個模式,就需要一箇中間代理人 Broker,它維護著釋出者和訂閱者的關係,訂閱者把感興趣的主題告訴它,而釋出者的資訊也通過它路由到各個訂閱者處。
from collections import defaultdict route_table = defaultdict(list) def sub(topic, callback): if callback in route_table[topic]: return route_table[topic].append(callback) def pub(topic, *args, **kw): for func in route_table[topic]: func(*args, **kw)
將以上程式碼放在 Broker.py 的模組,省去了各種引數檢測、優先處理、取消訂閱的需求,只向我們展示釋出訂閱模式的基礎實現:
import Broker def greeting(name): print('Hello, {}'.format(name)) Broker.sub('greet', greeting) Broker.pub('greet', 'LaiYonghao')
注意學習 blinker 和 python-message 兩個模組
建議 53:用狀態模式美化程式碼
所謂狀態模式,就是當一個物件的內在狀態改變時允許改變其行為,但這個物件看起來像是改變了其類。
def workday(): print('work hard') def weekend(): print('play harder') class People(object): pass people = People() while True: for i in range(1, 8): if i == 6: people.day = weekend if i == 1: people.day = workday people.day()
但上述例子還有缺陷:
-
查詢物件的當前狀態很麻煩
-
狀態切換時需要對原狀態做一些清掃工作,而對新狀態做初始化工作,因每個狀態需要做的事情不同,全部寫在切換狀態的程式碼中必然重複
這時候我們可以使用 Python-state 來解決。
改寫之前的例子:
from state import curr, switch, stateful, State, behavior @stateful class People(object): class Workday(State): default = True @behavior # 相當於staticmethod def day(self): # 這裡的self並不是Python的關鍵字,而是有助於我們理解狀態類的宿主是People的例項 print('work hard') class Weekend(State): @behavior def day(self): print('play harder') people = People() while True: for i in range(1, 8): if i == 6: switch(people, People.Weekend) if i == 1: switch(people, People.Workday) people.day()
@statefule裝飾器過載了被修飾的類的__getattr__()從而使得 People 的例項能夠呼叫當前狀態類的方法,同時被修飾的類的例項是帶有狀態的,能夠使用curr()查詢當前狀態,也可以使用switch()進行狀態切換,預設的狀態是通過類定義的 default 屬性標識,default = True的類成為預設狀態。
狀態類 Workday 和 Weekend 繼承自 State 類,從其派生的子類可以使用__begin__和__end___狀態轉換協議,自定義進入和離開當前狀態時對宿主的初始化和清理工作。
下面是一個真實業務的例子:
@stateful class User(object): class NeedSignin(State): default = True @behavior def signin(self, user, pwd): ... switch(self, Player.Signin) class Signin(State): @behavior def move(self, dst): ... @behavior def atk(self, other): ...
第 6 章 內部機制
建議 54:理解 built-in objects
Python 中一切皆物件,在新式類中,object 是所有內建型別的基類,使用者自定義的類可以繼承自 object 也可繼承自內建型別。
In [1]: class TestNewClass: ...: __metaclass__ = type ...: In [2]: type(TestNewClass) Out[2]: type In [3]: TestNewClass.__bases__ Out[3]: (object,) In [4]: a = TestNewClass() In [5]: type(a) Out[5]: __main__.TestNewClass In [6]: a.__class__ Out[6]: __main__.TestNewClass
新式類支援 property 和描述符特性,作為新式類的祖先,Object 類還定義了一些特殊方法:__new__()、__init__()、__delattr__()、__getattribute__()、__setattr__()、__hash__()、__repr__()、__str__()等。
建議 55:__init__()不是構造方法
class A(object): def __new__(cls, *args, **kw): print(cls) print(args) print(kw) print('----------') instance = object.__new__(cls, *args, **kw) print(instance) def __init__(self, a, b): print('init gets called') print('self is {}'.format(self)) self.a, self.b = a, b a1 = A(1, 2) print(a1.a) print(a1.b)
執行結果:
<class '__main__.A'> (1, 2) {} ---------- Traceback (most recent call last): File "test.py", line 19, in <module> a1 = A(1, 2) File "test.py", line 13, in __new__ instance = object.__new__(cls, *args, **kw) TypeError: object() takes no parameters
從結果中我們可以看出,程式輸出了__new__()呼叫所產生的輸出,並丟擲了異常。於是我們知道,原來__new__()才是真正建立例項,是類的構造方法,而__init__()是在類的物件建立好之後進行變數的初始化。上面程式丟擲異常是因為在__new__()中沒有顯式返回物件,a1此時為None,當去訪問例項屬性時就丟擲了異常。
根據官方文件,我們可以總結以下幾點:
-
object.__new__(cls[, args...]):其中 cls 代表類,args 為引數列表,為靜態方法
-
object.__init__(self[, args...]):其中 self 代表例項物件,args 為引數列表,為例項方法
-
控制例項建立的時候可使用 __new__() ,而控制例項初始化的時候使用 __init__()
-
__new__()需要返回類的物件,當返回類的物件時將會自動呼叫__init__()進行初始化,沒有物件返回,則__init__()不會被呼叫。__init__() 方法不需要顯示返回,預設為 None,否則會在執行時丟擲 TypeError
-
但當子類繼承自不可變型別,如 str、int、unicode 或者 tuple 的時候,往往需要覆蓋__new__()
-
覆蓋 __new__() 和 __init__() 的時候這兩個方法的引數必須保持一致,如果不一致將導致異常
下面我們來總結需要覆蓋__new__()的幾種特殊情況:
-
當類繼承不可變型別且預設的 __new__() 方法不能滿足需求的時候
-
用來實現工廠模式或者單例模式或者進行元類程式設計,使用__new__()來控制物件建立
-
作為用來初始化的 __init__() 方法在多繼承的情況下,子類的 __init__()方法如果不顯式呼叫父類的 __init__() 方法,則父類的 __init__() 方法不會被呼叫;通過super(子類, self).__init__()顯式呼叫父類的初始化方法;對於多繼承的情況,我們可以通過迭代子類的 __bases__ 屬性中的內容來逐一呼叫父類的初始化方法
分別來看例子加深理解:
# 建立一個集合能夠將任何以空格隔開的字串變為集合中的元素 class UserSet(frozenset): def __new__(cls, *args): if args and isinstance(args[0], str): args = (args[0].split(), ) + args[1:] return super(UserSet, cls).__new__(cls, *args) # 一個工廠類根據傳入的參量決定建立出哪一種產品類的例項 class Shape(object): def __init__(object): pass def draw(self): pass class Triangle(Shape): def __init__(self): print("I am a triangle") def draw(self): print("I am drawing triangle") class Rectangle(Shape): def __init__(self): print("I am a rectangle") def draw(self): print("I am drawing triangle") class Trapezoid(Shape): def __init__(self): print("I am a trapezoid") def draw(self): print("I am drawing triangle") class Diamond(Shape): def __init__(self): print("I am a diamond") def draw(self): print("I am drawing triangle") class ShapeFactory(object): shapes = {'triangle': Triangle, 'rectangle': Rectangle, 'trapzoid': Trapezoid, 'diamond': Diamond} def __new__(cls, name): if name in ShapeFactory.shapes.keys(): print('creating a new shape {}'.format(name)) return ShapeFactory.shapes[name]() else: print('creating a new shape {}'.format(name)) return Shape()
建議 56:理解名字查詢機制
在 Python 中所謂的變數其實都是名字,這些名字指向一個或多個 Python 物件。這些名字都存在於一個表中(名稱空間),我們稱之為區域性變數,呼叫locals()可以檢視:
>>> locals() {'__package__': None, '__spec__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None, '__name__': '__main__', '__builtins__': <module 'builtins' (built-in)>} >>> globals() {'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__doc__': None, '__spec__': None, '__name__': '__main__'}
Python 中的作用域分為:
-
區域性作用域: 一般來說函式的每次呼叫都會建立一個新的本地作用域, 擁有新的名稱空間
-
全域性作用域: 定義在 Python 模組檔案中的變數名擁有全域性作用域, 即在一個檔案的頂層的變數名僅在這個檔案內可見
-
巢狀作用域: 多重函式巢狀時才會考慮, 即使使用 global 進行申明也不能達到目的, 其結果最終是在巢狀的函式所在的名稱空間中建立了一個新的變數
-
內建作用域: 通過標準庫中的__builtin__實現的
當訪問一個變數的時候,其查詢順序遵循變數解析機制 LEGB 法則,即依次搜尋 4 個作用域:區域性作用域、巢狀作用域、全域性作用域以及內建作用域,並在第一個找到的地方停止搜尋,如果沒有搜到,則會丟擲異常。
Python 3 中引入了 nonlocal 關鍵字:
def foo(x): a = x def bar(): nonlocal a b = a * 2 a = b + 1 print(a) return bar
建議 57: 為什麼需要 self 引數
在類中當定義例項方法的時候需要將第一個引數顯式宣告為self, 而呼叫時不需要傳入該引數, 我們通過self.x訪問例項變數, self.m()訪問例項方法:
class SelfTest(object): def __init__(self.name): self.name = name def showself(self): print('self here is {}'.format(self)) def display(self): self.showself() print('The name is: {}'.format(self.name)) st = SelfTest('instance self') st.display() print('{}'.format(st))
執行結果:
self here is <__main__.SelfTest object at 0x7f440c53ba58> The name is: instance self <__main__.SelfTest object at 0x7f440c53ba58>
從中可以發現, self 表示例項物件本身, 即 SelfTest 類的物件在記憶體中的地址. self 是對物件 st 本身的引用, 我們在呼叫例項方法時也可以直接傳入例項物件: SelfTest.display(st). 同時 self 或 cls 並不是 Python 的關鍵字, 可以替換成其它的名稱.
Python 中為什麼需要 self 呢:
-
借鑑了其他語言的特徵
-
Python 語言本身的動態性決定了使用 self 能夠帶來一定便利
-
在存在同名的區域性變數以及例項變數的情況下使用 self 使得例項變數更容易被區分
Python 屬於一級物件語言, 我們有好幾種方法可以引用類方法:
A.__dict__["m"] A.m.__func__
Python 的哲學是:顯示優於隱式(Explicit is better than implicit).
建議 58: 理解 MRO 與多繼承
古典類與新式類所採取的 MRO (Method Resolution Order, 方法解析順序) 的實現方式存在差異.
古典類是按照多繼承申明的順序形成繼承樹結構, 自頂向下採用深度優先的搜尋順序. 而新式類採用的是 C3 MRO 搜尋方法, 在新式類通過__mro__得到 MRO 的搜尋順序, C3 MRO 的演算法描述如下:
假定,C1C2...CN 表示類 C1 到 CN 的序列,其中序列頭部元素(head)=C1,序列尾部(tail)定義 = C2...CN;
C 繼承的基類自左向右分別表示為 B1,B2...BN
L[C] 表示 C 的線性繼承關係,其中 L[object] = object。
演算法具體過程如下:
L[C(B1...BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
其中 merge 方法的計算規則如下:在 L[B1]...L[BN],B1...BN 中,取 L[B1] 的 head,如果該元素不在 L[B2]...L[BN],B1...BN 的尾部序列中,則新增該元素到 C 的線性繼承序列中,同時將該元素從所有列表中刪除(該頭元素也叫 good head),否則取 L[B2] 的 head。繼續相同的判斷,直到整個列表為空或者沒有辦法找到任何符合要求的頭元素(此時,將引發一個異常)。
菱形繼承是我們在多繼承設計的時候需要儘量避免的一個問題.
建議 59: 理解描述符機制
In [1]: class MyClass(object): ...: class_attr = 1 ...: # 每一個類都有一個__dict__屬性, 包含它的所有屬性 In [2]: MyClass.__dict__ Out[2]: mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, '__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, 'class_attr': 1}) In [3]: my_instance = MyClass() # 每一個例項也相應有一個例項屬性, 我們通過例項訪問一個屬性時, # 它首先會嘗試在例項屬性中查詢, 找不到會到類屬性中查詢 In [4]: my_instance.__dict__ Out[4]: {} # 例項訪問類屬性 In [5]: my_instance.class_attr Out[5]: 1 # 如果通過例項增加一個屬性,只能改變此例項的屬性 In [6]: my_instance.inst_attr = 'china' In [7]: my_instance.__dict__ Out[7]: {'inst_attr': 'china'} # 對於類屬性而言並沒有絲毫變化 In [8]: MyClass.__dict__ Out[8]: mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>, '__doc__': None, '__module__': '__main__', '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, 'class_attr': 1}) # 我們可以動態地給類增加一個屬性 In [9]: MyClass.class_attr2 = 100 In [10]: my_instance.class_attr2 Out[10]: 100 # 但Python的內建型別並不能隨意地為它增加屬性或方法
.操作符封裝了對例項屬性和類屬性兩種不同屬性進行查詢的細節。
但是如果是訪問方法呢:
In [1]: class MyClass(object): ...: def my_method(self): ...: print('my_method') ...: In [2]: MyClass.__dict__['my_method'] Out[2]: <function __main__.MyClass.my_method> In [3]: MyClass.my_method Out[3]: <function __main__.MyClass.my_method> In [4]: type(MyClass.my_method) Out[4]: function In [5]: type(MyClass.__dict__['my_method']) Out[5]: function
根據通過例項訪問屬性和根據類訪問屬性的不同,有以下兩種情況:
-
一種是通過例項訪問,比如程式碼 obj.x,如果 x 是一個描述符,那麼 __getattribute__() 會返回 type(obj).__dict__['x'].__get__(obj, type(obj)) 結果,即:type(obj) 獲取 obj 的型別;type(obj).__dict__['x'] 返回的是一個描述符,這裡有一個試探和判斷的過程;最後呼叫這個描述符的 __get__() 方法。
-
另一個是通過類訪問的情況,比如程式碼 cls.x,則會被 __getattribute__()轉換為 cls.__dict__['x'].__get__(None, cls)。
描述符協議是一個 Duck Typing 的協議,而每一個函式都有 __get__ 方法,也就是說其他每一個函式都是描述符。所有對屬性, 方法進行修飾的方案往往都用到了描述符, 如classmethod, staticmethod, property等, 以下是property的參考實現:
class Property(object): "Emulate PyProperty_Type() in Objects/descrobject.c" def __init__(self, fget=None, fset=None, fdel=None, doc=None): self.fget = fget self.fset = fset self.fdel = fdel self.__doc__ = doc def __get__(self, obj, objtype=None): if obj is None: return self if self.fget is None: raise AttributeError, "unreadable attribute" return self.fget(obj) def __set__(self, obj, value): if self.fset is None: raise AttributeError, "can't set attribute" self.fset(obj, value) def __delete__(self, obj): if self.fdel is None: raise AttributeError, "can't delete attribute" self.fdel(obj)
建議 60:區別__getattr__()和__getattribute__()方法
以上兩種方法可以對例項屬性進行獲取和攔截:
-
__getattr__(self, name):適用於屬性在例項中以及對應的類的基類以及祖先類中都不存在;
-
__getattribute__(self, name):對於所有屬性的訪問都會呼叫該方法
但訪問不存在的例項屬性時,會由內部方法__getattribute__()丟擲一個 AttributeError 異常,也就是說只要涉及例項屬性的訪問就會呼叫該方法,它要麼返回實際的值,要麼丟擲異常。詳情請參考。
那麼__getattr__()在什麼時候呼叫呢:
-
屬性不在例項的__dict__中;
-
屬性不在其基類以及祖先類的__dict__中;
-
觸發AttributeError異常時(注意,不僅僅是__getattribute__()方法的AttributeError異常,property 中定義的get()方法丟擲異常的時候也會呼叫該方法)。
當這兩個方法同時被定義的時候,要麼在__getattribute__()中顯式呼叫,要麼觸發AttributeError異常,否則__getattr__()永遠不會被呼叫。
我們知道 property 也能控制屬性的訪問,如果一個類中如果定義了 property、__getattribute__()以及__getattr__()來對屬性進行訪問控制,會最先搜尋__getattribute__()方法,由於 property 物件並不存在於 dict 中,因此並不能返回該方法,此時會搜尋 property 中的get()方法;當 property 中的set()方法對屬性進行修改並再次訪問 property 的get()方法會丟擲異常,這時會觸發__getattr__()的呼叫。
__getattribute__()總會被呼叫,而__getattr__()只有在__getattribute__()中引發異常的情況下呼叫。
相關文章
- 改善 Python 程式的 91 個建議Python
- 改善 Python 程式的 91 個建議(一)Python
- 改善 Python 程式的 91 個建議(二)Python
- 改善 Python 程式的 91 個建議(四)Python
- 改善 Python 程式的 91 個建議(六)Python
- 《改善python程式的91個建議》讀書筆記Python筆記
- 編寫高質量程式碼 改善Python程式的91個建議Python
- 讀改善c#程式碼157個建議:建議1~3C#
- 讀改善c#程式碼157個建議:建議4~6C#
- 讀改善c#程式碼157個建議:建議7~9C#
- 讀改善c#程式碼157個建議:建議10~12C#
- 讀改善c#程式碼157個建議:建議13~15C#
- Flutter 6 個建議改善你的程式碼結構Flutter
- 編寫高質量程式碼:改善Java程式的151個建議(第4章:字串___建議52~55)Java字串
- 編寫高質量程式碼:改善Java程式的151個建議(第4章:字串___建議56~59)Java字串
- 《編寫高質量程式碼:改善Java程式的151個建議》筆記Java筆記
- 程式設計師必備基礎:改善Java程式的20個實用建議程式設計師Java
- 學習Java程式設計的三個建議Java程式設計
- 編寫高質量程式碼:改善Java程式的151個建議(第2章:基本型別___建議21~25)Java型別
- 編寫高質量程式碼:改善Java程式的151個建議(第2章:基本型別___建議26~30)Java型別
- 改善網頁設計的10個絕佳SEO建議網頁
- 編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議93~97)Java泛型反射
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議60~64)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議65~69)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議70~74)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議75~78)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第5章:陣列和集合___建議79~82)Java陣列
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議41~46)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議47~51)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議31~35)Java物件
- 編寫高質量程式碼:改善Java程式的151個建議(第3章:類、物件及方法___建議36~40)Java物件
- 改善 ASP.NET MVC 程式碼庫的 5 點建議ASP.NETMVC
- 改善Java文件的理由、建議和技巧Java
- 編寫高質量程式碼:改善Java程式的151個建議(第6章:列舉和註解___建議83~87)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第6章:列舉和註解___建議88~92)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議110~113)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第8章:異常___建議114~117)Java
- 編寫高質量程式碼:改善Java程式的151個建議(第7章:泛型和反射___建議98~101)Java泛型反射