天天看點

python:函數的進階特性

很多語言中,都允許把函數本身做為參數,傳遞給其它參數:即所謂的高階函數。python中也有類似特性:

一、map/reduce、filter、sorted

hadoop裡的map-reduce思想在python裡已經變成内置函數了。map是将某個函數逐一作用于清單中的每個元素。reduce則先從清單中取頭2個元素,傳到指定函數,然後将計算結果與餘下元素依次重複,直到List處理完。

1.1 map示例:(将List中的所有元素*10)

def fn_map(x):
    print("fn_map->", x)
    return 10 * x


L = [3, 4, 6, 8]

print(list(map(fn_map, L)))
print("\n")
      

輸出:

fn_map-> 3
fn_map-> 4
fn_map-> 6
fn_map-> 8
[30, 40, 60, 80]
      

結合map,我們再把reduce函數加上(最終效果:将所有元素*10再平方,最終得出 “平方和”的"平方根")

def fn_sqrt(x, y):
    print("fn_sqrt->", x, ",", y)
    return math.sqrt(x ** 2 + y ** 2)


def fn_map(x):
    print("fn_map->", x)
    return 10 * x


L = [3, 4, 6, 8]

result = reduce(fn_sqrt, map(fn_map, L))
print(result)

print("\n")
print(math.sqrt((3 * 10) ** 2 + (4 * 10) ** 2 + (6 * 10) ** 2 + (8 * 10) ** 2))
      

 注:要先import math,上面的代碼輸出如下:

fn_map-> 3
fn_map-> 4
fn_sqrt-> 30 , 40
fn_map-> 6
fn_sqrt-> 50.0 , 60
fn_map-> 8
fn_sqrt-> 78.10249675906654 , 80
111.80339887498948


111.80339887498948
      

上面這個例子,可能實用性不大,下面給個實用性更強的示例,将每個單詞的首字母大寫,其它字母變小寫。

def normalize(name):
    return name[:1].upper() + name[1:].lower()


L1 = ['adam', 'LISA', 'barT']
print(list(map(normalize, L1)))
      

['Adam', 'Lisa', 'Bart']

1.2 filter

filter跟java8裡的stream的filter是類似的,可以實作對集合中的元素,按某種規則進行篩選。

示例1:找出10以内的偶數

result = filter(lambda x: x % 2 == 0, range(1, 11))
print(list(result))


# 上面的寫法,等效于下面這個
def even(x):
    return x % 2 == 0


print(list(filter(even, range(1, 11))))
      
[2, 4, 6, 8, 10]
[2, 4, 6, 8, 10]
      

示例2:找出200以内的"回數"(即:從左向右,從右向左,都是一樣的數,比如:131, 141)

def is_palindrome1(n):
    if n < 10:
        return True
    s = str(n)
    for i in range(0, int(len(s) / 2)):
        if s[i] == s[-i - 1]:
            return True
    return False


def is_palindrome2(n):
    s1 = str(n)
    s2 = list(reversed(s1))
    return list(s1) == s2


print(list(filter(is_palindrome1, range(1, 201))))
print(list(filter(is_palindrome2, range(1, 201))))
      
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]
      

  

1.3 sorted

python内置的排序函數sorted,支援數字/字母/以及複雜對象排序,預設是從小到大排序,對于複雜對象的排序規則可以開發者自定義。參考下面的示例:

origin = [-1, 3, -5, 2, -4, 6]
# 從小到大排序
a = sorted(origin)
print(a)

# 按abs絕對值,從小大到排序
a = sorted(origin, key=abs)
print(a)

# 從大到小排序
a = sorted(origin, reverse=True)
print(a)

origin = ["Xy", "Aa", "Bb", "dd", "cC", "aA", "Zo"]

# 按字母ascii值從小到大排序
print(sorted(origin))

# 将字母轉大寫後的值排序(即:忽略大小寫)
print(sorted(origin, key=str.upper))

# 将字母轉大寫後的值倒排序
print(sorted(origin, key=str.upper, reverse=True))

