python函式

yepkeepmoving發表於2017-07-23

函式是 python 中組織程式碼的最小單元。函式有輸入(引數)和輸出(返回值),函式其實是一個程式碼單元,把輸入轉換成輸出。

在 python 中可以建立四種函式:

  1. 全域性函式:定義在模組中;
  2. 區域性函式:巢狀於其他函式中;
  3. lambda 函式:表示式;
  4. 方法:與特定資料型別關聯的函式,並且只能與資料型別關聯一起使用。

將程式中重複的地方抽取出來,定義成一個函式。使用函式的好處:

  • 程式可擴充套件性;
  • 減少程式程式碼;
  • 方便程式架構的更改。

定義語法:

def 函式名(引數1, 引數2):
    print(引數1 + 引數2)
    ...
    return 引數1 + 引數2 # 定義函式的返回值
複製程式碼

定義函式的時候,並不會執行函式體。只有當我們呼叫的時候,它才會執行。

函式的呼叫:

函式名(引數1, 引數2)
複製程式碼

傳入的引數必須和函式定義時的引數相匹配,如果不匹配,會丟擲 TypeError。引數按照定義的順序傳入,這樣的傳參方法叫做位置引數。

函式的引數

函式的引數分為形式引數和實際引數。

  • 在定義函式時,函式後面括號中的變數名稱叫做“形式引數”,或者稱為“形參”;
  • 在呼叫函式時,函式後面括號中的變數名稱叫做“實際引數”,或者稱為“實參”。
In [1]: def fun(x,y):
   ...:     print x + y
   ...:

In [2]: fun(3,4)
7

In [3]: fun('ab','c')
abc
複製程式碼

判斷傳遞給指令碼的引數是數字:

#!/usr/bin/env python
import sys
def isNum(s):
    for i in s:
        if i in '0123456789':
            print '%s is a number' %s
            sys.exit()
    print '%s is not a number' %s

isNum(sys.argv[1])
複製程式碼

位置引數傳參

引數按照定義的順序傳入,這樣的傳參方法叫做位置引數。這是最簡單、也是最容易理解的傳參方式了。

>>> def add(x, y):
...     return x + y
...
>>> add(2, 3) # 2 對應 x,3 對應 y
Out[10]: 5
複製程式碼

預設引數

函式中的引數可以不給其賦值,它會有預設值,比如有的網站選擇國籍不管選不選都會有一個預設值。

>>> def inc(base, x=1):
...     return base + x
...
>>> inc(3) # 可以傳遞一個引數
Out[12]: 4
>>> inc(3, 3) # 可以給其傳遞引數進行覆蓋
Out[13]: 6
複製程式碼

但是預設引數必須出現在不帶預設值引數之後,不然當只傳遞一個引數的時候,它不知道給誰。

>>> def inc(x=1, base):
...     return base + x
...
  File "<ipython-input-14-ac010ba50fd9>", line 1
    def inc(x=1, base):
           ^
SyntaxError: non-default argument follows default argument
複製程式碼

可變引數

有這麼一種情況:

>>> def sum(lst):
...     ret = 0
...     for i in lst:
...         ret += i
...     return ret
...
>>> sum([1, 2, 3]) # 只能傳遞一個引數
Out[16]: 6
複製程式碼

這個函式只能傳遞一個引數給它,但是如果想要想要將列表中的元素通過引數的形式傳遞給它時,可以這麼定義引數:

>>> def sum(*lst): # 加個星號即可
...     ret = 0
...     for i in lst:
...         ret += i
...     return ret
...
>>> sum(1, 2, 3, 4) # 可以傳遞任意個引數
Out[19]: 10
複製程式碼

可變引數分兩種:

  • 位置可變引數:引數前加一個星號,表示這個引數是可變的,也就是可以接受任意多個引數。這些引數構成一個元組,引數只能通過位置引數傳參。
  • 關鍵字可變引數:引數前加兩個星號,表示這個引數是可變的,也就是可以接受任意多個引數。這些引數構成一個字典,引數只能通過關鍵字引數傳參。
>>> def connect(**kwargs):
...     print(type(kwargs))
...     for k, v in kwargs.items():
...         print('{} -> {}'.format(k, v))
...
>>> connect(host='10.0.0.1', port=3306)
<class 'dict'>
host -> 10.0.0.1
port -> 3306
複製程式碼

注意事項:

  • 這兩種可變引數可以一起使用,但是字典必須在後。
  • 普通引數可以和可變引數一起使用,但是傳參的時候必須匹配。
  • 位置可變引數可以在普通引數之前,但是在它之後的普通引數只能通過關鍵字引數傳遞。
  • 關鍵字可變引數不允許在普通引數之前。

為了防止出錯,或者呼叫方疑惑,我們應該遵循以下規則:

  1. 預設引數靠後;
  2. 可變引數靠後;
  3. 預設引數和可變引數不同時出現。

當我們連線資料庫時,可以這麼處理:

# 第一種方式
def connect(host='127.0.0.1', port='3306', user='root', password='', db='test', **kwargs):
    pass

# 第二種方式
def connect(**kwargs):
    host = kwargs.pop('host', '127.0.0.1')
複製程式碼

引數解構

呼叫函式時,使用 * 開頭的引數,可用於將引數集合打散,從而傳遞任意多基於位置或關鍵字的引數。在這之前,我們回顧下變數解包:

>>> l1 = ['Sun', 'Mon', 'Tus']
>>> x, y, z = l1
>>> x, y, z
('Sun', 'Mon', 'Tus')
複製程式碼

引數解包也就是這種結果:

>>> def f1(a, b, c):
...   print a, b, c
...
>>> f1(*['Sun', 'Mon', 'Tus'])
Sun Mon Tus
複製程式碼

引數的個數和列表中元素的個數要一一匹配,多了少了都不行。

>>> def sum(*args):
...     ret = 0
...     for i in args:
...         ret += i
...     return ret
...
>>> sum(*range(10))
Out[34]: 45
複製程式碼

引數的解構有兩種形式:

  • 一個星號:解構的物件是可迭代物件,解構的結果是位置引數;
  • 兩個星號:解構的物件是字典,結構的結果是關鍵字引數。

要解構的字典的 key 必須是字串。

關鍵字傳參

這個是針對實參的,也就是傳遞給函式時使用的,函式本身的定義並沒有特殊的地方。這樣就避免了預設引數不能定義順序的問題了,可以說這個可以配合預設引數使用。

先定義一個函式,然後使用關鍵字引數傳遞:

>>> def add(x, y):
...     return x + y
...
 >>> add(2, y=3)
Out[21]: 5
複製程式碼

當位置引數和關鍵字引數混合使用時,位置引數必須在前面,不然會報錯。

>>> def add(x, y):
...     return x + y
...
>>> add(x=2, 3)
  File "<ipython-input-9-92c728388ce1>", line 1
    add(x=2, 3)
            ^
SyntaxError: positional argument follows keyword argument
複製程式碼

預設引數和關鍵字傳參結合起來非常好用,它能讓函式的呼叫非常簡潔。

