首頁»Python»改善 Python 程序的 91 個建議(三)

                    改善 Python 程序的 91 個建議(三)

                    來源:馭風者 發布時間:2017-05-17 閱讀次數:

                    第 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 呢:

                    1. 借鑒了其他語言的特征

                    2. Python 語言本身的動態性決定了使用 self 能夠帶來一定便利

                    3. 在存在同名的局部變量以及實例變量的情況下使用 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__()中引發異常的情況下調用。

                    QQ群:WEB開發者官方群(515171538),驗證消息:10000
                    微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
                    提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
                    網友評論(共0條評論) 正在載入評論......
                    理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
                    登錄會員中心
                    福彩试机号今天 海南飞鱼游戏彩票控 22选5历史走势 体彩浙江20选518206期 河南11选5胆拖玩法 宁夏十一选五彩票平台 32张小牌九技术 内部六肖中特 安徽十一选五一定牛遗漏 新快3和老快3 微信足彩交流群 网上如何买彩票 德甲球队 500万彩票网澳客网 买彩票怎样选号码 推牌九绝技视频