1. <em id="2qvri"><tr id="2qvri"></tr></em>
      1. 首頁»Python»改善 Python 程序的 91 個建議(四)

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

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

        第 6 章 內部機制

        建議 61:使用更加安全的 property

        property 實際上是一種實現了 __get__() 、 __set__() 方法的類,用戶也可以根據自己的需要定義個性化的 property,其實質是一種特殊的數據描述符(數據描述符:如果一個對象同時定義了 __get__() 和 __set__() 方法,則稱為數據描述符,如果僅定義了__get__() 方法,則稱為非數據描述符)。它和普通描述符的區別在于:普通描述符提供的是一種較為低級的控制屬性訪問的機制,而 property 是它的高級應用,它以標準庫的形式提供描述符的實現,其簽名形式為:

        property(fget=None, fset=None, fdel=None, doc=None) -> property attribute
        

        property 有兩種常用的形式:

        1、第一種形式

        class Some_Class(object):
            def __init__(self):
                self._somevalue = 0
            def get_value(self):
                print('calling get method to return value')
                return self._somevalue
            def set_value(self, value):
                print('calling set method to set value')
                self._somevalue = value
            def def_attr(self):
                print('calling delete method to delete value')
                def self._somevalue
            x = property(get_value, set_value, del_attr, "I'm the 'x' property.")
        obj = Some_Class()
        obj.x = 10
        print(obj.x + 2)
        del obj.x
        obj.x
        

        2、第二種形式

        class Some_Class(self):
            _x = None
            def __init__(self):
                self._x = None
            @property
            def x(self):
                print('calling get method to return value')
                return self._x
            @x.setter
            def x(self, value):
                print('calling set method to set value')
                self._x = value
            @x.deleter
            def x(self):
                print('calling delete method to delete value')
                del self._x
        

        以上我們可以總結出 property 的優勢:

        1、代碼更簡潔,可讀性更強

        2、更好的管理屬性的訪問。property 將對屬性的訪問直接轉換為對對應的 get、set 等相關函數的調用,屬性能夠更好地被控制和管理,常見的應用場景如設置校驗(如檢查電子郵件地址是否合法)、檢查賦值的范圍(某個變量的賦值范圍必須在 0 到 10 之間)以及對某個屬性進行二次計算之后再返回給用戶(將 RGB 形式表示的顏色轉換為#******)或者計算某個依賴于其他屬性的屬性。

        class Date(object):
            def __init__(self, year, month, day):
                self.year = year
                self.month = month
                self.day = day
            def get_date(self):
                return self.year + '-' + self.month + '-' + self.day
            def set_date(self, date_as_string):
                year, month, day = date_as_string.split('-')
                if not (2000 <= year <= 2017 and 0 <= month <= 12 and 0 <= day <= 31):
                    print('year should be in [2000:2017]')
                    print('month should be in [0:12]')
                    print('day should be in [0, 31]')
                    raise AssertionError
                self.year = year
                self.month = month
                self.day = day
            date = property(get_date, set_date)
        

        創建一個 property 實際上就是將其屬性的訪問與特定的函數關聯起來,相對于標準屬性的訪問,property 的作用相當于一個分發器,對某個屬性的訪問并不直接操作具體的對象,而對標準屬性的訪問沒有中間這一層,直接訪問存儲屬性的對象:

        3、代碼可維護性更好。property 對屬性進行再包裝,以類似于接口的形式呈現給用戶,以統一的語法來訪問屬性,當具體實現需要改變的時候,訪問的方式仍然可以保持一致。

        4、控制屬性訪問權限,提高數據安全性。如果用戶想設置某個屬性為只讀,來看看 property 是如何實現的。

        class PropertyTest(object):
            def __init__(self):
                self.__var1 = 20
            @property
            def x(self):
                return self.__var1
        pt = PropertyTest()
        print(pt.x)
        pt.x = 12
        

        注意這樣使用 property 并不能真正意義達到屬性只讀的目的,正如以雙下劃線命令的變量并不是真正的私有變量一樣,我們還是可以通過pt._PropertyTest__var1 = 30來修改屬性。稍后我們會討論如何實現真正意義上的只讀和私有變量。

        既然 property 本質是特殊類,那么就可以被繼承,我們就可以自定義 property:

        def update_meta(self, other):
            self.__name__ = other.__name__
            self.__doc__ = other.__doc__
            self.__dict__.update(other.__dict__)
            return self
        class UserProperty(property):
            def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
                if fget is not None:
                    def __get__(obj, objtype=None, name=fget.__name__):
                        fegt = getattr(obj, name)
                        return fget()
                    fget = update_meta(__get__, fget)
                if fset is not None:
                    def __set__(obj, value, name=fset.__name__):
                        fset = getattr(obj, name)
                        return fset(value)
                    fset = update_meta(__set__, fset)
                if fdel is not None:
                    def __delete__(obj, name=fdel.__name__):
                        fdel = getattr(obj, name)
                        return fdel()
                    fdel = update_meta(__delete__, fdel)
                return property(fget, fset, fdel, doc)
        class C(object):
            def get(self):
                return self._x
            def set(self, x):
                self._x = x
            def delete(self):
                del self._x
            x = UserProperty(get, set, delete)
        c = C()
        c.x = 1
        print(c.x)
        def c.x
        

        UserProperty 繼承自 property,其構造函數 __new__(cls, fget=None, fset=None, fdel=None, doc=None) 中重新定義了 fget() 、 fset() 以及 fdel() 方法以滿足用戶特定的需要,最后返回的對象實際還是 property 的實例,因此用戶能夠像使用 property 一樣使用 UserProperty。

        使用 property 并不能真正完全達到屬性只讀的目的,用戶仍然可以繞過阻礙來修改變量。我們來看看一個可行的實現:

        def ro_property(obj, name, value):
            setattr(obj.__class__, name, property(lambda obj: obj.__dict__["__" + name]))
            setattr(obj, "__" + name, value)
        
        class ROClass(object):
            def __init__(self, name, available):
                ro_property(self, "name", name)
                self.available = available
        
        a = ROClass("read only", True)
        print(a.name)
        a._Article__name = "modify"
        print(a.__dict__)
        print(ROClass.__dict__)
        print(a.name)
        

        建議 62:掌握 metaclass

        關于元類這知識點,推薦stackoverflow上Jerub回答

        這里有中文翻譯

        建議 63:熟悉 Python 對象協議

        因為 Python 是一門動態語言,Duck Typing 的概念遍布其中,所以其中的 Concept 并不以類型的約束為載體,而另外使用稱為協議的概念。

        In [1]: class Object(object):
           ...:     def __str__(self):
           ...:         print('calling __str__')
           ...:         return super(Object, self).__str__()
           ...:     
        
        In [2]: o = Object()
        
        In [3]: print('%s' % o)
        calling __str__
        <__main__.Object object at 0x7f133ff20160>
        

        比如在字符串格式化中,如果有占位符 %s,那么按照字符串轉換的協議,Python 會自動地調用相應對象的 __str__() 方法。

        總結一下 Python 中的協議:

        1、類型轉換協議:__str__() 、__repr__()、__init__()、__long__()、__float__()、__nonzero__() 等。

        2、比較大小的協議:__cmp__(),當兩者相等時,返回 0,當 self < other 時返回負值,反之返回正值。同時 Python 又有 __eq__()、__ne__()、__lt__()、__gt__() 等方法來實現相等、不等、小于和大于的判定。這也就是 Python 對 ==、!=、< 和 > 等操作符的進行重載的支撐機制。

        3、數值相關的協議:

        其中有個 Python 中特有的概念:反運算。以something + other為例,調用的是something的__add__(),若沒有定義__add__(),這時候 Python 有一個反運算的協議,查看other有沒有__radd__(),如果有,則以something為參數調用。

        4、容器類型協議:容器的協議是非常淺顯的,既然為容器,那么必然要有協議查詢內含多少對象,在 Python 中,就是要支持內置函數 len(),通過 __len__() 來完成,一目了然。而 __getitem__()、__setitem__()、__delitem__() 則對應讀、寫和刪除,也很好理解。__iter__() 實現了迭代器協議,而 __reversed__() 則提供對內置函數 reversed() 的支持。容器類型中最有特色的是對成員關系的判斷符 in 和 not in 的支持,這個方法叫 __contains__(),只要支持這個函數就能夠使用 in 和 not in 運算符了。

        5、可調用對象協議:所謂可調用對象,即類似函數對象,能夠讓類實例表現得像函數一樣,這樣就可以讓每一個函數調用都有所不同。

        In [1]: class Functor(object):
           ...:     def __init__(self, context):
           ...:         self._context = context
           ...:     def __call__(self):
           ...:         print('do something with %s' % self._context)
           ...:         
        
        In [2]: lai_functor = Functor('lai')
        
        In [3]: yong_functor = Functor('yong')
        
        In [4]: lai_functor()
        do something with lai
        
        In [5]: yong_functor()
        do something with yong
        

        6、還有一個可哈希對象,它是通過 __hash__() 方法來支持 hash() 這個內置函數的,這在創建自己的類型時非常有用,因為只有支持可哈希協議的類型才能作為 dict 的鍵類型(不過只要繼承自 object 的新式類就默認支持了)。

        7、上下文管理器協議:也就是對with語句的支持,該協議通過__enter__()和__exit__()兩個方法來實現對資源的清理,確保資源無論在什么情況下都會正常清理:

        class Closer:
            def __init__(self):
                self.obj = obj
            def __enter__(self):
                return self.obj
            def __exit__(self, exception_type, exception_val, trace):
                try:
                    self.obj.close()
                except AttributeError:
                    print('Not closeable.')
                    return True
        

        這里 Closer 類似的類已經在標準庫中存在,就是 contextlib 里的 closing。

        以上就是常用的對象協議,靈活地用這些協議,我們可以寫出更為 Pythonic 的代碼,它更像是聲明,沒有語言上的約束,需要大家共同遵守。

        建議 64:利用操作符重載實現中綴語法

        熟悉 Shell 腳本編程應該熟悉|管道符號,用以連接兩個程序的輸入輸出。如按字母表反序遍歷當前目錄的文件與子目錄:

        $ ls | sort -r
        Videos/
        Templates/
        Public/
        Pictures/
        Music/
        examples.desktop
        Dropbox/
        Downloads/
        Documents/
        Desktop/
        

        管道的處理非常清晰,因為它是中綴語法。而我們常用的 Python 是前綴語法,比如類似的 Python 代碼應該是 sort(ls(), reverse=True)。

        Julien Palard 開發了一個 pipe 庫,利用|來簡化代碼,也就是重載了 __ror__() 方法:

        class Pipe:
            def __init__(self, function):
                self.function = function
            def __ror__(self, other):
                return self.function(other)
            def __call__(self, *args, **kwargs):
                return Pipe(lambda x: self.function(x, *args, **kwargs))
        

        這個 Pipe 類可以當成函數的 decorator 來使用。比如在列表中篩選數據:

        @Pipe
        def where(iterable, predicate):
            return (x for x in iterable if (predicate(x)))
        

        pipe 庫內置了一堆這樣的處理函數,比如 sum、select、where 等函數盡在其中,請看以下代碼:

        fib() | take_while(lambda x: x < 1000000) \
              | where(lambda x: x % 2) \
              | select(lambda x: x * x) \
              | sum()
        

        這樣寫的代碼,意義是不是一目了然呢?就是找出小于 1000000 的斐波那契數,并計算其中的偶數的平方之和。

        我們可以使用pip3 install pipe安裝,安裝完后測試:

        In [1]: from pipe import *
        
        In [2]: [1, 2, 3, 4, 5] | where(lambda x: x % 2) | tail(2) | select(lambda x: x * x) | add
        Out[2]: 34
        

        此外,pipe 是惰性求值的,所以我們完全可以弄一個無窮生成器而不用擔心內存被用完:

        In [3]: def fib():
           ...:     a, b = 0, 1
           ...:     while True:
           ...:         yield a
           ...:         a, b = b, a + b
           ...:         
        
        In [4]: euler2 = fib() | where(lambda x: x % 2 ==0) | take_while(lambda x: x < 400000) | add
        
        In [5]: euler2
        Out[5]: 257114
        

        讀取文件,統計文件中每個單詞出現的次數,然后按照次數從高到低對單詞排序:

        from __future__ import print_function
        from re import split
        from pipe import *
        with open("test_descriptor.py") as f:
            print(f.read()
                  | Pipe(lambda x: split("/W+", x))
                  | Pipe(lambda x:(i for i in x if i.strip()))
                  | groupby(lambda x:x)
                  | select(lambda x:(x[0], (x[1] | count)))
                  | sort(key=lambda x: x[1], reverse=True)
                  )
        

        建議 65:熟悉 Python 的迭代器協議

        首先介紹一下 iter() 函數,iter() 可以輸入兩個實參,為了簡化,第二個可選參數可以忽略。iter() 函數返回一個迭代器對象,接受的參數是一個實現了 __iter__() 方法的容器或迭代器(精確來說,還支持僅有 __getitem__() 方法的容器)。對于容器而言,__iter__() 方法返回一個迭代器對象,而對迭代器而言,它的 __iter__() 方法返回其自身。

        所謂協議,是一種松散的約定,并沒有相應的接口定義,所以把協議簡單歸納如下:

        1. 實現 __iter__() 方法,返回一個迭代器

        2. 實現 next() 方法,返回當前的元素,并指向下一個元素的位置,如果當前位置已無元素,則拋出 StopIteration 異常

        沒錯,其實 for 語句就是對獲取容器的迭代器、調用迭代器的 next() 方法以及對 StopIteration 進行處理等流程進行封裝的語法糖(類似的語法糖還有 in/not in 語句)。

        迭代器最大的好處是定義了統一的訪問容器(或集合)的統一接口,所以程序員可以隨時定義自己的迭代器,只要實現了迭代器協議即可。除此之外,迭代器還有惰性求值的特性,它僅可以在迭代至當前元素時才計算(或讀取)該元素的值,在此之前可以不存在,在此之后也可以銷毀,也就是說不需要在遍歷之前實現準備好整個迭代過程中的所有元素,所以非常適合遍歷無窮個元素的集合或或巨大的事物(斐波那契數列、文件):

        class Fib(object):
            def __init__(self):
                self._a, self._b = 0, 1
            def __iter__(self):
                return self
            def next(self):
                self._a, self._b = self._b, self._a + self._b
                return self._a
        for i, f in enumerate(Fib()):
            print(f)
            if i > 10:
                break
        

        下面來看看與迭代有關的標準庫 itertools。

        itertools 的目標是提供一系列計算快速、內存高效的函數,這些函數可以單獨使用,也可以進行組合,這個模塊受到了 Haskell 等函數式編程語言的啟發,所以大量使用 itertools 模塊中的函數的代碼,看起來有點像函數式編程語言。比如 sum(imap(operator.mul, vector1, vector2)) 能夠用來運行兩個向量的對應元素乘積之和。

        itertools 提供了以下幾個有用的函數:chain() 用以同時連續地迭代多個序列;compress()、dropwhile() 和 takewhile() 能用遴選序列元素;tee() 就像同名的 UNIX 應用程序,對序列作 n 次迭代;而 groupby 的效果類似 SQL 中相同拼寫的關鍵字所帶的效果。

        [k for k, g in groupby("AAAABBBCCDAABB")] --> A B C D A B
        [list(g) for k, g in groupby("AAAABBBCCD")] --> AAAA BBB CC D
        

        除了這些針對有限元素的迭代幫助函數之外,還有 count()、cycle()、repeat() 等函數產生無窮序列,這 3 個函數就分別可以產生算術遞增數列、無限重復實參的序列和重復產生同一個值的序列。

        組合函數意義product()計算 m 個序列的 n 次笛卡爾積permutations()產生全排列combinations()產生無重復元素的組合combinations_with_replacement()產生有重復元素的組合

        In [1]: from itertools import *
        
        In [2]: list(product('ABCD', repeat=2))
        Out[2]: 
        [('A', 'A'),
         ('A', 'B'),
         ('A', 'C'),
         ('A', 'D'),
         ('B', 'A'),
         ('B', 'B'),
         ('B', 'C'),
         ('B', 'D'),
         ('C', 'A'),
         ('C', 'B'),
         ('C', 'C'),
         ('C', 'D'),
         ('D', 'A'),
         ('D', 'B'),
         ('D', 'C'),
         ('D', 'D')]
        # 其中 product() 可以接受多個序列
        In [5]: for i in product('ABC', '123', repeat=2):
           ...:     print(''.join(i))
           ...:     
        A1A1
        A1A2
        A1A3
        A1B1
        A1B2
        A1B3
        A1C1
        A1C2
        ...
        

        建議 66:熟悉 Python 的生成器

        生成器,顧名思義,就是按一定的算法生成一個序列。

        迭代器雖然在某些場景表現得像生成器,但它絕非生成器;反而是生成器實現了迭代器協議的,可以在一定程度上看作迭代器。

        如果一個函數,使用了 yield 關鍵字,那么它就是一個生成器函數。當調用生成器函數時,它返回一個迭代器,不過這個迭代器是以生成器對象的形式出現的:

        In [1]: def fib(n):
           ...:     a, b = 0, 1
           ...:     while a < n:
           ...:         yield a
           ...:         a, b = b, a + b
           ...: for i, f in enumerate(fib(10)):
           ...:     print(f)
           ...:     
        0
        1
        1
        2
        3
        5
        8
        
        In [2]: f = fib(10)
        
        In [3]: type(f)
        Out[3]: generator
        
        In [4]: dir(f)
        Out[4]: 
        ['__class__',
         '__del__',
         '__delattr__',
         '__dir__',
         '__doc__',
         '__eq__',
         '__format__',
         '__ge__',
         '__getattribute__',
         '__gt__',
         '__hash__',
         '__init__',
         '__iter__',
         '__le__',
         '__lt__',
         '__name__',
         '__ne__',
         '__new__',
         '__next__',
         '__qualname__',
         '__reduce__',
         '__reduce_ex__',
         '__repr__',
         '__setattr__',
         '__sizeof__',
         '__str__',
         '__subclasshook__',
         'close',
         'gi_code',
         'gi_frame',
         'gi_running',
         'gi_yieldfrom',
         'send',
         'throw']
        

        可以看到它返回的是一個 generator 類型的對象,這個對象帶有__iter__()和__next__()方法,可見確實是一個迭代器。

        分析:

        1. 每一個生成器函數調用之后,它的函數并不執行,而是到第一次調用 next() 的時候才開始執行;

        2. yield 表達式的默認返回值為 None,當第一次調用 next() 方法時,生成器函數開始執行,執行到 yield 表達式為止;

        3. 再次調用next()方法,函數將在上次停止的地方繼續執行。

        send() 是全功能版本的 next(),或者說 next() 是 send()的快捷方式,相當于 send(None)。還記得 yield 表達式有一個返回值嗎?send() 方法的作用就是控制這個返回值,使得 yield 表達式的返回值是它的實參。

        除了能 yield 表達式的“返回值”之外,也可以讓它拋出異常,這就是 throw() 方法的能力。

        對于常規業務邏輯的代碼來說,對特定的異常有很好的處理(比如將異常信息寫入日志后優雅的返回),從而實現從外部影響生成器內部的控制流。

        當調用 close() 方法時,yield 表達式就拋出 GeneratorExit 異常,生成器對象會自行處理這個異常。當調用 close() 方法,再次調用 next()、send() 會使生成器對象拋出 StopIteration 異常。換言之,這個生成器對象已經不再可用。當生成器對象被 GC 回收時,會自動調用 close()。

        生成器還有兩個很棒的用處:

        • 實現 with 語句的上下文管理協議,利用的是調用生成器函數時函數體并不執行,當第一次調用 next() 方法時才開始執行,并執行到 yield 表達式后中止,直到下一次調用 next() 方法這個特性;

        • 實現協程,利用的是 send()、throw()、close() 等特性。

        第二個用處在下一個小節講解,先看第一個:

        In [1]: with open('/tmp/test.txt', 'w') as f:
           ...:     f.write('Hello, context manager.')
           ...:     
        
        In [2]: from contextlib import contextmanager
        
        In [3]: @contextmanager
           ...: def tag(name):
           ...:     print('<%s>' % name)
           ...:     yield
           ...:     print('<%s>' % name)
           ...:     
        
        In [4]: with tag('h1'):
           ...:     print('foo')
           ...:     
        <h1>
        foo
        <h1>
        

        這是 Python 文檔中的例子。通過 contextmanager 對 next()、throw()、close() 的封裝,yield 大大簡化了上下文管理器的編程復雜度,對提高代碼可維護性有著極大的意義。除此之外,yield 和 contextmanager 也可以用以“池”模式中對資源的管理和回收,具體的實現留給大家去思考。

        建議 67:基于生成器的協程及 greenlet

        先介紹一下協程的概念:

        協程,又稱微線程和纖程等,據說源于 Simula 和 Modula-2 語言,現代編程語言基本上都支持這個特性,比如 Lua 和 ruby 都有類似的概念。

        協程往往實現在語言的運行時庫或虛擬機中,操作系統對其存在一無所知,所以又被稱為用戶空間線程或綠色線程。又因為大部分協程的實現是協作式而非搶占式的,需要用戶自己去調度,所以通常無法利用多核,但用來執行協作式多任務非常合適。用協程來做的東西,用線程或進程通常也是一樣可以做的,但往往多了許多加鎖和通信的操作。

        基于生產著消費者模型,比較搶占式多線程編程實現和協程編程實現。線程實現至少有兩點硬傷:

        • 對隊列的操作需要有顯式/隱式(使用線程安全的隊列)的加鎖操作。

        • 消費者線程還要通過 sleep 把 CPU 資源適時地“謙讓”給生產者線程使用,其中的適時是多久,基本上只能靜態地使用經驗,效果往往不盡如人意。

        下面來看看協程的解決方案,代碼來自廖雪峰 Python3 教程

        def consumer():
            r = ''
            while True:
                n = yield r
                if not n:
                    return
                print('[CONSUMER] Consuming %s...' % n)
                r = '200 OK'
        
        def produce(c):
            c.send(None)
            n = 0
            while n < 5:
                n = n + 1
                print('[PRODUCER] Producing %s...' % n)
                r = c.send(n)
                print('[PRODUCER] Consumer return: %s' % r)
            c.close()
        
        c = consumer()
        produce(c)
        

        執行結果:

        [PRODUCER] Producing 1...
        [CONSUMER] Consuming 1...
        [PRODUCER] Consumer return: 200 OK
        [PRODUCER] Producing 2...
        [CONSUMER] Consuming 2...
        [PRODUCER] Consumer return: 200 OK
        [PRODUCER] Producing 3...
        [CONSUMER] Consuming 3...
        [PRODUCER] Consumer return: 200 OK
        [PRODUCER] Producing 4...
        [CONSUMER] Consuming 4...
        [PRODUCER] Consumer return: 200 OK
        [PRODUCER] Producing 5...
        [CONSUMER] Consuming 5...
        [PRODUCER] Consumer return: 200 OK
        

        注意到consumer函數是一個generator,把一個consumer傳入produce后:

        1. 首先調用c.send(None)啟動生成器;

        2. 然后,一旦生產了東西,通過c.send(n)切換到consumer執行;

        3. consumer通過yield拿到消息,處理,又通過yield把結果傳回;

        4. produce拿到consumer處理的結果,繼續生產下一條消息;

        5. produce決定不生產了,通過c.close()關閉consumer,整個過程結束。

        整個流程無鎖,由一個線程執行,produce和consumer協作完成任務,所以稱為“協程”,而非線程的搶占式多任務。

        最后套用Donald Knuth的一句話總結協程的特點:

        “子程序就是協程的一種特例。”

        greenlet 是一個 C 語言編寫的程序庫,它與 yield 關鍵字沒有密切的關系。greenlet 這個庫里最為關鍵的一個類型就是 PyGreenlet 對象,它是一個 C 結構體,每一個 PyGreenlet 都可以看到一個調用棧,從它的入口函數開始,所有的代碼都在這個調用棧上運行。它能夠隨時記錄代碼運行現場,并隨時中止,以及恢復。它跟 yield 所能夠做到的相似,但更好的是它提供從一個 PyGreenlet 切換到另一個 PyGreenlet 的機制。

        from greenlet import greenlet
        def test1():
            print(12)
            gr2.switch()
            print(34)
        def test2():
            print(56)
            gr1.switch()
            print(78)
        gr1 = greenlet(test1)
        gr2 = greenlet(test2)
        gr1.switch()
        

        協程雖然不能充分利用多核,但它跟異步 I/O 結合起來以后編寫 I/O 密集型應用非常容易,能夠在同步的代碼表面下實現異步的執行,其中的代表當屬將 greenlet 與 libevent/libev 結合起來的 gevent 程序庫,它是 Python 網絡編程庫。最后,以 gevent 并發查詢 DNS 的例子為例,使用它進行并發查詢 n 個域名,能夠獲得幾乎 n 倍的性能提升:

        In [1]: import gevent
        
        In [2]: from gevent import socket
        
        In [3]: urls = ['www.baidu.com', 'www.python.org', 'www.qq.com']
        
        In [4]: jobs = [gevent.spawn(socket.gethostbyname, url) for url in urls]
        
        In [5]: gevent.joinall(jobs, timeout=2)
        Out[5]: 
        [<Greenlet at 0x7f37e439c508>,
         <Greenlet at 0x7f37e439c5a0>,
         <Greenlet at 0x7f37e439c340>]
        
        In [6]: [job.value for job in jobs]
        Out[6]: ['115.239.211.112', '151.101.24.223', '182.254.34.74']
        

        建議 68:理解 GIL 的局限性

        多線程 Python 程序運行的速度比只有一個線程的時候還要慢,除了程序本身的并行性之外,很大程度上與 GIL 有關。由于 GIL 的存在,多線程編程在 Python 中并不理想。GIL 被稱為全局解釋器鎖(Global Interpreter Lock),是 Python 虛擬機上用作互斥線程的一種機制,它的作用是保證任何情況下虛擬機中只會有一個線程被運行,而其他線程都處于等待 GIL 鎖被釋放的狀態。不管是在單核系統還是多核系統中,始終只有一個獲得了 GIL 鎖的線程在運行,每次遇到 I/O 操作便會進行 GIL 鎖的釋放。

        但如果是純計算的程序,沒有I/O操作,解釋器則會根據sys.setcheckinterval的設置來自動進行線程間的切換,默認是每隔100個內部時鐘就會釋放GIL鎖從而輪換到其他線程:

        在單核 CPU 中,GIL 對多線程的執行并沒有太大影響,因為單核上的多線程本質上就是順序執行的。但對于多核 CPU,多線程并不能真正發揮優勢帶來效率上明顯的提升,甚至在頻繁 I/O 操作的情況下由于存在需要多次釋放和申請 GIL 的情形,效率反而會下降。

        那么 Python 解釋器為什么要引入 GIL 呢?

        我們知道 Python 中對象的管理與引用計數器密切相關,當計數器變為 0 的時候,該對象便會被垃圾回收器回收。當撤銷一個對象的引用時,Python 解釋器對對象以及其計數器的管理分為以下兩步:

        1. 使引用計數值減1

        2. 判斷該計數值是否為 0,如果為0,則銷毀該對象

        鑒于此,Python 引入了 GIL,以保證對虛擬機內部共享資源訪問的互斥性。

        GIL 的引入確實使得多線程不能再多核系統中發揮優勢,但它也帶來了一些好處:大大簡化了 Python 線程中共享資源的管理,在單核 CPU 上,由于其本質是順序執行的,一般情況下多線程能夠獲得較好的性能。此外,對于擴展的 C 程序的外部調用,即使其不是線程安全的,但由于 GIL 的存在,線程會阻塞直到外部調用函數返回,線程安全不再是一個問題。

        在 Python3.2 中重新實現了 GIL,其實現機制主要集中在兩個方面:一方面是使用固定的時間而不是固定數量的操作指令來進行線程的強制切換;另一個方面是在線程釋放 GIL 后,開始等待,直到某個其他線程獲取 GIL 后,再開始嘗試去獲取 GIL,這樣雖然可以避免此前獲得 GIL 的線程,不會立即再次獲取 GIL,但仍然無法保證優先級高的線程優先獲取 GIL。這種方式只能解決部分問題,并未改變 GIL 的本質。

        Python 提供了其他方式可以繞過 GIL 的局限,比如使用多進程 multiprocess 模塊或者采用 C 語言擴展的方式,以及通過 ctypes 和 C 動態庫來充分利用物理內核的計算能力。

        建議 69:對象的管理與垃圾回收

        class Leak(object):
            def __init__(self):
                print('object with id %d was born' % id(self))
        while(True):
            A = Leak()
            B = Leak()
            A.b = B
            B.a = A
            A = None
            B = None
        

        運行上述程序,我們會發現 Python 占用的內存消耗一直在持續增長,直到最后內存耗光。

        先簡單談談 Python 中的內存管理的方式:

        Python 使用引用計數器(Reference counting)的方法來管理內存中的對象,即針對每一個對象維護一個引用計數值來表示該對象當前有多少個引用。

        當其他對象引用該對象時,其引用計數會增加 1,而刪除一個隊當前對象的引用,其引用計數會減 1。只有當引用計數的值為 0 時的時候該對象才會被垃圾收集器回收,因為它表示這個對象不再被其他對象引用,是個不可達對象。引用計數算法最明顯的缺點是無法解決循環引用的問題,即兩個對象相互引用。如同上述代碼中A、B對象之間相互循環引用造成了內存泄露,因為兩個對象的引用計數都不為 0,該對象也不會被垃圾回收器回收,而無限循環導致一直在申請內存而沒有釋放。

        循環引用常常會在列表、元組、字典、實例以及函數使用時出現。對于由循環引用而導致的內存泄漏的情況,可以使用 Python 自帶的一個 gc 模塊,它可以用來跟蹤對象的“入引用(incoming reference)“和”出引用(outgoing reference)”,并找出復雜數據結構之間的循環引用,同時回收內存垃圾。有兩種方式可以觸發垃圾回收:一種是通過顯式地調用 gc.collect() 進行垃圾回收;還有一種是在創建新的對象為其分配內存的時候,檢查 threshold 閾值,當對象的數量超過 threshold 的時候便自動進行垃圾回收。默認情況下閾值設為(700,10,10),并且 gc 的自動回收功能是開啟的,這些可以通過 gc.isenabled() 查看:

        In [1]: import gc
        
        In [2]: print(gc.isenabled())
        True
        
        In [3]: gc.isenabled()
        Out[3]: True
        
        In [4]: gc.get_threshold()
        Out[4]: (700, 10, 10)
        

        所以修改之前的代碼:

        def main():
            collected = gc.collect()
            print("Garbage collector before running: collected {} objects.".format(collected))
            print("Creating reference cycles...")
            A = Leak()
            B = Leak()
            A.b = B
            B.a = A
            A = None
            B = None
            collected = gc.collect()
            print(gc.garbage)
            print("Garbage collector after running: collected {} objects".format(collected))
        
        if __name__ == "__main__":
            ret = main()
            sys.exit(ret)
        

        gc.garbage 返回的是由于循環引用而產生的不可達的垃圾對象的列表,輸出為空表示內存中此時不存在垃圾對象。gc.collect() 顯示所有收集和銷毀的對象的數目,此處為 4(2 個對象 A、B,以及其實例屬性 dict)。

        我們再來考慮一個問題:如果在類 Leak 中添加析構方法 __del__(),會發現 gc.garbage 的輸出不再為空,而是對象 A、B 的內存地址,也就是說這兩個對象在內存中仍然以“垃圾”的形式存在。

        這是什么原因呢?實際上當存在循環引用并且當這個環中存在多個析構方法時,垃圾回收器不能確定對象析構的順序,所以為了安全起見仍然保持這些對象不被銷毀。而當環被打破時,gc 在回收對象的時候便會再次自動調用 __del__() 方法。

        gc 模塊同時支持 DEBUG 模式,當設置 DEBUG 模式之后,對于循環引用造成的內存泄漏,gc 并不釋放內存,而是輸出更為詳細的診斷信息為發現內存泄漏提供便利,從而方便程序員進行修復。更多 gc 模塊可以參考文檔

        第 7 章 使用工具輔助項目開發

        Python 項目的開發過程,其實就是一個或多個包的開發過程,而這個開發過程又由包的安裝、管理、測試和發布等多個節點構成,所以這是一個復雜的過程,使用工具進行輔助開發有利于減少流程損耗,提升生產力。本章將介紹幾個常用的、先進的工具,比如 setuptools、pip、paster、nose 和 Flask-PyPI-Proxy 等。

        建議 70:從 PyPI 安裝包

        PyPI 全稱 Python Package Index,直譯過來就是“Python 包索引”,它是 Python 編程語言的軟件倉庫,類似 Perl 的 CPAN 或 Ruby 的 Gems。

        $ tar zxvf requests-1.2.3.tar.gz
        $ cd requests-1.2.3
        $ python setup.py install
        $ sudo aptitude install python-setuptools   # 自動安裝包
        

        建議 71:使用 pip 和 yolk 安裝、管理包

        pip 常用命令:

        $ pip install package_name
        $ pip uninstall package_name
        $ pip show package_name
        $ pip freeze
        

        建議 72:做 paster 創建包

        distutils 標準庫,至少提供了以下幾方面的內容:

        • 支持包的構建、安裝、發布(打包)

        • 支持 PyPI 的登記、上傳

        • 定義了擴展命令的協議,包括 distutils.cmd.Command 基類、distutils.commands 和 distutils.key_words 等入口點,為 setuptools 和 pip 等提供了基礎設施。

        要使用 distutils,按習慣需要編寫一個 setup.py 文件,作為后續操作的入口點。在arithmetic.py同層目錄下建立一個setup.py文件,內容如下:

        from distutils.core import setup
        setup(name="arithmetic",
             version='1.0',
             py_modules=["your_script_name"],
             )
        

        setup.py 文件的意義是執行時調用 distutils.core.setup() 函數,而實參是通過命名參數指定的。name 參數指定的是包名;version 指定的是版本;而 py_modules 參數是一個序列類型,里面包含需要安裝的 Python 文件。

        編寫好 setup.py 文件以后,就可以使用 python setup.py install 進行安裝了。

        distutils 還帶有其他命令,可以通過 python setup.py --help-commands 進行查詢。

        實際上若要把包提交到 PyPI,還要遵循 PEP241,給出足夠多的元數據才行,比如對包的簡短描述、詳細描述、作者、作者郵箱、主頁和授權方式等:

        setup(
            name='requests',?
            version=requests.__version__,?
            description='Python HTTP for Humans.',?
            long_description=open('README.rst').read() + '\n\n' +?
            				open('HISTORY.rst').read(),?
            author='Kenneth Reitz',?
            author_email='[email protected]',?
            url='http://python-requests.org',?
            packages=packages,?
            package_data={'': ['LICENSE', 'NOTICE'], 'requests': ['*.pem']},?
            package_dir={'requests': 'requests'},?
            include_package_data=True,?
            install_requires=requires,?
            license=open('LICENSE').read(),?
            zip_safe=False,?
            classifiers=(?
            	'Development Status :: 5 - Production/Stable',?
                'Intended Audience :: Developers',?
                'Natural Language :: English',
                'License :: OSI Approved :: Apache Software License',?
                'Programming Language :: Python',?
                'Programming Language :: Python :: 2.6',?
                'Programming Language :: Python :: 2.7',?
                'Programming Language :: Python :: 3',?
                'Programming Language :: Python :: 3.3',?
                ),?
        )
        

        包含太多內容了,如果每一個項目都手寫很困難,最好找一個工具可以自動創建項目的 setup.py 文件以及相關的配置、目錄等。Python 中做這種事的工具有好幾個,做得最好的是 pastescript。pastescript 是一個有著良好插件機制的命令行工具,安裝以后就可以使用 paster 命令,創建適用于 setuptools 的包文件結構。

        安裝好 pastescript 以后可以看到它注冊了一個命令行入口 paster:

        $ paster create --list-template     # 查詢目錄安裝的模板
        $ paster create -o arithmethc-2 -t basic_package atithmetic     # 為了 atithmetic 生成項目包
        

        簡單地填寫幾個問題以后,paster 就在 arithmetic-2 目錄生成了名為 arithmetic 的包項目。

        用上 --config 參數,它是一個類似 ini 文件格式的配置文件,可以在里面填好各個模板變量的值(查詢模板有哪些變量用 --list-variables參數),然后就可以使用了。

        [pastescript]
        description = corp-prj
        license_name = 
        keywords = Python
        long_description = corp-prj
        author = xxx corp
        author_email = [email protected]
        url = http://example.com
        version = 0.0.1
        

        以上配置文件使用paster create -t basic_package --config="corp-prj-setup.cfg" arithmetic

        建議 73:理解單元測試概念

        單元測試用來驗證程序單元的正確性,一般由開發人員完成,是測試過程的第一個環節,以確保縮寫的代碼符合軟件需求和遵循開發目標。好的單元測試有以下好處:

        • 減少了潛在 bug,提高了代碼的質量。

        • 大大縮減軟件修復的成本

        • 為集成測試提供基本保障

        有效的單元測試應該從以下幾個方面考慮:

        • 測試先行,遵循單元測試步驟:

          • 創建測試計劃(Test Plan)

          • 編寫測試用例,準備測試數據

          • 編寫測試腳本

          • 編寫被測代碼,在代碼完成之后執行測試腳本

          • 修正代碼缺陷,重新測試直到代碼可接受為止

        • 遵循單元測試基本原則:

          • 一致性:避免currenttime = time.localtime()這種不確定執行結果的語句

          • 原子性:執行結果只有 True 或 False 兩種

          • 單一職責:測試應該基于情景(scenario)和行為,而不是方法。如果一個方法對應著多種行為,應該有多個測試用例;而一個行為即使對應多個方法也只能有一個測試用例

          • 隔離性:不能依賴于具體的環境設置,如數據庫的訪問、環境變量的設置、系統的時間等;也不能依賴于其他的測試用例以及測試執行的順序,并且無條件邏輯依賴。單元測試的所有輸入應該是確定的,方法的行為和結構應是可以預測的。

        • 使用單元測試框架,在單元測試方面常見的測試框架有 PyUnit 等,它是 JUnit 的 Python 版本,在 Python2.1 之前需要單獨安裝,在 Python2.1 之后它成為了一個標準庫,名為 unittest。它支持單元測試自動化,可以共享地進行測試環境的設置和清理,支持測試用例的聚集以及獨立的測試報告框架。unittest 相關的概念主要有以下 4 個:

          • 測試固件(test fixtures):測試相關的準備工作和清理工作,基于類 TestCase 創建測試固件的時候通常需要重新實現 setUp() 和 tearDown() 方法。當定義了這些方法的時候,測試運行器會在運行測試之前和之后分別調用這兩個方法

          • 測試用例(test case):最小的測試單元,通常基于 TestCase 構建

          • 測試用例集(test suite):測試用例的集合,使用 TestSuite 類來實現,除了可以包含 TestCase 外,也可以包含 TestSuite

          • 測試運行器(test runner):控制和驅動整個單元測試過程,一般使用 TestRunner 類作為測試用例的基本執行環境,常用的運行器為 TextTestRunner,它是 TestRunner 的子類,以文字方式運行測試并報告結果。

        # 測試以下類
        class MyCal(object):
            def add(self, a, b):
                return a + b
            def sub(self, a, b):
                return a - b
        # 測試
        class MyCalTest(unittest.TestCase):
            def setUp(self):
                print('running set up')
            def tearDown(self):
                print('running teardown')
                self.mycal = None
            def testAdd(self):
                self.assertEqual(self.mycal.add(-1, 7), 6)
            def testSub(self):
                self.assertEqual(self.mycal.sub(10, 2), 8)
        suite = unittest.TestSuite()
        suite.addTest(MyCalTest("testAdd"))
        suite.addTest(MyCalTest("testSub"))
        runner = unittest.TextTestRunner()
        runner.run(suite)
        

        運行 python3 -m unittest -v MyCalTest 得到測試結果。

        QQ群:WEB開發者官方群(515171538),驗證消息:10000
        微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
        提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
        網友評論(共0條評論) 正在載入評論......
        理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
        登錄會員中心
        江苏快3投注技巧