def connect(host='127.0.0.1', port='3306', user='root', password='', db='test'):
    pass

connect('10.0.0.5', password='123456')
複製程式碼

可變引數允許你傳入 0 個或任意個引數,這些可變引數在函式呼叫時自動封裝為一個 tuple。而關鍵字引數允許你傳入 0 個或任意個含引數名的引數,這些關鍵字引數在函式內部自動組裝為一個 dict。請看示例:

>>> def person(name, age, **kw):
...     print('name:', name, 'age:', age, 'other:', kw)
複製程式碼

函式 person 除了必選引數 name 和 age 外,還接受關鍵字引數 kw。在呼叫該函式時,可以只傳入必選引數:

>>> person('Michael', 30)
name: Michael age: 30 other: {}
複製程式碼

也可以傳入任意個數的關鍵字引數:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
複製程式碼

關鍵字引數有什麼用?它可以擴充套件函式的功能。比如,在 person 函式裡,我們保證能接收到 name 和 age 這兩個引數,但是如果呼叫者願意提供更多的引數,我們也能收到。

試想你正在做一個使用者註冊的功能,除了使用者名稱和年齡是必填項外,其他都是可選項,利用關鍵字引數來定義這個函式就能滿足註冊的需求。

和可變引數類似,也可以先組裝出一個 dict,然後把該 dict 轉換為關鍵字引數傳進去:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
複製程式碼

當然,上面複雜的呼叫可以用簡化的寫法:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
複製程式碼

**extra 表示把 extra 這個 dict 的所有 key-value 用關鍵字引數傳入到函式的 **kw 引數,kw 將獲得一個 dict,注意 kw 獲得的 dict 是 extra 的一份拷貝,對 kw 的改動不會影響到函式外的 extra。

keyword-only引數

這個是 python3 新加入的。

  • 星號之後的引數只能通過關鍵字引數傳遞,叫做 keyword-only 引數。
  • 星號本身不接收任何值。
  • 可變位置引數之後的引數也是 keyword-only 引數。
  • keyword-only 引數可以有預設值;
  • keyword-only 引數可以和預設引數一起出現,不管它有沒有預設值,不管預設引數是不是 keyword-only 引數。
>>> def fn(*, x): # 星號後面的引數都是 keyword-only 引數,不管星號後面的引數有多少個
...     print(x)
...
>>> fn(1) # 不能使用位置引數傳遞
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-cb5d79cf2c77> in <module>()
----> 1 fn(1)

TypeError: fn() takes 0 positional arguments but 1 was given

>>> fn(x=3) # 只能通過關鍵字引數傳遞
3
複製程式碼

通常的用法,keyword-only 引數都有預設值。python 的標準庫中大量的使用了 keyword-only 引數。

return

函式一般都有輸入輸出,return 就是定義函式的輸出,也就是返回值的。

def fun():
    print('hello world')

fun()
複製程式碼

執行這個指令碼會輸出 hello world,如何檢視該函式的返回值呢?只要加個 print 即可:

>>> def fun():
...     print('hehe')
...
>>> print(fun())
hehe
None
複製程式碼

可以看到返回值為 None,函式的返回值如果沒有定義,預設為 None。此時就可以通過 return 定義返回值了:

>>> def fun(x, y):
...     return x + y
...     print('hehe')
...
>>> fun(3, 5)
Out[57]: 8
複製程式碼

從上面可以看到一旦遇到 return 函式就終止執行了,後面的程式碼不會再執行。

一個函式中可以有多個 return,執行到哪個 return,就由哪個 return 返回結果並結束函式。

>>> def guess(x):
...     if x > 3:
...         return '> 3'
...     else:
...         return '<= 3'
...
>>> guess(4)
Out[59]: '> 3'
複製程式碼

函式的返回值可以任意定義。事實上函式中一般都是使用 return,很少使用 print。使用 return 的目的在於能夠用到它返回的值,然後使用這個值做些事情。

作用域和全域性變數

作用域是一個變數的可見範圍。在接觸函式之前,是沒有作用域這個概念的,因為所有的程式碼都在同一個作用域中。由於函式是組織程式碼的最小單位,因此從函式開始就有了作用域的概念。

python 建立、改變或查詢變數名都是在名稱空間中進行,在程式碼中變數名被賦值的位置決定了其能被訪問到的範圍。函式定義了區域性作用域,而模組定義了全域性作用域。每個模組都是一個全域性作用域,因此,全域性作用域的範圍僅限於單個程式檔案。

每次對函式的呼叫都會建立一個新的本地作用域,賦值的變數除非宣告為全域性變數,否則均為變數。

所有的變數名都可以歸納為區域性、全域性或內建的(由 __builtin__ 模組提供)。

變數名引用分為三個作用域進行,首先是區域性,接著是全域性,最後是內建。也就是說引用這個變數先在本地函式(Local function)中查詢,沒找到就會在它的外層函式(Enclosing function locals),還沒有找到就要找模組中的全域性變數(Global module),還沒有的話找內建變數(Built-in),如果還是找不到的話就報錯了。

區域性變數的作用域僅限於一個函式中,區域性變數只存在於函式中,也就是說在函式中定義的變數就是區域性變數;而全域性變數的作用域為整個程式。區域性變數的優先順序高於全域性變數,當區域性變數和全域性變數的變數名相同時,它們之間並不會相互覆蓋。

>>> x = 1 # 定義在全域性作用域中
>>> def inc():
...     x += 1
...
>>> inc() # 直接報錯
Traceback (most recent call last):
  File "/usr/local/python/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-62-ae671e6b904f>", line 1, in <module>
    inc()
  File "<ipython-input-61-661b9217054c>", line 2, in inc
    x += 1
UnboundLocalError: local variable 'x' referenced before assignment
複製程式碼

每個程式都有一個全域性作用域,頂行寫的程式碼的作用域就是全域性的。有全域性自然也有區域性,並且區域性作用域會隨著它層次的加深,會出現多層的區域性作用域。

>>> def outer(): # 上級作用域對下級只讀可見
...     x = 1
...     print(x)
...     def inner():
...         x = 2 # 賦值即定義,在下級作用域中重新定義了 x
...     inner()
...     print(x)
...
>>> outer()
1
1
複製程式碼

作用域的特點:

  • 變數的作用域在於定義它的位置。如果定義在全域性,那麼任何地方都是可見的;
  • 上級作用域對下級只讀可見。

作用域的規則是可以打破的。

>>> x = 5
>>> def fun():
...     global x # 通過 global 提升變數作用域為全域性變數
...     x += 1
...     return x
...
>>> fun()
Out[69]: 6
複製程式碼

global 只是將一個變數標記為全域性變數,但是並不會定義,因此還需手動定義。

除非你清楚的知道 global 會帶來什麼,並且明確的知道非 global 不行,否則不要使用 global。

如果不想定義全域性變數,但是又要在其他函式中使用某一個函式中的區域性變數應該怎麼辦呢?方法很簡單,只要在某個函式中使用 return 返回需要呼叫函式的值,然後在函式外再使用變數接收即可。

