後悔沒早知道這些Python特性

CSDN 程式人生發表於2019-11-01

640?wx_fmt=gif

寫 Python 也好幾年時間了。講道理,在工作中大家肯定遇到過這樣的場景:

640?wx_fmt=jpeg

這個故事告訴我們什麼?先造輪子再去 GitHub?還是提高下 GitHub 搜尋技巧?

都不是!

實際上,在日常的工作中,我們很多需求,無論是常見的、還是不常見的,Python 都為我們提供了一些獨特的解決方案,既不需要自己造輪子,也不需要引入新的依賴(引入新的依賴勢必會增加專案的複雜度)。

但是 Python 有太多功能和特性被我們忽略了,導致我們在遇到問題的時候,沒法第一時間作出良好的決策。

所以,乾脆來一起掃清這些被我們忽略的 Python 死角。

裝飾器的妙用

我們經常會想完成一些註冊&呼叫的功能,比如我們有四個函式:

現在我們想將這四個函式和 +、-、*、/ 四個操作符繫結,那麼我們該怎麼做?

可能我們第一反應是這樣:

operator_map = {}	
def add(a: int, b: int) -> float:	
    return a + b	
def sub(a: int, b: int) -> float:	
    return a - b	
def mul(a: int, b: int) -> float:	
    return a * b	
def div(a: int, b: int) -> float:	
    return a / b	
operator_map["+"] = add	
operator_map["-"] = sub	
operator_map["*"] = mul	
operator_map["/"] = div

但這樣寫起來,有一個很大的問題就是太不美觀了。因為直接對於 dict 的操作從實際上來講可維護性是很差的,那麼我們這個地方應該怎麼做?

在改進這段程式碼之前,我們首先要明確 Python 中一個很重要的概念,即:函式/方法是:First Class Member 。用不精確的話來講,就是函式/方法可以作為引數被傳遞、被使用。

舉個例子:

import typing	
def execute(func: typing.Callable, *args, **kwargs) -> typing.Any:	
    return func(*args, **kwargs)	
def print_func(data: int) -> None:	
    print(data)	
execute(print, 2)

大家可以看到我們將 print_func 這個函式作為引數傳遞給 execute 函式並被呼叫。

那麼我們來改造下之前的程式碼:

好了,大家看看,目前整體程式碼的可讀性以及可維護性是不是改了很多?

但是我們現在的問題在於,每次都需要在單獨呼叫一次 register_operator 函式,這樣也太煩了吧!要不要再改進一下?要得。我們可以用裝飾器來改進一下。

首先,看一個最簡單的裝飾器例子:

import functools	
import typing	
import time	
def execute(func: typing.Callable) -> typing.Callable:	
    @functools.wraps(func)	
    def wraps(*args, **kwargs) -> typing.Any:	
        start_time = time.time()	
        result = func(*args, **kwargs)	
        print("{}".format(time.time() - start_time))	
        return result	
    return wraps	
@execute	
def add(a: int, b: int) -> float:	
    return a + b

我們能看到這段函式的意義是計算函式的執行時間。那麼這個原理是什麼?

實際上裝飾器是一個語法糖,具體可以參見 PEP318 Decorators for Functions and Methods。

簡而言之,實際上是 Python 替我們做了一個替換過程。以上面的例子為例,這個替換過程就是 add=execute(add) 。

好了,我們就用這個知識點來改進下之前的程式碼:

import typing	
operator_map = {}	
def register_operator(operator: str) -> typing.Callable:	
    def wraps(func: typing.Callable) -> typing.Callable:	
        operator_map[operator] = func	
        return func	
    return wraps	
@register_operator("+")	
def add(a: int, b: int) -> float:	
    return a + b	
@register_operator("-")	
def sub(a: int, b: int) -> float:	
    return a - b	
@register_operator("*")	
def mul(a: int, b: int) -> float:	
    return a * b	
@register_operator("/")	
def div(a: int, b: int) -> float:	
    return a / b
這樣我們這段程式碼的註冊過程是不是就顯得更優雅了?
嗯,是的!實際上 Python 中有很多特性會幫助我們的程式碼更簡潔,更優美。
接下來這個例子很可能幫我們減輕工作量。

聊聊 OrderedDict

dict 是我們經常使用的一種資料解構。但是在 Python 3.6 之前 dict 都是無序的,即我插入的順序,和資料在 dict 中存放的順序並無關聯(筆者注:Python 3.6 dict 有序只是新版實現的順帶產物,Python 3.7 正式作為 feature 被固定下來)。
但是很多時候,比如在驗籤等場景,我們需要保證 dict 資料存放順序,和我們插入順序是一致的。那麼我們該怎麼辦?
老闆有需求下來了,我們肯定不能告訴老闆這個需求沒法做。那我們就自己實現一個ordereddict 吧。於是,想了想,寫了如下的程式碼:
import typing	
class OrderedDict:	
    def __init__(self, *args, **kwargs):	
        self._data = {}	
        self._ordered_key = []	
    def __getitem__(self, key: typing.Any) -> typing.Any:	
        return self._data[key]	
    def __setitem__(self, key: typing.Any, value: typing.Any) -> None:	
        if key not in self._data:	
            return	
        self._data[key] = value	
        self._ordered_key.append(key)	
    def __delitem__(self, key: typing.Any):	
        del self._data[key]	
        self._ordered_key.remove(key)

