13 - 高階函數-柯里化-裝飾器

1 高階函數

        在Python中一切皆對象,固然也包括函數。函數在Python中是一等公民(First Class Object)。即函數與字符串數組整型無異,它能夠被命名能夠被賦值能夠看成參數被傳進另外一個函數也能夠被另外一個函數看成返回值能夠放在任何位置,簡單來講:程序員

  • 函數也是一個對象,而且是一個可調用對象(callable)
  • 函數能夠做爲普通變量、參數、返回值等等

        那什麼是高階函數呢?在Python中咱們能夠理解爲:當一個函數接受另外一個函數做爲參數使用,或者一個函數最後返回了另一個函數。在這兩種狀況下,這個函數就能夠稱之爲高階函數(知足以上任意一種狀況便可,沒必要同時知足)。算法

數學概念: y = g(f(x))數組

def outer(x):
    def inner(step=1):
        nonlocal x    # 聲明局部變量x不是一個局部變量,應該在外層尋找 
        x += step
        return x
    return inner
foo1 = outer(10)
foo2 = outer(10)
print(foo1())
print(foo2())

        內層函數inner還引用了外層函數的自由變量x,造成了閉包,因爲外部函數返回了內部函數,因此這就是一個典型的高階函數。針對上面實例還須要說明是:閉包

  • ounter函數每調用一次生成的都是一個新False的函數對象
  • foo1和foo2也都是相互獨立的兩個函數
In [1]: def outer(x):
   ...:     def inner(step=1):
   ...:         nonlocal x    # 聲明局部變量x不是一個局部變量,應該在外層尋找
   ...:         x += step
   ...:         return x
   ...:     return inner
   ...: foo1 = outer(10)
   ...: foo2 = outer(10)
   ...: foo1 is foo2

is的比較作則是:先比較元素的內存地址,而後比較元素的內容。首先 foo1和foo2 屬於不一樣的對象,因此內存地址確定不一樣,而後因爲函數對象沒有實現函數內容的比較因此這裏返回Falseapp

1.1 自定義sort函數

        從頭構建一個相似於內置函數sorted的函數,來體會高階函數的做用,順便理解一下sorted的原理。dom

1.1.1 將規模縮小,先實現排序,先無論key和reverse參數

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        for i, k in enumerate(new_list):
            if value < k:
                new_list.insert(i, value)
                break
        else:
            new_list.append(value)
    return new_list

print(sort([1,6,2,7,9,3,5]))

分析:函數

  • sort返回一個新的列表,咱們這裏構建一個列表,用於存放排序好的列表
  • 爲了練習算法,這裏選擇使用直接插入排序,注意:直接插入排序是原地排序,這裏是變化,直接插入到新的已排序列表中去
  • 利用enumerate構建索引,用於確認插入位置,注意:由於new_list在咱們這個場景下是有序的!因此才能夠這樣用
  • 初始狀況下,因爲new_list爲空,因此待排序列表的第一個元素會被直接插入到已排序列表的首位
  • 後續只需在待排序的列表中,拿出一個數據,和已排序區的元素,從左至右依次對比便可
  • 若是大於已排序區的全部元素,那麼直接追加便可,若是小於某一個元素只須要在對應的元素爲插入待排序元素便可
  • 若是小於的話,由於列表的特性,在一個位置上插入數據,那麼原數據會自動向右移動,因此符合直接插入排序的原理

1.1.2 添加reverse參數判斷

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        for i, k in enumerate(new_list):
            flag = value > k if reverse else value < k
            # if reverse:
            #     flag = value > k
            # else:
            #     flag = value < k
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
    # return new_list[::-1]
print(sort([1,6,2,7,9,3,3,5]))

分析:學習

  • 倒序有兩種表達方式,即正序排好,而後倒着截取。或者是按照倒序排列,這裏使用倒序排列。
  • 正序時:若是value大於k,那麼須要把value插入到k的位置上
  • 倒序時,若是value小於k,那麼須要把value插入到k的位置上
  • 因此添加flag來採集用戶傳入的reverse參數,這裏使用了三元表達式來簡化代碼

1.1.3 添加key參數判斷

def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        value_new = key(value) if key else value
        for i, k in enumerate(new_list):
            k_new = key(k) if key else k
            flag = value_new > k_new if reverse else value_new < k_new
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
    # return new_list[::-1]
print(sort(['a',1,2,'b'], key=str, reverse=True))