>>> def fun():
...     x = {'a': 1, 'b': 2}
...     return x
...
>>> x = fun()
>>> x
Out[72]: {'a': 1, 'b': 2}
複製程式碼

閉包

Python 中的最小作用域是函式,也就是說函式內的變數從外部無法訪問到,但是子函式卻可以訪問到父函式的變數,這就是閉包。

global 的缺陷是:

>>> def outer():
...     y = 1
...     def inner():
...         global y
...         y += 1
...         return y
...     return inner
...
>>> y = 1
>>> f()
Out[83]: 2
>>> y = 100 # 某個位置定義了 y
>>> f() # 直接就影響了這個函式
Out[85]: 101
複製程式碼

inner 是無法修改 outer 中的 y 的,因為 inner 中對 y 重新進行了賦值。反過來說,如果我們不對 y 重新賦值,而是修改它自身,是不是就可以繞過這個規則呢?我們知道列表是可變的,因此可以拿列表進行測試。

>>> def outer():
...     y = [0]
...     def inner():
...         y[0] += 1
...         return y[0]
...     return inner
...
>>> f = outer()
>>> f()
Out[89]: 1
>>> f()
Out[90]: 2
>>> y = 100 # 定義全域性變數
>>> f()
Out[91]: 3 # 完全不受影響
複製程式碼

閉包的概念是:函式已經結束,但是函式內部的部分變數的引用還存在。在上面的例子中,當 outer 函式中執行 return inner 時,這個函式就已經結束了,按理來講,它裡面的變數都會被銷燬。但是我們通過其內部的函式 inner 卻還是可以訪問到裡面的變數 y。

python 中的閉包可以通過可變的容器(比如列表)實現,這也是 python2 唯一的方式。而在 python3 中還可以使用 nonlocal 關鍵字。

>>> def outer():
...     y = 1
...     def inner():
...         nonlocal y
...         y += 1
...         return y
...     return inner
...
>>> f = outer()
>>> f()
Out[97]: 2
>>> f()
Out[98]: 3
>>> f()
Out[99]: 4
複製程式碼

nonlocal 關鍵字用於標記一個變數由它的上級作用域定義,通過 nonlocal 標記的變數,可讀可寫。而如果上級作用域沒有定義此變數的話,會丟擲語法錯誤。

預設引數作用域

python 中一切皆物件,函式也是物件,引數是函式物件的屬性,所以函式引數的作用域伴隨函式整個生命週期。也就是說只要函式一直存在,那麼其內部的引數對其都是一直可見的。

對於定義在全域性作用域裡面的函式,銷燬的時機為:

  • 重新定義
  • del 刪除
  • 程式結束退出

區域性作用域為:

  • 重新定義
  • del
  • 上級作用域被銷燬

看下面的程式碼:

>>> def fn(x=[]):
...     x.append(1)
...     return x
...
>>> fn()
Out[101]: [1]
>>> fn()
Out[102]: [1, 1]
>>> fn()
Out[103]: [1, 1, 1]
複製程式碼

每執行一次都會往這個列表中新增一個元素。

函式中的預設引數儲存在其內部的 __default__ 方法中:

>>> fn.__defaults__
Out[104]: ([1, 1, 1],)
>>> fn()
Out[105]: [1, 1, 1, 1]
>>> fn.__defaults__
Out[106]: ([1, 1, 1, 1],)
複製程式碼

在上面的例子中,預設引數是可變物件列表,那麼換成非可變物件會出現這種情況嗎?

>>> def fn(x=1, y=1):
...     x += 1
...     y += 1
...
>>> fn.__defaults__
Out[112]: (1, 1)
>>> fn()
>>> fn.__defaults__
Out[114]: (1, 1)
複製程式碼

可以看出可變物件和非可變物件的結果是不同的。為什麼會出現這種情況呢?還是那就話,賦值即定義。列表直接修改自身,並沒有賦值操作。

因此,當可變型別作用函式預設引數時,需要特別注意。

為了避免這種情況的發生,我們可以:

  • 不使用可變型別作為函式的預設引數;
  • 在函式中不對其做修改。

不使用列表作為引數可以這麼幹:

>>> def fn(lst=None):
...     if lst is None:
...         lst = []
...     # else: # 這種方式更靈活,想修改就修改,想不修改就不修改
...     #     lst = lst[:]
...     lst.append(1) # 如果傳入的引數不是 None,還是會對傳入的引數進行修改,因此可以加上上面的 else
...     return lst
...
>>> fn()
Out[123]: [1]
>>> fn()
Out[124]: [1]
>>> fn.__defaults__
Out[125]: (None,)
複製程式碼

函式內不修改可以這麼幹:

>>> def fn(lst=[]):
...     lst = lst[:] # 注意這是影子拷貝
...     lst.append(1) # 無論如何都不會對傳入引數做修改
...     return lst
...
>>> fn()
Out[128]: [1]
>>> fn()
Out[129]: [1]
>>> fn.__defaults__
Out[130]: ([],)
複製程式碼

通常如果使用一個可變型別做為函式的預設引數,會使用 None 來代替。

函式執行流程

程式會在 CPU 上執行,並且從上到下。剛開始時程式的主流程首先在 CPU 上執行,遇到函式之後,主流程會從 CPU 上下來,讓函式上去。此時主流程的狀態,包括變數、執行到哪段程式碼等這樣的資訊會儲存在棧中。當函式執行完之後,這個函式會被銷燬,然後主流程又被排程到 CPU 之上,並且從棧中恢復它的狀態。

當呼叫函式時,直譯器會把當前現場壓棧,然後執行被調函式執行完成,直譯器彈出當前棧頂,恢復現場。

定義函式幫助文件

寫在函式名下面的第一行的字串就是幫助資訊,通過 help 這個函式可以獲得。

def fn():
    '''this is fn'''

>>> help(fn)
Help on function fn in module __main__:

fn()
    this is fn
(END)
複製程式碼

還可以通過函式本身的 __doc__ 方法:

>>> fn.__doc__
Out[3]: 'this is fn'
複製程式碼

匿名函式

lambda 函式也叫做匿名函式,也就是沒有名字的函式。它是一種快速定義單行的最小函式,可以用在任何需要函式的地方。它與 def 不同的是,def 定義的是語句,而 lambda 卻是表示式,它可以出現在任意表示式可以出現的地方。

語法為:

lambda [args]: expression
複製程式碼
  • args:以逗號分隔的引數列表,引數可省略;
  • expression:定義返回值。

最簡單的使用方法:

>>> lambda x, y: x + y
Out[135]: <function __main__.<lambda>>
複製程式碼

呼叫它:

>>> (lambda x, y: x + y)(3, 4)
Out[134]: 7
複製程式碼

第一對括號用來改變優先順序,第二對括號表示函式呼叫。由於我們不確定函式的定義和執行的哪個優先順序更高,因此使用第一個括號來提升它的優先順序。

常規的呼叫方式:

>>> f = lambda x, y: x + y
>>> f(3, 4)
Out[137]: 7
複製程式碼

