1. 程式人生 > 其它 >介紹Python中的一些高階程式設計技巧

介紹Python中的一些高階程式設計技巧

正文:

本文展示一些高階的Python設計結構和它們的使用方法。在日常工作中,你可以根據需要選擇合適的資料結構,例如對快速查詢性的要求、對資料一致性的要求或是對索引的要求等,同時也可以將各種資料結構合適地結合在一起,從而生成具有邏輯性並易於理解的資料模型。Python的資料結構從句法上來看非常直觀,並且提供了大量的可選操作。這篇指南嘗試將大部分常用的資料結構知識放到一起,並且提供對其最佳用法的探討。
推導式(Comprehensions)

如果你已經使用了很長時間的Python,那麼你至少應該聽說過列表推導(list
comprehensions)。這是一種將for迴圈、if表示式以及賦值語句放到單一語句中的一種方法。換句話說,你能夠通過一個表示式對一個列表做對映或過濾操作。

一個列表推導式包含以下幾個部分:

  • 一個輸入序列
  • 一個表示輸入序列成員的變數
  • 一個可選的斷言表示式
  • 一個將輸入序列中滿足斷言表示式的成員變換成輸出列表成員的輸出表達式

舉個例子,我們需要從一個輸入列表中將所有大於0的整數平方生成一個新的序列,你也許會這麼寫:

    num = [1, 4, -5, 10, -7, 2, 3, -1]
    filtered_and_squared = []
     
    for number in num:
     if number > 0:
     filtered_and_squared.append(number ** 2)
    print filtered_and_squared
     
    # [1, 16, 100, 4, 9]
    

很簡單是吧?但是這就會有4行程式碼,兩層巢狀外加一個完全不必要的append操作。而如果使用filter、lambda和map函式,則能夠將程式碼大大簡化:

    num = [1, 4, -5, 10, -7, 2, 3, -1]
    filtered_and_squared = map(lambda x: x ** 2, filter(lambda x: x > 0, num))
    print filtered_and_squared
     
    # [1, 16, 100, 4, 9]
    

嗯,這麼一來程式碼就會在水平方向上展開。那麼是否能夠繼續簡化程式碼呢?列表推導能夠給我們答案:

    num = [1, 4, -5, 10, -7, 2, 3, -1]
    filtered_and_squared = [ x**2 for x in num if x > 0]
    print filtered_and_squared
     
    # [1, 16, 100, 4, 9]
    
  • 迭代器(iterator)遍歷輸入序列num的每個成員x
  • 斷言式判斷每個成員是否大於零
  • 如果成員大於零,則被交給輸出表達式,平方之後成為輸出列表的成員。

列表推導式被封裝在一個列表中,所以很明顯它能夠立即生成一個新列表。這裡只有一個type函式呼叫而沒有隱式呼叫lambda函式,列表推導式正是使用了一個常規的迭代器、一個表示式和一個if表示式來控制可選的引數。

另一方面,列表推導也可能會有一些負面效應,那就是整個列表必須一次性加載於記憶體之中,這對上面舉的例子而言不是問題,甚至擴大若干倍之後也都不是問題。但是總會達到極限,記憶體總會被用完。

針對上面的問題,生成器(Generator)能夠很好的解決。生成器表示式不會一次將整個列表載入到記憶體之中,而是生成一個生成器物件(Generator
objector),所以一次只加載一個列表元素。

生成器表示式同列表推導式有著幾乎相同的語法結構,區別在於生成器表示式是被圓括號包圍,而不是方括號:

    num = [1, 4, -5, 10, -7, 2, 3, -1]
    filtered_and_squared = ( x**2 for x in num if x > 0 )
    print filtered_and_squared
     
    # <generator object <genexpr> at 0x00583E18>
     
    for item in filtered_and_squared:
     print item
     
    # 1, 16, 100 4,9
    

這比列表推導效率稍微提高一些,讓我們再一次改造一下程式碼:

    num = [1, 4, -5, 10, -7, 2, 3, -1]
     
    def square_generator(optional_parameter):
     return (x ** 2 for x in num if x > optional_parameter)
     
    print square_generator(0)
    # <generator object <genexpr> at 0x004E6418>
     
    # Option I
    for k in square_generator(0):
     print k
    # 1, 16, 100, 4, 9
     
    # Option II
    g = list(square_generator(0))
    print g
    # [1, 16, 100, 4, 9]
    
    

除非特殊的原因,應該經常在程式碼中使用生成器表示式。但除非是面對非常大的列表,否則是不會看出明顯區別的。

下例使用zip()函式一次處理兩個或多個列表中的元素:

    alist = ['a1', 'a2', 'a3']
    blist = ['1', '2', '3']
     
    for a, b in zip(alist, blist):
     print a, b
     
    # a1 1
    # a2 2
    # a3 3
    

再來看一個通過兩階列表推導式遍歷目錄的例子:

    import os
    def tree(top):
     for path, names, fnames in os.walk(top):
     for fname in fnames:
      yield os.path.join(path, fname)
     
    for name in tree('C:\Users\XXX\Downloads\Test'):
     print name
    

裝飾器(Decorators)

裝飾器為我們提供了一個增加已有函式或類的功能的有效方法。聽起來是不是很像Java中的面向切面程式設計(Aspect-Oriented
Programming)概念?兩者都很簡單,並且裝飾器有著更為強大的功能。舉個例子,假定你希望在一個函式的入口和退出點做一些特別的操作(比如一些安全、追蹤以及鎖定等操作)就可以使用裝飾器。