分析:優化

  • key傳入了一個函數,用於對每一個key進行轉換,而後使用轉換後的元素來進行比較,因此咱們這裏使用了轉化後的變量來比較
  • 當key能夠被調用,那麼咱們認爲它是一個函數,那麼調用他對元素進行轉換。
  • 這裏傳入了str函數,若是結合前面所學的知識能夠改成lambda表達式。
def sort(iterable, *, key=None, reverse=False):
    new_list = []
    for value in iterable:
        value_new = key(value) if callable(key) else value
        for i, k in enumerate(new_list):
            k_new = key(k) if callable(key) else k
            flag = value_new > k_new if reverse else value_new < k_new
            if flag:
                    new_list.insert(i, value)
                    break
        else:
            new_list.append(value)
    return new_list
print(sort(['a',1,2,'b'], key=lambda x:str(x), reverse=True))
  • 傳參時利用了lambda表達式,在函數內部,每次傳入一個參數,返回它的str對象,其實效果等同於str,這裏只是提一下lambda表達式。

1.2 內建函數(高階函數)

Python內置了不少高階函數的應用,這裏僅介紹較爲經常使用的。

  • sorted: 排序函數,直接返回新的列表
  • filter:過濾函數,返回一個迭代器
  • map:映射函數,返回一個迭代器
  • zip:拉鍊函數,返回一個迭代器

1.2.1 sorted排序

sorted(iterable, /, *, key=None, reverse=False)

當即返回一個新的列表,對一個可迭代對象的全部元素排序。

  • key: 排序規則爲key定義的函數
  • reverse: 表示是否進行翻轉排序
In [49]: lst
Out[49]: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
In [50]: import random
In [51]: random.shuffle(lst)
In [52]: lst
Out[52]: [70, 45, 90, 40, 30, 80, 25, 55, 5, 75, 85, 95, 50, 20, 35, 15, 10, 60, 65, 0]
In [53]: sorted(lst,reverse=True)
Out[53]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
In [54]: lst.append('a')
In [55]: sorted(lst,key=str,reverse=True)
Out[55] :['a', 95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 5, 45, 40, 35, 30, 25, 20, 15, 10, 0]
In [58]: sorted(lst,key=lambda x:str(x))
Out[58]: [0, 10, 15, 20, 25, 30, 35, 40, 45, 5, 50, 55, 60, 65, 70 75, 80, 85, 90, 95, 'a']
In [59]: lst.remove('a')
In [61]: sorted(lst,key=lambda x : 100 - x )
Out[61]: [95, 90, 85, 80, 75, 70, 65, 60, 55, 50, 45, 40, 35, 30, 25, 20, 15, 10, 5, 0]
  • 這裏定義了一個接受一個參數的匿名函數,而後返回處理後的元素(這是匿名函數最經常使用的場景之一)
  • 功能等同於直接調用str函數

    sorted直接返回一個新的列表,而列表也有sort方法,經過list.sort()進行排序是直接原地進行排序的。

1.2.2 filter 過濾

filter(function or None, iterable) --> filter object

過濾可迭代對象的元素,返回一個迭代器

  • function:表示一個函數,它的功能是:每次從可迭代對象iterable取出一個元素交給function函數處理,若是返回True,則保留該元素,不然提出該元素,若是是None,表示剔除等效False的對象
  • iterable:可迭代對象
In [62]: lst1 = [1,9,55,150,-3,78,28,123]
In [64]: list(filter(lambda x:x%3==0,lst1))
Out[64]: [9, 150, -3, 78, 123]


# 等同於
In [66]: def func(iterable):
    ...:     for i in iterable:
    ...:         if i % 3 == 0:
    ...:             yield i
    ...:

In [67]: list(func(lst1))
Out[67]: [9, 150, -3, 78, 123]


# 或者
In [66]: def func(iterable):
    ...:     for i in iterable:
    ...:         if (lambda x:x%3==0)(i):    ## l這裏屬於函數調用:func()()
    ...:             yield i
    ...:

1.2.3 map 映射

map(func, *iterables) --> map object

對多個可迭代對象的元素按照指定的函數進行映射,返回一個迭代器

  • func:一個函數,用於處理iterable的元素,在指定多個iterable對象是,函數的參數數量與iterable的數量是相等的。
  • *iterable:一個或多個可迭代對象
In [69]: list(map(str,range(10)))
Out[69]: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

In [70]: list(map(lambda x:x+1,range(10)))
Out[70]: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

In [73]: dict(list(map(lambda x,y:(x,y),'abcde',range(10))))
Out[73]: {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4}