# 複雜對象排序
origin = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]


def by_name(t):
    return t[0]


# 按人名排序
print(sorted(origin, key=by_name))


def by_score(t):
    return t[1]


# 按得分倒排
print(sorted(origin, key=by_score, reverse=True))
      
[-5, -4, -1, 2, 3, 6]
[-1, 2, 3, -4, -5, 6]
[6, 3, 2, -1, -4, -5]
['Aa', 'Bb', 'Xy', 'Zo', 'aA', 'cC', 'dd']
['Aa', 'aA', 'Bb', 'cC', 'dd', 'Xy', 'Zo']
['Zo', 'Xy', 'dd', 'cC', 'Bb', 'Aa', 'aA']
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]
[('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]
      

二、延遲計算/閉包

python的函數定義可以嵌套(即:函數内部再定義函數),利用這個特性很容易實作延遲計算:

import time


def export1(month):
    print("export1 month:", month, " doing...")
    time.sleep(5)
    print("export1 done!")


def export2(month):
    def do():
        print("export2 month:", month, " doing...")
        time.sleep(5)
        print("export2 done!")

    return do


export1(10)

print("----------------")

r2 = export2(10)
print(r2)
r2()
      

這裡我們模拟一個耗時的導出功能(假設:要求傳入月份,然後導出該月的報表資料),export1為正常版本,調用export1就會馬上執行。而export2則是傳回一個内部函數do(),調用export2後,傳回的是一個Function,并沒有實際執行(可以了解為: 傳回的是業務處理算法,而非處理結果),真正需要結果的時候,再來調用"傳回函數"。

上面的代碼輸出如下:

export1 month: 10  doing...
export1 done!
----------------
<function export2.<locals>.do at 0x107a24a60>
export2 month: 10  doing...
export2 done!
      

閉包

很多語言都支援閉包特性,python中當然少不了這個,參考下面的示例:

def my_sqrt1(n):
    r = []

    def do():
        for i in range(1, n + 1):
            r.append(i ** 2)
        return r

    return do


a = my_sqrt1(4)
print(type(a))
b = a()
print(type(b))
print(b)
      
<class 'function'>
<class 'list'>
[1, 4, 9, 16]        

閉包有一個經典的坑:不要在閉包函數中使用“值會發生變化的變量"(比如:for循環中的變量)。原因是:python中的閉包本質上是是"内部函數"延時計算,如果有循環變量,循環過程中閉包函數并不會執行,等循環結束了,閉包中引用的循環變量其實是循環結束後最終的值。說起來有點繞口,看下面的示例:

def my_sqrt2(n):
    r = []
    for i in range(1, n + 1):
        def do():
            r.append(i ** 2)
            return r

    return do


a = my_sqrt2(4)
print(type(a))
b = a()
print(type(b))
print(b)
      
<class 'function'>
<class 'list'>
[16]
      

解釋一下:調用a = my_sqrt2(4)時,my_sqrt2(4)馬上執行完了,這時候裡面的fox循環執行完了,最後i的值停在4,然後這個值被封閉在do函數裡,并沒有馬上執行。然後再調用a()時,這時候才真正調用do()函數,此時i值=4,是以最終r[]清單裡,就隻追回了一個值4*4=16 

如果非要使用循環變量,隻能想招兒把這個循環變量,也封閉到一個内部函數裡,然後再使用,比如下面這樣:

def my_sqrt3(n):
    def f(j):
        def g():
            return j ** 2

        return g

    r = []
    for i in range(1, n + 1):
        r.append(f(i))

    return r


a = my_sqrt3(4)
print(type(a))

for x in a:
    print(x())
      

這個例子仔細研究下蠻有意思的,r.append(f(i)),清單裡追加的并非計算結果,而是f(j)裡傳回的函數g,是以a = my_sqrt3(4)這裡,a得到的是一個function組成的list,然後list裡的每個g函數執行個體,都封閉了當次循環的變量i,因為閉包的緣故,i值已經被封印在g内部,不管外部的for循環如何變量,都不會影響函數g。

輸出如下:

<class 'list'>
1
4
9
16
      

關于閉包,最後再來看一個廖老師教程上的作業題,用閉包的寫法寫一個計數器:

def create_counter1():
    r = [0]

    def counter():
        r[0] += 1
        return r[0]

    return counter


count = create_counter1();

print([count(), count(), count()])
      
[1, 2, 3]       

對于有潔癖的程式員,可能會覺得要額外設定一個隻儲存1個元素的list,有點浪費。可以換種寫法:

def create_counter2():
    n = 0

    def counter():
        nonlocal n
        n += 1
        return n

    return counter

count = create_counter2();

print([count(), count(), count()])
      
[1, 2, 3]        

注意這裡有一個關鍵字nonlocal,号稱是python3新引入的關鍵字,為的是讓閉包的内部函數裡面,能讀寫内部函數外的變量。(但是在第1種寫法中,r=[0]不也是定義在外部麼?差別就是list是複雜的變量類型,而第2種寫法中n是簡單類型的變量,做為python初學者,不是很了解這個哲學思想^_~)

三、aop/裝飾器

aop是java生态體系中的精髓之一,而python裡同樣能做到,而且看上去更簡潔。

比如有一個加法函數:

def add1(i, j):
    return i + j        

想在add1調用時,自動把入參,傳回結果,以及執行時間都記錄下來,可以這麼做,再定義一個log函數(類似java中的aspect切面定義)

import time

def log(fn):
    def do(*args, **kw):
        start = time.time()
        result = fn(*args, **kw)
        end = time.time()
        print("function=>", fn.__name__, ",args1=>", args, ",args2=>", kw, ",result=>", result, ",exec_time=>",
              (end - start) * 1000, "ms")
        return result

    return do        

然後在add1函數上加上這個"注解"就可以了

@log
def add1(i, j):
    return i + j


print(add1(1, 2))
      
function=> add1 ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 0.0030994415283203125 ms
3
      

如果aop本身的"切面"也需要傳參數進來,比如:在日志前想附加一段特定的字首,可以參考下面這樣:

import time


def log(fn):
    def do(*args, **kw):
        start = time.time()
        result = fn(*args, **kw)
        end = time.time()
        print("function=>", fn.__name__, ",args1=>", args, ",args2=>", kw, ",result=>", result, ",exec_time=>",
              (end - start) * 1000, "ms")
        return result

    return do


def log2(log_prefix):
    def around(fn):
        def do(*args, **kw):
            start = time.time()
            result = fn(*args, **kw)
            end = time.time()
            print(log_prefix, ",function=>", fn.__name__, ",args1=>", args,
                  ",args2=>", kw, ",result=>", result, ",exec_time=>",
                  (end - start) * 1000, "ms")
            return result

        return do

    return around


@log2("調用日志:")
@log
def add2(i, j):
    time.sleep(1)
    return i + j


print(add2(1, 2))
      
function=> add2 ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 1000.8599758148193 ms
調用日志: ,function=> do ,args1=> (1, 2) ,args2=> {} ,result=> 3 ,exec_time=> 1001.0490417480469 ms
3
      

注:這裡我們刻意把log,log2同時運用在add2上,從輸出上看,二個aspect都起作用了。

四、偏函數

還是拿add(i,j) 這個來說事兒吧,如果我們經常會遇到一個場景:想讓某個數字固定+10,在其它語言裡,通常是再定義一個類似add_10(i)的重載版本(java/c#都是這麼幹的),但是對于python來說,可以更優雅:

import functools


def add(i, j):
    return i + j


# 偏函數
add_10 = functools.partial(add, j=10)

print(add(1, 2))
print(add_10(1))
      
3
11
      

這種把參數清單中的某些常用項固定,然後再生成一個别名函數的玩法,就稱為偏函數

參考文檔:

1、廖雪峰的python教程:

函數式程式設計

作者:

菩提樹下的楊過

出處:

http://yjmyzz.cnblogs.com

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。