匿名函式的特點:

  • 使用 lambda 定義;
  • 引數列表不需要小括號;
  • 冒號不是用來開啟新的語句塊;
  • 普通函式支援的引數的變化,匿名函式都支援;
  • 沒有 return,最後一個表示式的值即返回值。

lambda 語句定義的程式碼必須是合法的表示式,只能寫在一行。不能出現多條件語句(可使用 if 的三元表示式)和其他非表示式語句,如 for 和 while 等。lambda 的首要用途是指定短小的回撥函式,它將返回一個函式而不是將函式賦值給某變數名。

lambda 也支援預設引數:

>>> (lambda x, y=4: x + y)(3)
Out[138]: 7
複製程式碼

可變引數也支援:

>>> (lambda *args, **kwargs: print(args, kwargs))(*range(3), **{str(x): x for x in range(3)})
(0, 1, 2) {'0': 0, '1': 1, '2': 2}
複製程式碼

keywork-only 同樣支援:

>>> (lambda *, x: x)(x=5)
Out[141]: 5
複製程式碼

可以看到普通函式能夠支援的各種引數,匿名函式都支援。

匿名函式多用於高階函式。

>>> User = namedtuple('Uers', ['name', 'age'])
>>> users = [User('tom', 18), User('jerry', 15), User('sam', 44)]
>>> sorted(users, key=lambda x: x.age) # 按年齡排序
Out[165]:
[Uers(name='jerry', age=15),
 Uers(name='tom', age=18),
 Uers(name='sam', age=44)]
複製程式碼

lambda 可以起到函式速寫的作用,一些簡單函式,又不想定義一個函式時,可以使用 lambda。

>>> reduce(lambda x,y:x+y,range(1,101))
5050
複製程式碼

求階乘:

>>> reduce(lambda x,y:x*y,range(1,6))
120
複製程式碼

lambda 因為是表示式,因為可用於列表中:

>>> l1 = [(lambda x:x*2),(lambda y:y*3)]

# 通過for迴圈為其賦值:
>>> for i in l1: # 它會把這個lambda函式,也就是每一個元素賦值給i
...   print i(3)
...
6
9
複製程式碼

lambda 可以用在 map 函式中:

>>> a=range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> map(lambda x:x**2,a) # 把a傳遞給x
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
複製程式碼

來個複雜點的:

>>> b=range(10)
>>> map(lambda x,y:x**y,a,b)
[1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489] # 最後的結果為9的9次方
複製程式碼

高階函式

返回函式或者引數是函式的函式就是高階函式。

高階函式有什麼用呢?比如定義一個排序函式:

def sort(it, r=False):
    ret = []
    def cmp(a, b):
        if r:
            return a < b
        else:
            return a > b

    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
複製程式碼

這個排序函式用到了一個內部的函式 cmp,用來控制是順序排還是逆序排。但是我們可以將 cmp 函式拿出來,由使用者定義。

def cmp(a, b):
    return a > b # 只要改成小於就是逆序

def sort(it, cmp):
    ret = []
    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
複製程式碼

還可以繼續完善:

def sort(it, cmp=lambda a, b: a<b): # 給函式定義一個預設值
    ret = []
    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
複製程式碼

以上是函式作為引數的場景,它同樣用於大多數邏輯固定,少部分邏輯不固定的場景。而函式作為返回值通常用於閉包的場景,需要封裝一些變數。由於類可以封裝,因此使用函式作為返回值的場景並不多。

函式作為引數,返回值也是函式:同樣用於作為引數函式執行前後需要一些額外操作,最經典的應用就是裝飾器。

比如:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

def add(x, y):
    return x + y

>>> f = logger(add)
>>> f.__name__
Out[25]: 'wrap'
>>> f(3, 5)
call add took 0:00:00.000041
Out[26]: 8
複製程式碼

logger 函式就是用來對執行 add 這個函式之後和之後做一些操作。執行 add 之前記錄當前時間,執行之後記錄當前時間,然後可以計算執行 add 花了多少時間。

上面的兩個函式可以寫成這樣:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

@logger
def add(x, y):
    return x + y

print(add(3, 5))
複製程式碼

其實意義和上面是一樣的,這也就是裝飾器的語法。從上面我們可以得出,通過 @函式名 裝飾一個函式,就是將這個函式作為引數傳遞到裝飾器中。

裝飾器

前面已經提到了,裝飾器就是通過一個函式讓在執行另一個函式之前和之後做一些額外的操作。作為裝飾器的函式本身是一個高階函式。

引數是一個函式,返回值是一個函式的函式,就可以作為裝飾器。

裝飾器很好的體現了 AOP 的程式設計思想,它針對一類問題做處理而與具體的業務邏輯無關。常見的使用場景有:

  • 監控
  • 快取
  • 路由
  • 許可權
  • 審計

裝飾器會有一個問題,就拿前面定義的裝飾器來舉例:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

@logger
def add(x, y):
    return x + y
複製程式碼

此時執行 add.__name__,返回的並不是 add 這個函式的名稱:

>>> add.__name__
Out[8]: 'wrap'
複製程式碼

多數時候並沒有什麼影響,但是在依賴函式名的場景中肯定會出現問題。其實解決起來也很簡單。

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    wrap.__name__ = fn.__name__ # 重新賦值即可
    wrap.__doc__ = fn.__doc__ # 順便也改下 doc 屬性
    return wrap

@logger
def add(x, y):
    return x + y

>>> add.__name__ # 解決
Out[10]: 'add'
複製程式碼

還可以將賦值語句抽出來定義成一個函式:

import datetime

# 最開始是這種函式
# def copy_property(src, dst):
#     dst.__name__ = src.__name__
#     dst.__doc__ = src.__doc__