In [74]: dict(map(lambda x:(x%5,x),range(500)))
Out[74]: {0: 495, 1: 496, 2: 497, 3: 498, 4: 499}

map對象的長度,等同於最小的iterable長度。(木桶效應)

2 柯里化

        在計算機科學中,柯里化(英語:Currying),又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。-- 來自維基百科的解釋
        總結一下:柯里化指的就是將原來接受兩個參數的函數變成新的接受一個參數的函數的過程。新的函數返回一個以原有第二個參數爲參數的函數。其數學表達式爲:

z = f(x, y) 轉換成 z = f(x)(y) 的形式

這裏把經典的add函數拿來進行轉換:

  • 想要轉換爲add(4)(5),那麼add(4)必需要返回一個函數,不然沒法將5看成參數傳入
def add(x, y):
    return x + y

def new_add(x):
    def inner(y):
        return x + y

    return inner


print(add(4, 5))     # 9
print(new_add(4)(5)) # 9
print(add(4, 5) == new_add(4)(5))  # True

經過函數嵌套,就能夠完成函數的柯里化了,對柯里化有所瞭解之後,那麼咱們就能夠繼續來看Python中對咱們小白來講的第一個難點:裝飾器

3 裝飾器

        什麼是裝飾器?歸納的講,裝飾器的做用就是爲已經存在的對象添加額外的功能。咱們接下來從一個需求開始學習裝飾器。

3.1 需求分析

如今有以下函數,咱們須要將這個函數的日誌打印到終端

def add(x,y):
    print('我被調用執行啦')  # 新增打印日誌語句
    return x + y
add(100,200)

        可是仔細思考,打印日誌是一個獨立的功能,它和add函數自己並無什麼關聯關係,咱們說一個函數是爲完成一個工程的,因此直接寫在函數裏面不是不能夠,可是不建議,而且打印日誌屬於調試信息功能,與業務無關,不該該放在業務函數加法中。
        若是不須要寫在函數的裏面,那麼咱們得想辦法寫在函數的外面

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

def logger(fn, x, y):
    print('函數開始執行')
    res = fn(x, y)
    print('函數執行完畢')
    return res

logger(add,4,5)
  • 這樣就解決了打印語句在函數內定義的問題了。
  • 可是若是add函數的形參不少,咱們要挨個寫上嗎?因此*args,**kwargs幫了咱們很大的忙
def add(x, y):
    return x + y

def logger(fn, *args, **kwargs):
    print('函數開始執行')
    res = fn(*args, **kwargs)
    print('函數執行完畢')
    return res

logger(add,4,5)

3.2 函數柯里化

        使用logger(add,4,5)來調用咱們的add函數真是太醜了,給其餘人看,可能人家也不知道你要幹啥,結合前面所學的柯里化,咱們進行以下變更

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

def logger(fn):
    def wrapper(*args, **kwargs):
        print('函數被執行了')
        res = fn(*args, **kwargs)
        print('函數執行完畢')
        return res
    return wrapper

logger(add)(4,5)

        這樣看起來是否是就好看多了?當指定logger(add)(4,5)時,纔會打印日誌,但若是想要在全部調用add函數的地方,咱們還須要在全部調用add的地方修改成logger(add)(參數),想想,若是我能把logger(add)變成add是否是就能夠直接寫成add(4,5)了呢?

logger(add)(4,5)
--------
add = logger(add)
add(4,5)    # 將add從新指向了新的函數wrapper

        按照柯里化的原型中logger(add)返回了一個函數wrapper,而咱們的(4,5)實際上是傳遞給了wrapper,結合咱們前面所學的高階函數,這裏的wrapper,是一個閉包函數,由於在內部對fn進行了執行,並且增長了打印日誌的功能,咱們在執行wrapper的同時,也會執行原來的函數fn,而且添加了打印日誌的功能,因此logger就是一個裝飾器函數!!!

3.3 裝飾器函數(語法糖)

        語法糖(Syntactic sugar),也譯爲糖衣語法,是由英國計算機科學家彼得·蘭丁發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,可是更方便程序員使用。 語法糖讓程序更加簡潔,有更高的可讀性。Python針對咱們剛剛編寫的logger(add)函數,進行了語法糖優化,因此下面是咱們使用語法糖以後的

def logger(fn):
    def wrapper(*args, **kwargs):
        print('函數被執行了')
        res = fn(*args, **kwargs)
        print('函數執行完畢')
        return res
    return wrapper

@logger   # 等於 add = logger(add)
def add(x, y):
    return x + y