通過額外維護一個 list 來維護 key 插入的順序。這段程式碼,看似完成了我們的需求,但是實則存在很大問題。大家可以猜猜問題在哪?

3,2,1!

揭曉答案,這段程式碼利用 list 來保證 key 的有序性,在刪除的時候, list 的刪除操作,是一個時間複雜度 O(n) 的操作。換句話說,我們的刪除操作隨著內部資料的增多,所需的刪除時間也變得越長。這對於某些效能敏感的場景是無法接受的。

那要怎麼辦呢?事實上,Python 在很早之前就已經內建了有序字典,即很多人可能都用過的 collections.OrderedDict 。

 OrderedDict 中, Python 維護了一個雙向連結串列解構,來保證插入的有序性,如下圖所示:

640?wx_fmt=png

在最左側維護一個衛兵節點,衛兵節點的 next 指標恆指向於資料中最後插入的節點。那麼插入新的資料時,我們將新的資料插入到衛兵節點之後,從而達成維護插入順序的目的。

在刪除的時候,通過額外維護的一個字典找到待刪除的 key 所對應的節點。這個操作是 O(1) 的複雜度,然後大家都知道,雙向連結串列刪除一個節點的時間複雜度也是 O(1) 。通過這樣保證我們在即便有大量資料的情況下,也能保證相應的效能。
好了,我們按照這個思路來做一個最簡單的實現:
import typing	
class Node:	
    def __init__(self, key: typing.Any, value: typing.Any) -> None:	
        self.key = key	
        self.value = value	
        self.prev = None	
        self.next = None	
class OrderedDict:	
    def __init__(self, *args, **kwargs):	
        self._data = {}	
        self._head = Node(None, None)	
        self._last = self._head	
    def __getitem__(self, key: typing.Any) -> typing.Any:	
        if key in self._data:	
            return self._data[key].value	
        raise ValueError	
    def __setitem__(self, key: typing.Any, value: typing.Any) -> None:	
        if key not in self._data:	
            return	
        value_node = Node(key, value)	
        next_node = self._head.next	
        if not next_node:	
            self._head.next = value_node	
            value_node.prev = self._head	
            self._last = value_node	
        else:	
            value_node.next = next_node	
            next_node.prev = value_node	
            value_node.prev = self._head	
            self._head.next = value_node	
        self._data[key] = value_node	
    def __delitem__(self, key: typing.Any):	
        if key not in self._data:	
            return	
        value_node = self._data[key]	
        if value_node == self._last:	
            self._last = value_node.prev	
            self._last.next = None	
        else:	
            prev_node = value_node.prev	
            next_node = value_node.next	
            prev_node.next = next_node	
            next_node.prev = prev_node	
        del self._data[key]	
        del value_node

(此段程式碼,如有錯亂,煩請將瀏覽字型調小几號)

這只是一個 OrderedDict 的簡化版,如果想完成一個完整的 OrderedDict 還有很多很多的 corner case 要去處理。不過現在,我們可以使用內建的資料結構去完成我們需求。怎麼樣,是不是有了一種幸福的感覺?

隨意聊聊

通過今天的兩個例子,我們發現 Python 提供了相當多的功能去幫助我們完成日常的工作與學習任務。同時通過去深入地瞭解 Python 內部的一些功能實現,以便我們能更好地去學習一些知識。比如,上文提到的 OrderedDict 的實現,會讓我們學到雙頭連結串列的一種非常典型的應用,與此同時,雙頭連結串列也會用於諸如 LRU 這樣非常常用的資料解構的實現。所以,多去深入瞭解 Python 的方方面面,有助於我們整體能力的提升。

那麼既然電腦上已經有了 Python,為何不將這些 tricks 用起來,讓 Python 更 6 呢?安利這位來自德國的大咖,資深養蛇玩家 Dan Bader。

他的部落格 dbader.org  每月有超過 20 萬的瀏覽量,Real Python 視訊累計瀏覽量超過百萬。訂閱他的郵件,每天都會準時收到他更新 Python 技巧,一日不差,是一個極其自律的人。

640?wx_fmt=png
暢銷書 Python Tricks 作者,影響全球 1 000 000 以上程式設計師的 PythonistaCafe 社群創始人,Real Python 培訓機構總編,擁有近 20 年軟體開發經驗。巴德爾畢業於歐洲歷史悠久的慕尼黑工業大學,該校以優異的科教質量聞名,截至 2018 年已經培養出 17 位諾貝爾獎得主。
他把自己對 Python 的理解成書,試圖通過一些常用的小例子幫助更多開發者擁抱 Python。

640?wx_fmt=jpeg

精進Python不二之選

《深入理解Python特性》
Dan Bader 著
孫波翔 譯

640?wx_fmt=png

(掃一掃,新舊封面隨機發貨)
上市兩個月獲 Amazon 百餘條五星評價,詳解用好 Python 需要了解的最重要特性,與《流暢的Python》互為補充,Python 進階必備。幫助 Python 開發人員挖掘這門語言及相關程式庫的優秀特性,避免重複勞動,同時寫出簡潔、流暢、易讀、易維護的程式碼。

640?wx_fmt=jpeg

640?wx_fmt=gif

作為碼書商店的運營人員,誠邀你們進入我們的“CSDN碼書福利群”,進入群后,你想要的書籍都有推薦,你想要的優惠也都可以實現(當然不能讓我賣掉我自己),你也可以在學習累的時候和大家吹吹牛放鬆放鬆

640?wx_fmt=png

640?wx_fmt=gif

相關文章