裝飾器是一個包裝了另一個函式的特殊函式:主函式被呼叫,並且其返回值將會被傳給裝飾器,接下來裝飾器將返回一個包裝了主函式的替代函式,程式的其他部分看到的將是這個包裝函式。

    def timethis(func):
     '''
     Decorator that reports the execution time.
     '''
     pass
     
    @timethis
    def countdown(n):
     while n > 0:
     n -= 1
    

語法糖@標識了裝飾器。

好了,讓我們回到剛才的例子。我們將用裝飾器做一些更典型的操作:

    import time
    from functools import wraps
     
    def timethis(func):
     '''
     Decorator that reports the execution time.
     '''
     @wraps(func)
     def wrapper(*args, **kwargs):
     start = time.time()
     result = func(*args, **kwargs)
     end = time.time()
     print(func.__name__, end-start)
     return result
     return wrapper
     
    @timethis
    def countdown(n):
     while n > 0:
     n -= 1
     
    countdown(100000)
     
    # ('countdown', 0.006999969482421875)
    
    

當你寫下如下程式碼時:

    @timethis
    def countdown(n):
    

意味著你分開執行了以下步驟:

    def countdown(n):
    ...
    countdown = timethis(countdown)
    

裝飾器函式中的程式碼建立了一個新的函式(正如此例中的wrapper函式),它用 *args 和 **kwargs
接收任意的輸入引數,並且在此函式內呼叫原函式並且返回其結果。你可以根據自己的需要放置任何額外的程式碼(例如本例中的計時操作),新建立的包裝函式將作為結果返回並取代原函式。

    @decorator
    def function():
     print("inside function")
    

當編譯器檢視以上程式碼時,function()函式將會被編譯,並且函式返回物件將會被傳給裝飾器程式碼,裝飾器將會在做完相關操作之後用一個新的函式物件代替原函式。

裝飾器程式碼是什麼樣的?大部分的例子都是將裝飾器定義為函式,而我發覺將裝飾器定義成類更容易理解其功能,並且這樣更能發揮裝飾器機制的威力。

對裝飾器的類實現唯一要求是它必須能如函式一般使用,也就是說它必須是可呼叫的。所以,如果想這麼做這個類必須實現__call__方法。

這樣的裝飾器應該用來做些什麼?它可以做任何事,但通常它用在當你想在一些特殊的地方使用原函式時,但這不是必須的,例如:

    class decorator(object):
     
     def __init__(self, f):
     print("inside decorator.__init__()")
     f() # Prove that function definition has completed
     
     def __call__(self):
     print("inside decorator.__call__()")
     
    @decorator
    def function():
     print("inside function()")
     
    print("Finished decorating function()")
     
    function()
     
    # inside decorator.__init__()
    # inside function()
    # Finished decorating function()
    # inside decorator.__call__()
    
    

譯者注:

  1. 語法糖@decorator相當於function=decorator(function),在此呼叫decorator的__init__列印“inside decorator.init()”
  2. 隨後執行f()列印“inside function()”
  3. 隨後執行“print(“Finished decorating function()”)”
  4. 最後在呼叫function函式時,由於使用裝飾器包裝,因此執行decorator的__call__列印 “inside decorator.call()”。

一個更實際的例子:

    def decorator(func):
     def modify(*args, **kwargs):
     variable = kwargs.pop('variable', None)
     print variable
     x,y=func(*args, **kwargs)
     return x,y
     return modify
     
    @decorator
    def func(a,b):
     print a**2,b**2
     return a**2,b**2
     
    func(a=4, b=5, variable="hi")
    func(a=4, b=5)
     
    # hi
    # 16 25
    # None
    # 16 25
    

上下文管理庫(ContextLib)

contextlib模組包含了與上下文管理器和with宣告相關的工具。通常如果你想寫一個上下文管理器,則你需要定義一個類包含__enter__方法以及__exit__方法,例如:

    import time
    class demo:
     def __init__(self, label):
     self.label = label
     
     def __enter__(self):
     self.start = time.time()
     
     def __exit__(self, exc_ty, exc_val, exc_tb):
     end = time.time()
     print('{}: {}'.format(self.label, end - self.start))
    

完整的例子在此:

    import time
     
    class demo:
     def __init__(self, label):
     self.label = label
     
     def __enter__(self):
     self.start = time.time()
     
     def __exit__(self, exc_ty, exc_val, exc_tb):
     end = time.time()
     print('{}: {}'.format(self.label, end - self.start))
     
    with demo('counting'):
     n = 10000000
     while n > 0:
     n -= 1
     
    # counting: 1.36000013351
    
    

上下文管理器被with宣告所啟用,這個API涉及到兩個方法。

  1. __enter__方法,當執行流進入with程式碼塊時,__enter__方法將執行。並且它將返回一個可供上下文使用的物件。
  2. 當執行流離開with程式碼塊時,__exit__方法被呼叫,它將清理被使用的資源。

利用@contextmanager裝飾器改寫上面那個例子:

    from contextlib import contextmanager
    import time
     
    @contextmanager
    def demo(label):
     start = time.time()
     try:
     yield
     finally:
     end = time.time()
     print('{}: {}'.format(label, end - start))
     
    with demo('counting'):
     n = 10000000
     while n > 0:
     n -= 1
     
    # counting: 1.32399988174
    
    

看上面這個例子,函式中yield之前的所有程式碼都類似於上下文管理器中__enter__方法的內容。而yield之後的所有程式碼都如__exit__方法的內容。如果執行過程中發生了異常,則會在yield語句觸發。
描述器(Descriptors)

描述器決定了物件屬性是如何被訪問的。描述器的作用是定製當你想引用一個屬性時所發生的操作。

構建描述器的方法是至少定義以下三個方法中的一個。需要注意,下文中的instance是包含被訪問屬性的物件例項,而owner則是被描述器修辭的類。