add(4,5)

        當解釋器執行到@logger時,會自動把它下面的函數看成參數,傳給logger函數,因此這裏@logger 其實就等於 add = logger(add) 另外,logger必需要定義在add函數以前才能夠被裝載!這一點很重要!

3.4 裝飾器帶來的問題

利用裝飾器計算以下函數的運行時間

import time
import datetime

def logger(fn):
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函數:{} 執行用時:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

執行結果:
In [76]: add(4,5)
函數:wrapper 執行用時:2.000944

這裏__name__表示函數的名稱.

        什麼鬼?這裏爲何打印的是wrapper啊,爲何不是add呢?這樣的話,別人不就發現我把這個函數給偷偷換掉了嗎?不行不行,我得想個辦法把函數的屬性複製過來,因爲這個功能和打印用時的裝飾器不是一個功能,那麼咱們還得給裝飾器另加一個裝飾器。-_-!

import time
import datetime

def copy_properties(old_fn):
    def wrapper(new_fn):
        new_fn.__name__ = old_fn.__name__
        return new_fn
    return wrapper

def logger(fn):
    @copy_properties(fn)  # wrapper = copy_properties(fn)(wrapper)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函數:{} 執行用時:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

add(4,5)
  • 解釋器執行到 @copy_properties(fn) 時,會把下面的wraper裝入,等於wrapper = copy_properties(fn)(wrapper)
  • 因爲知道了參數的個數(必定是一個函數對象),這裏就沒有使用*args, **kwargs
  • 函數的屬性不止__name__一個,其餘的怎麼辦呢?

3.5 拷貝函數屬性

        Python的內置模塊functools中,內置了不少經常使用的高階函數,其中wraps就是用來拷貝函數的屬性及簽名信息的。利用wraps,咱們就不須要本身編寫copy_properties函數了,下面是修改後的版本

import time
import datetime
import functools

def logger(fn):
    @functools.wraps(fn)  # wrapper = functools.wraps(fn)(wrapper)
    def wrapper(*args, **kwargs):
        start = datetime.datetime.now()
        res = fn(*args, **kwargs)
        total_seconds = (datetime.datetime.now() - start).total_seconds()
        print('函數:{} 執行用時:{}'.format(wrapper.__name__,total_seconds))
        return res
    return wrapper

@logger
def add(x, y):
    time.sleep(2)
    return x + y

add(4,5)

經過使用 @functools.wraps(fn) 咱們能夠方便的拷貝函數的屬性簽名信息,好比:'module', 'name', 'qualname', 'doc','annotations'等,這些屬性信息,將在後續部分進行講解,這裏知道便可

4 代參裝飾器

        上面章節講到的是帶一個參數(函數)的裝飾器,在Python中這種裝飾器被稱爲無參裝飾器,由於語法糖的表現形式就是 @logger,下面要說的是代參數的裝飾器,即@logger(50)

4.1 仍是從一個需求開始

        以上述函數爲例,咱們須要記錄當函數執行超過必定時間時的日誌信息,該怎麼辦呢?假設這個時間是5秒,那麼很顯然,咱們須要把這個時間變量傳入到裝飾器中進行判斷。也就是說咱們須要寫成這種形式:

logger(5)(add)

looger(5)返回的是一個函數,不然沒法將add傳入

4.2 代參裝飾器編寫

import time
import datetime
import functools

def logger(var):
    def inner(func):
        def wrapper(*args, **kwargs):
            start = datetime.datetime.now()
            res = func(*args, **kwargs)
            total_seconds = (datetime.datetime.now() - start).total_seconds()
            if total_seconds > var:
                print('函數執行時間過長')
            return res
        return wrapper
    return inner

@logger(5)  # logger(5)(add)
def add(x, y):
    time.sleep(6)
    return x + y

是否是很簡單? 在掌握了柯里化以及無參裝飾器。

4.3 代參裝飾器小結

代參裝飾器有以下特色:

  1. 它是一個函數
  2. 函數做爲它的形參
  3. 返回值是一個不帶參數的裝飾器函數
  4. 使用@function_name(參數列表)方式調用
  5. 能夠看作在裝飾器外層又加了一層函數

    裝飾器何時被執行?,還記得@logger等於什麼嗎? add = logger(add) 等號等於賦值,是否是要先計算右邊的?因此,裝飾器在函數定義階段就已經被執行了!不是等到被裝飾的函數執行時,才執行哦!真正執行時,每次都會生成一個新的wrapper被調用而已!

相關文章
相關標籤/搜索