# 後來換成了這個函式
def copy_property(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    return _copy

def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    copy_property(fn)(wrap)
    return wrap

@logger
def add(x, y):
    return x + y

print(add.__name__)
複製程式碼

之所以換成下面的函式是為了柯里化,也許這樣看不明白,因為確實沒有高明到哪裡去,那麼接著往下看:

import datetime

def copy_property(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst # 加一行,_copy 就變成裝飾器了,並且是帶引數的
    return _copy

def logger(fn):
    @copy_property(fn) # 通過裝飾器修改
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

@logger
def add(x, y):
    return x + y

print(add.__name__)
複製程式碼

而 copy_property 這個函式的功能完全可以由 functools.wraps 進行代替,它的作用就是將 wrap 的一些屬性改成 fn 的,至於修改哪些屬性通過 help 可以看到:

import datetime
import functools

def logger(fn):
    @functools.wraps(fn) # 直接替換
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

@logger
def add(x, y):
    return x + y

print(add.__name__)
複製程式碼

帶引數

一個函式,返回一個不帶引數的裝飾器,那麼這個函式就是帶引數的裝飾器。因為作為裝飾器的函式只能直接一個函式作為引數,而不能接受其他引數了。因此只能在裝飾器外面再包一層函式,通過這個這個函式傳遞引數的方式將引數傳遞到裝飾器內部。

下面定義一個函式,這個函式接受一個引數,然後返回一個裝飾器。這個裝飾器用來計算函式執行的時間,超過多少秒後輸出資訊。

import time
import datetime
import functools

def logger(s):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                print('call {} took {}'.format(fn.__name__, end-start))
            return ret
        return wrap
    return _logger

@logger(2)
def sleep(x):
    time.sleep(x)

sleep(1)
複製程式碼

為了幫助理解,上面的程式碼如果不用裝飾器是這麼執行的:

import time
import datetime
import functools

def logger(s):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                print('call {} took {}'.format(fn.__name__, end-start))
            return ret
        return wrap
    return _logger

# @logger(2)
def sleep(x):
    time.sleep(x)

_logger = logger(2)
sleep = _logger(sleep)
sleep(3)
複製程式碼

甚至可以給它傳遞引數,讓其成為高階函式:

import time
import datetime
import functools

def logger(s, p=lambda name, t: print('call {} took {}'.format(name, t))):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                p(fn.__name__, end-start)
            return ret
        return wrap
    return _logger

@logger(2)
def sleep(x):
    time.sleep(x)

sleep(1)
複製程式碼

這樣一來,我們不僅可以監控函式(比如慢查詢)的執行時間,還可以傳遞一個告警的函式給它,一旦函式執行時間超過我們定義的閥值就傳送告警。

三層

帶引數的裝飾器是兩層,裝飾器外面包裝了一層,如果再包一層呢?

import time
import datetime
import functools

def logger(s):
    def _logger(p=lambda name, t: print('call {} took {}'.format(name, t))):
        def __logger(fn):
            @functools.wraps(fn)
            def wrap(*args, **kwargs):
                start = datetime.datetime.now()
                ret = fn(*args, **kwargs)
                end = datetime.datetime.now()
                if (end - start).total_seconds() > s:
                    p(fn.__name__, end-start)
                return ret
            return wrap
        return __logger
    return _logger()

@logger(2)()
def sleep(x):
    time.sleep(x)

# 直接報錯,不支援這麼幹
File "<ipython-input-23-c89f301d8b2d>", line 18
    @logger(2)()
              ^
SyntaxError: invalid syntax
複製程式碼

但是如果非要這麼搞也是可以的,如:

f = logger(2) # 先定義一個變數即可,然後通過這個變數進行裝飾
@f
def sleep(x):
    time.sleep(x)

sleep(1)
複製程式碼

多個裝飾器裝飾同一個函式

一個函式可以應用多個裝飾器,第一個裝飾器用來增強第二個裝飾器中函式的功能,而第二個裝飾器用來增強原函式的功能。

def outer0(fun):
    # 用來增強outer1中wrapper函式的功能
    def wrapper(*args, **kwargs):
        print('start')
        ret = fun(*args, **kwargs)
        return ret
    return wrapper

def outer1(fun):
    # 用來增強原函式的功能
    def wrapper(*args, **kwargs):
        print('123')
        ret = fun(*args, **kwargs)
        return ret
    return wrapper

@outer0
@outer1
def Func1(a1, a2):
    print('func1')

Func1(1, 2)
複製程式碼

想要理解兩個裝飾器的執行原理,就要將它先展開。

image_1avquo5lj1pss107v06n8krdg9.png-27.5kB

如上圖所示,執行 Func1 這個函式時,會執行 outer0 中的 wrapper 函式。執行到這個函式中的 fun 函式時,就相當於執行 outer1 中的 wrapper 函式。執行到這個函式中的 fun 函式時,就相當於執行 Func1 這個函式。Func1 這個函式執行完畢後,將返回值 None 返回給了 outer1 中的 ret,緊接著又將 ret 返回給 outer0 中的 wrapper 函式,最終將 None 返回,也就得到了最終的結果 None。

遞迴

遞迴就是函式內部自己呼叫自己,通常用它計算階乘。遞迴函式要給一個退出條件,不然就會陷入無止盡的遞迴中。遞迴需要邊界條件、遞迴前進和遞迴返回段。

為了保護直譯器,python 對遞迴的最大深度有限制,這個深度可以在 python 中看到:

>>> import sys
>>> sys.getrecursionlimit()
Out[132]: 2000
複製程式碼

通過下面的方法修改其上限:

sys.setrecursionlimit()
複製程式碼

python 遞迴很慢,特別是呼叫遞迴函式過多的時候。Python 需要為每次遞迴維護一個內部狀態,因此,要儘量避免使用遞迴。

直接定義成遞迴,也就是函式內自己呼叫自己,python 自身可以檢測出來它是遞迴,會對其深度進行限制。但是下面這樣的語句其實就是遞迴,但是 python 卻檢測不出來,遇到這樣的情況,直譯器會當場死。

def f():
    g()

def g():
    k()

def k()
    f()
複製程式碼

將它們寫在一起當然很容易發現,但是當這些函式分別在不同的檔案中時,就不好定位了。特別是當其中一個函式在一個很小的功能中,平時都不會用上,但是一旦用上就會觸發無限的重複呼叫,一執行就死。所以會出現這樣的情況:程式跑了幾個月沒事,但是突然程式就掛了,然後啟動之後好好的,過了一段時間又掛了。不光是 python,其他語言都會遇到這樣的問題,由於這種情況下,一瞬間程式就掛了,伺服器的負載什麼的都是正常狀態,因此這種問題排查起來非常麻煩。所以寫程式一定要小心,要避免這樣的情況發生。

階乘的實現:

def fact(n):
  if n <= 1: return 1
  else: return n*fact(n-1)
複製程式碼

return返回一個返回值,這個返回值包含了對自己函式本身的呼叫。通過 n-1 的方式給出遞迴的退出條件。假如 n 為 3,那麼:

fact(3)
  return: 3*fact(2)
    return: 2*fact(1)
      return: 1
複製程式碼

結果就是 321,也就是 6。它會先將所有生成的結果儲存在記憶體中,等到最終返回1時,再從下到上進行相乘。這就是遞迴,一個函式內部包含了對自己的呼叫。

返回斐波那契數列的第 10 個數:

def f1(a1, a2, n):
    if n == 10:
        return a1
    else:
        a3 = a1 + a2
        # 一定要使用一個變數接收f1這個函式的返回值,這樣才能把結果傳遞給上一個呼叫的函式,直到把值給第一個函式。
        a = f1(a2, a3, n+1)
        # 傳遞的引數變成了a2, a3了
        return a

print(f1(0, 1, 1))
複製程式碼

這個函式肯定會不斷的遞迴巢狀,當 n=10,if 條件滿足時,會返回值為 34 的 a1。這個 34 正好被前一個函式,也就是 n=9 時的函式中的 a 接收,a 也就獲得 34 這個值,然後執行 return 語句,將 34 返回給前面一個函式,也就是 n=8 時的函式中的 a。以此類推,最終回到第一個函式中的 a,它獲得了前面函式返回而來的 34,於是返回最終結果 34。

也就是說遞迴會不斷巢狀,不斷往裡一層套一層。但是當給它一個某個時間退出的條件時,它就會不斷往外一層一層的解開。因此一個要有一個引數來接收後一個函式的返回值。下面就是一個錯誤的案例,沒有接收前一個函式的返回值:

def f1(a1, a2, n):
    if n == 10:
        return a1
    else:
        a3 = a1 + a2
        # 下面並沒有接收
        f1(a2, a3, n+1)
        return a1

print(f1(0, 1, 1))
複製程式碼

雖然返回了 a1 也就是 34,但是前一個函式並沒有接收這個值(只是執行函式),因此最終結果是最初傳遞給它的 0。

型別提示

Python 是動態型別的語言。也就是說一個變數的型別,是在執行時決定的,而一個變數的型別在應用的生命週期中是可變的。

當我們一定一個函式時,別人並不知道要給它傳遞的引數應該是什麼型別,通常常用的用法是在文件中寫明。比如:

def add(x, y):
    '''
    :param x: int
    :param y: int
    :return: int
    '''
    return x + y
複製程式碼

問題在於並不是所有人都會寫文件,並且文件並不一定會隨著程式碼一起更新。還有就是文件是自然語言,不方便機器操作。因此,Python3 開始支援型別提示,也就是有那麼一個標準讓我們知道引數以及返回值型別是啥。

def add(x: int, y: int) -> int:
    return x + y
複製程式碼

需要注意的是,它只是一個註解,Python 並不會做任何的檢查。它用來提供給第三方工具(如 IDE,靜態分析工具),或者在執行時獲取資訊。

Python 將這個資訊儲存在這裡:

>>> add.__annotations__
Out[25]: {'return': int, 'x': int, 'y': int}
複製程式碼

3.5 和 之前的版本,型別提示只能用在函式的引數和返回值上。3.6 中可以這麼用:

i: int = 1
複製程式碼

為了支援型別註解,Python 還提供了 typing 庫。下面表示列表中的元素是整型。

import typing

def add(lst: typing.List[int]) -> int:
    ret = 0
    for i in lst:
        ret += i
    return ret

>>> add.__annotations__
Out[28]: {'lst': typing.List[int], 'return': int}
複製程式碼

以下是一個示例,用於強制輸入的型別和定義的型別相符合,不然丟擲異常:

import inspect
import functools

def typed(fn):
    @functools.wraps(fn)
    # 這裡要判斷關鍵字傳參和非關鍵字傳參
    def wrap(*args, **kwargs):
        # 通過 inspect 模組獲取引數指定的型別
        params = inspect.signature(fn).parameters
        # 判斷關鍵字傳參
        for k, v in kwargs.items():
            if not isinstance(v, params[k].annotation):
                raise TypeError('parameter {} required {}, but {}'.format(k, params[k], type(v)))
        # 非關鍵字傳參
        for i, arg in enumerate(args):
            param = list(params.values())[i]
            if not isinstance(arg, param.annotation):
                raise TypeError('parameter {} required {}, but {}'.format(param.name, param.annotation, type(arg)))
        return fn(*args, **kwargs)
    return wrap

@typed
# 必須要給引數指定型別,不然判斷不了
def add(x: int, y: int) -> int:
    return x + y
複製程式碼

練習

扁平化字典(遞迴法):

def flatten(d):
    def _flatten(src, dst, prefix=''):
        for k, v in src.items():
            key = k if prefix is '' else '{}.{}'.format(prefix, k)
            if isinstance(v, dict):
                _flatten(v, dst, key)
            else:
                dst[key] = v
    result = {}
    _flatten(d, result)
    return result

>>> flatten({'a': 2, 'b': {'c': 4, 'g': 9}, 'd': {'e': {'f': 6}}})
Out[30]: {'a': 2, 'b.c': 4, 'b.g': 9, 'd.e.f': 6}
複製程式碼

雖然遞迴效率低,但是當字典深度不大時,使用遞迴更方便,只不過看起來有些難懂。

實現base64編碼

base64 編碼實現了將二進位制轉換成字串,它的特點:

  • 有一個 table:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
  • 輸入按三位元組(24位)分組,不足三位元組補 0;
  • 按 6 位分組,轉化為整數
  • 整數作為 table 的索引
  • 補 0 的位元組用 = 表示
def b64encode(data: bytes) -> str:
    table = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    encoded = bytearray()
    c = 0
    for x in range(3, len(data)+1, 3):
        i = int.from_bytes(data[c: x], 'big')
        for j in range(1, 5):
            encoded.append(table[i >> (24 - j*6) & 0x3f])
        c += 3
    r = len(data) - c
    if r > 0:
        i = int.from_bytes(data[c:], 'big') << (3-r) * 8
        for j in range(1, 5-(3-r)):
            encoded.append(table[i >> (24 - j*6) & 0x3f])
        for _ in range(3-r):
            encoded.append(int.from_bytes(b'=', 'big'))
    return encoded.decode()

print(b64encode(b'abcdefg'))
複製程式碼

解碼:

def b64decode(data: str) -> bytes:
    table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    decoded = bytearray()
    s = 0
    for e in range(4, len(data)+1, 4):
        tmp = 0
        for i, c in enumerate(data[s:e]):
            if c != '=':
                tmp += table.index(c) << 24 - (i+1) * 6
            else:
                tmp += 0 << 24 - (i+1) * 6
        decoded.extend(tmp.to_bytes(3, 'big'))
        s += 4
    return  bytes(decoded.rstrip(b'\x00'))
複製程式碼

快取裝飾器

類似於 functools.lru_cache,但是能設定超時。

難點在於:

  1. 要將 *args, **kwargs 作為 key,但是列表和字典很明顯不能作為字典的 key 的,因此要對列表和字典進行解析,實現最終的 key = 'args=1&args=2&args=3&kwargs=99' 這樣類似的格式。
  2. 由於沒有換出策略,因此只要訪問時判斷如果當前的時間戳減去當時的時間戳大於超時時間就直接返回結果。
import time
import inspect
import functools
from datetime import datetime

def cache(expire=0):
    def _cache(fn):
        # 將它定義在內部針對每一個函式生效,因此函式名沒有必要儲存
        cache_dic = {}
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            lst = []
            s = set()
            # 這個會儲存所有函式的引數,包括預設引數
            param = inspect.signature(fn).parameters
            if 'args' in param.keys():
                for arg in args:
                    lst.append(('args', arg))
                    s.add('args')
            else:
                for i, arg in enumerate(args):
                    para = list(param.keys())[i]
                    name = (para, arg)
                    lst.append(name)
                    s.add(para)
            lst.extend(list(kwargs.items()))
            s.update(kwargs.keys())
            if 'kwargs' not in param.keys():
                for k, v in param.items():
                    if k not in s:
                        lst.append((k, v.default))
                        s.add(k)
            lst.sort(key=lambda x: x[0])
            # 最終的 key 就是這個樣子
            key = '&'.join(['{}={}'.format(k, v) for k, v in lst])
            now = datetime.now().timestamp()
            if key in cache_dic:
                ret, timestamp = cache_dic[key]
                if expire == 0 or now - timestamp < expire:
                    print("cache hit")
                    return ret
            ret = fn(*args, **kwargs)
            cache_dic[key] = (ret, now)
            print('cache miss')
        return wrap
    return _cache
複製程式碼

接下來實現設定 lru 換出。

注意事項:

  • 每一個 key 對應的值有三個:函式執行結果、用於比較過期的時間、用於記錄最近一次訪問的時間。
  • 如果超時時間沒到,或者沒設超時程式碼沒什麼變化。
  • 當快取條數小於最大上限之前,不做檢查。
  • 當快取條數大於等於上限時,先做超時的換出,方法為遍歷整個字典。注意,遍歷的過程中字典可能會被修改,因此最好使用 list(cache_dic.items()),這樣遍歷的就不是原字典了。
  • 如果超時換出之後快取條數依然大於等於上限,就要對字典的用於記錄最近一次訪問的時間進行排序,然後取字典的第一項進行刪除。

上面的換出演算法很慢,效率很低。我們可以通過佇列完成,所有的 key 都放在佇列中(也不用儲存最後一個時間了)。每次命中之後,就將 key 放在佇列首部,因此換出的時候只需刪除佇列尾部即可。這個佇列可以通過列表來實現,列表的 insert 到第 0 的位置,雖然對導致其後所有的元素都往後移一位,但是效能不算太差。最大的問題在於列表的 remove,因為 remove 要遍歷字典,時間複雜度為 O(n)。

因此最好的方式是寫一個雙向連結串列,插入的時候放在 head,刪除的時候刪掉 tail。為了解決 remove 時要遍歷整個連結串列的方式找到對應的項,因為將資料都存到連結串列中,並在字典中存一個對應連結串列資料的引用,通過這個引用就能儲存查詢的速度為 O(1)。

from collections import namedtuple

Item = namedtuple('Item', ['key', 'value', 'timestamp'])

def linked_list():
    _head = None
    _tail = None

    def put(item):
        nonlocal _head
        nonlocal _tail
        if _head is None:
            _head = {'data': item, 'prev': None, 'next': None}
        else:
            node = {'data': item, 'prev': None, 'next': _head}
            _head['prev'] = node
            _head = node
        if _tail is None:
            _tail = _head
        print(id(_head), id(_tail))
        return _head

    def pop():
        nonlocal _tail
        nonlocal _head
        if _tail is None:
            _head = None
            return None
        node = _tail
        _tail = node['prev']
        return node

    def remove(node):
        nonlocal _head
        nonlocal _tail
        if node is _head:
            _head = node['next']
        if node is _tail:
            pop()
            return
        node['prev']['next'] = node['next']
        node['next']['prev'] = node['prev']

    return put, pop, remove

put, pop, remove = linked_list()


import inspect
import datetime
import functools

def cache(maxsize=128, expire=0):
    def make_key(fn, args, kwargs):
        ret = []
        names = set()
        params = inspect.signature(fn).parameters
        keys = list(params.keys())
        for i, arg in enumerate(args):
            ret.append((keys[i], arg))
            names.add(keys[i])
        ret.extend(kwargs.items())
        names.update(kwargs.items())
        for k, v in params.items():
            if k not in names:
                ret.append((k, v.default))
        ret.sort(key=lambda x: x[0])
        return '&'.join(['{}={}'.format(name, arg) for name, arg in ret])

    def _cache(fn):
        data = {}
        put, pop, remove = linked_list()
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            key = make_key(fn, args, kwargs)
            now = datetime.datetime.now().timestamp()
            if key in data.keys():
                node = data[key]
                item = node['data']
                remove(node)
                if expire == 0 or now - item.timestamp >= expire:
                    data[key] = put(item)
                    return item
                else:
                    data.pop(key)
            value = fn(*args, **kwargs)
            if len(data) >= maxsize:
                if expire != 0:
                    expires = set()
                    for k, node in data.items():
                        if now - node['data'].timestamp >= expire:
                            pop(node)
                            expires.add(k)
                    for k in expires:
                        data.pop(k)
            if len(data) >= maxsize:
                node = pop()
                data.pop(node['data'].key)
            node = put(Item(key, value, now))
            data[key] = node
            return value
        return wrap
    return _cache
複製程式碼

在通過 put 方法將資料加入到連結串列中之後,頭結點就是當前插入的內容,因此將這個頭結點儲存在快取字典中,下次就可以通過這個節點能以 O(1) 的速度在連結串列中找到並刪除。

命令分發器

通過輸入對應的命令來執行相應的函式。

def command():
    commands = {}

    def register(command):
        def _register(fn):
            if command in commands:
                raise Exception('command {} exist'.format(command))
            commands[command] = fn
            return fn
        return _register

    def default_fn():
        print('unknown command')

    def run():
        while True:
            cmd = input('>> ')
            if cmd.strip() == 'quit':
                return
            commands.get(cmd.strip(), default_fn)()

    return register, run

register, run = command()

@register('abc')
def papa():
    print('papa')

run()
複製程式碼

也就是說在函式上使用裝飾器,那麼這個函式就和傳遞給裝飾器的引數建立了聯絡。那麼下次輸入對應的引數就能執行裝飾的函式。

下面是帶引數的版本。所謂的帶引數就是使用者輸入的時候是可以帶引數的:

import inspect
from collections import namedtuple


def dispatcher(default_handler=None):
    Handler = namedtuple('Handler', ['fn', 'params'])
    # 這個列表儲存了被裝飾的函式的函式名和引數列表,也就是上面的 Handler
    commands = {}

    if default_handler is None:
        default_handler = lambda *args, **kwargs: print('not found')

    def register(command):
        def _register(fn):
            # 這是由引數組成的物件的字典
            params = inspect.signature(fn).parameters
            # 將函式名和引數儲存在字典中
            commands[command] = Handler(fn, params)
            return fn
        return _register

    def run():
        while True:
            command, _, params = input('>> ').partition(':')
            # 假如使用者輸入add:x,y,z=1
            if command.strip() == 'quit':
                return
            handler = commands.get(command.strip(), Handler(default_handler, {}))
            # 接下來解析使用者輸入的引數,包括使用冒號分隔的函式名和引數列表
            args = []
            kwargs = {}
            param_values = list(handler.params.values())
            for i, param in enumerate(params.split(',')):
                if '=' in param:
                    name, _, value = param.partition('=')
                    # 獲取被裝飾函式的引數列表中引數註解或者預設引數的值
                    # 比如預設引數的 <Parameter "y='abc'">
                    p = handler.params.get(name.strip())
                    # 事實上如果是預設引數,可以將預設引數的值通過default取出來並判斷它的型別
                    if p is not None and p.annotation != inspect.Parameter.empty:
                        kwargs[name.strip()] = p.annotation(value)
                    else:
                        kwargs[name.strip()] = value
                else:
                    if len(param_values) > i and param_values[i].annotation != inspect.Parameter.empty:
                        args.append(param_values[i].annotation(param.strip()))
                    else:
                        args.append(param.strip())
            ret = handler.fn(*args, **kwargs)
            if ret is not None:
                print(ret)
    return register, run

reg, run = dispatcher()
@reg('abc')
def abc(x: int, y: int):
    print(x+y)

run()
複製程式碼

引數需要進行處理,因為如果函式使用的 int 型別,但是 input 輸入的型別卻是字串。因此為了區分引數的型別,我們需要用到型別註解,通過這個進行判斷。

當引數是列表或者字典時,這個指令碼無法勝任。

解析httpd日誌

import datetime
from collections import namedtuple

line = '66.256.46.124 - - [10/Aug/2016:06:05:06 +0800] "GET /robots.txt HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible: Googlebot/2.1: +http://www.google.com/bot.html)"'

Request = namedtuple('Request', ['method', 'url', 'version'])
MapItem = namedtuple('MapItem', ['name', 'convert'])
mapping = [
    MapItem('remote', lambda x: x),
    MapItem('', None),
    MapItem('', None),
    MapItem('time', lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')),
    MapItem('request', lambda x: Request(*x.split())),
    MapItem('status', int),
    MapItem('length', int),
    MapItem('', None),
    MapItem('ua', lambda x: x)
]

def strptime(src: str) -> datetime.datetime:
    return datetime.datetime.strptime(src, '%d/%b/%Y:%H:%M:%S %z')

def extract(line):
    tmp = []
    ret = []
    split = True
    for c in line:
        if c == '[':
            split = False
            continue
        if c == ']':
            split = True
            continue
        if c == '"':
            split = not split
            continue
        if c == ' ' and split:
            ret.append(''.join(tmp))
            tmp.clear()
        else:
            tmp.append(c)
    ret.append(''.join(tmp))
    result = {}
    for i, item in enumerate(mapping):
        if item.name:
            result[item.name] = item.convert(ret[i])
    return result

# 從日誌檔案中載入
def load(path):
    with open(path) as f:
        try:
            yield extract(f.readline())
        except:
            pass
複製程式碼

正規表示式版:

import re
import datetime
from collections import namedtuple

Request = namedtuple('Request', ['method', 'url', 'version'])
line = '66.256.46.124 - - [10/Aug/2016:06:05:06 +0800] "GET /robots.txt HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible: Googlebot/2.1: +http://www.google.com/bot.html)"'

mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

def strptime(src: str) -> datetime.datetime:
    return datetime.datetime.strptime(src, '%d/%b/%Y:%H:%M:%S %z')

def extract(line):
    regexp = r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)'
    m = re.match(regexp, line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

print(extract(line))
複製程式碼

對於日誌或者監控資料這樣的時序資料做分析,通常進行時間進行相關的分析,因此需要一個滑動視窗。在時序分析中,滑動視窗是非常重要的,所謂的滑動視窗就是設定一個視窗在一堆以時間排列的資料中擷取一段資料,分析完成之後,視窗往前滑動,擷取下一段。滑動視窗有兩個引數非常重要,一個是 width(視窗的寬度),另一個是 interval(間隔)。意思是每 interval 秒分析前 width 秒的資料,因此 interval 必須要大於等於 width。

整合起來:

import re
import time
import queue
import datetime
import threading
from collections import namedtuple

matcher = re.compile(r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)')
Request = namedtuple('Request', ['method', 'url', 'version'])
mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

def extract(line):
    m = matcher.match(line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

def read(f):
    for _ in f:
        try:
            yield extract(f.readline())
        except:
            pass

def load(path):
    with open(path) as f:
        while True:
            read(f)
            time.sleep(0.1)

def window(source, handler, interval: int, width: int):
    store = []
    start = datetime.datetime.now()
    while True:
        data = next(source)
        current = datetime.datetime.now()
        if data:
            store.append(data)
            current = data['time']
        if (current - start).total_seconds() >= interval:
            start = current
            handler(store)
            dt = current - datetime.timedelta(seconds=width)
            store = [x for x in store if x['time'] > dt]

def dispatcher(source):
    analyers = []
    queues = []

    def _source(q):
        while True:
            yield q.get()

    def register(handler, interval, width):
        q = queue.Queue()
        queues.append(q)
        t = threading.Thread(target=window, args=(_source(q), handler, interval, width))
        analyers.append(t)

    def start():
        for t in analyers:
            t.start()
        for item in source:
            for q in queues:
                q.put(item)

    return register, start

# 以下是業務邏輯,需要分析什麼就寫在下面
def null_handler(items):
    pass

def status_handler(items):
    status = {}
    for x in items:
        if x['status'] not in status.keys():
            status[x['status']] = 0
        status[x['status']] += 1
    total = sum(x for x in status.values())
    for k, v in status.items():
        print('{} -> {}%'.format(k, v/total * 100))

if __name__ == '__main__':
    import sys
    register, start = dispatcher(load(sys.argv[1]))
    register(status_handler, 5, 10)
    start()
複製程式碼

上面的 handler 函式由我們自己定義,它就是要分析的內容,比如訪問的響應狀態碼等等。

import re
import time
import queue
import datetime
import threading
from collections import namedtuple
from watchdog.events import FileSystemEventHandler

matcher = re.compile(r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)')
Request = namedtuple('Request', ['method', 'url', 'version'])
mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

class Loader(FileSystemEventHandler):
    def __init__(self, path):
        self.path = path
        self.q = queue.Queue()
        self.f = open(path)

    def on_modified(self, event):
        if event.src_path == self.path:
            for item in read(self.f):
                self.q.put(item)

    def source(self):
        while True:
            yield self.q.get()

def extract(line):
    m = matcher.match(line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

def read(f):
    for line in f:
        try:
            yield extract(line)
        except:
            pass

def load(path):
    with open(path) as f:
        while True:
            yield from read(f)
            time.sleep(0.1)

def window(source, handler, interval: int, width: int):
    store = []
    start = None
    while True:
        data = next(source)
        store.append(data)
        current = data['time']
        if start is None:
            start = current
        if (current - start).total_seconds() >= interval:
            start = current
            try:
                handler(store)
            except:
                pass
            dt = current - datetime.timedelta(seconds=width)
            store = [x for x in store if x['time'] > dt]

def dispatcher(source):
    analyers = []
    queues = []

    def _source(q):
        while True:
            yield q.get()

    def register(handler, interval, width):
        q = queue.Queue()
        queues.append(q)
        t = threading.Thread(target=window, args=(_source(q), handler, interval, width))
        analyers.append(t)

    def start():
        for t in analyers:
            t.start()
        for item in source:
            for q in queues:
                q.put(item)

    return register, start

# 以下是業務邏輯,需要分析什麼就寫在下面
def null_handler(items):
    pass

def status_handler(items):
    status = {}
    for x in items:
        if x['status'] not in status.keys():
            status[x['status']] = 0
        status[x['status']] += 1
    total = sum(x for x in status.values())
    for k, v in status.items():
        print('\t{} -> {:.2f}%'.format(k, v/total * 100))

if __name__ == '__main__':
    import os
    import sys
    from watchdog.observers import Observer
    handler = Loader(sys.argv[1])
    observer = Observer()
    observer.schedule(handler, os.path.dirname(sys.argv[1]), recursive=False)
    register, start = dispatcher(handler.source())
    register(null_handler, 5, 10)
    observer.start()
    start()
複製程式碼

相關文章