學習筆記四:改善Python程序的91個建議

第 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 = 10print(obj.x + 2)del obj.xobj.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.__var1pt = 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 selfclass 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 = 1print(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 = availablea = 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 laiIn [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 -rVideos/Templates/Public/Pictures/Music/examples.desktopDropbox/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 來使用。比如在列表中篩選數據:

@Pipedef 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) | addOut[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) | addIn [5]: euler2Out[5]: 257114

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

from __future__ import print_functionfrom re import splitfrom 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._afor 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)) ...: A1A1A1A2A1A3A1B1A1B2A1B3A1C1A1C2...

建議 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) ...: 0112358In [2]: f = fib(10)In [3]: type(f)Out[3]: generatorIn [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 contextmanagerIn [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 greenletdef 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 geventIn [2]: from gevent import socketIn [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 gcIn [2]: print(gc.isenabled())TrueIn [3]: gc.isenabled()Out[3]: TrueIn [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 setupsetup(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() + "

" +? open("HISTORY.rst").read(),? author="Kenneth Reitz",? author_email="me@kennethreitz.com",? 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-prjlicense_name = keywords = Pythonlong_description = corp-prjauthor = xxx corpauthor_email = xxx@example.comurl = http://example.comversion = 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 得到測試結果。


推薦閱讀:

碎片化學習Python的又一神作:termux
從零開始寫Python爬蟲 --- 導言
Python爬蟲學習系列教程

TAG:Python | Python入门 | Python教程 |