歡迎光臨
每天分享高質量文章

教程 | 十分鐘學會函式式 Python

(點擊上方公眾號,可快速關註一起學Python)

導讀:函式式編程到底是什麼?本文將詳解其概念,同時分享怎樣在 Python 中使用函式式編程。主要內容包括串列解析式和其他形式的解析式。

 

作者 :Brandon Skerritt   

來源:CSDN     

函式式模型

在命令式模型中,執行程式的方式是給計算機一系列指令讓它執行。執行過程中計算機會改變狀態。例如,比如 A 的初始值是 5,後來改變了 A 的值。那麼 A 就是個變數,而變數的意思就是包含的值會改變。

而在函式式樣式中,你不需要告訴計算機做什麼,而是告訴計算機是什麼。比如數字的最大公約數是什麼,1 到 n 的乘積是什麼等等。

因此,變數是不能被改變的。變數一旦被設置,就永遠保持同一個值(註意在純粹的函式式語言中,它們不叫變數)。因此,在函式式模型中,函式沒有副作用。副作用就是函式對函式外的世界做出的改變。來看看下麵這段Python代碼的例子:

a = 3
def some_func():
    global a
    a = 5

some_func()
print(a)

代碼的輸出是 5。在函式式模型中,改變變數的值是完全不允許的,讓函式影響函式外的世界也是不允許的。函式唯一能做的就是做一些計算然後傳回一個值。

你可能會想:“沒有變數也沒有副作用?這有什麼好的?”好問題。

如果函式使用同樣的引數呼叫兩次,那麼我們可以保證它會傳回同樣的結果。如果你學過數學函式,你肯定知道這樣做的好。這叫做取用透明性(referential transparency)。由於函式沒有副作用,那麼我們可以加速計算某個東西的程式。比如,如果程式知道 func(2)傳回 3,那麼可以將這個值儲存在表中,這樣就不需要重覆運行我們早已知道結果的函式了。

通常,函式式編程不使用迴圈,而是使用遞迴。遞迴是個數學概念,通常的意思是“把結果作為自己的輸入”。使用遞迴函式,函式可以反覆呼叫自己。下麵就是個使用Python定義的遞迴函式的例子:

def factorial_recursive(n):
    # Base case: 1! = 1
    if n == 1:
        return 1

    # Recursive case: n! = n * (n-1)!
    else:
        return n * factorial_recursive(n-1)

函式式編程語言也是懶惰的。懶惰的意思是,除非到最後一刻,否則它們不會執行計算或做任何操作。如果代碼要求計算2+2,那麼函式式程式只有在真正用到計算結果的時候才會去計算。我們馬上就會介紹Python中的這種懶惰。

映射

要理解映射(map),首先需要理解什麼是可迭代物件。可迭代物件(iterable)指任何可以迭代的東西。通常是串列或陣列,但 Python 還有許多其他可迭代物件。甚至可以自定義物件,通過實現特定的魔術方法使其變成可迭代物件。魔術方法就像 API 一樣,能讓物件更有 Python 風格。要讓物件變成可迭代物件,需要實現以下兩個魔術方法:

class Counter:
    def __init__(self, low, high):
        # set class attributes inside the magic method __init__
        # for "inistalise"
        self.current = low
        self.high = high

    def __iter__(self):
        # first magic method to make this object iterable
        return self

    def __next__(self):
        # second magic method
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

第一個魔術方法“__iter__”(雙下劃線iter)傳回迭代子,通常在迴圈開始時呼叫。__next__則傳回迭代的下一個物件。

可以打開命令列試一下下麵的代碼:

for c in Counter(3, 8):
    print(c)

這段代碼將會輸出:

3
4
5
6
7
8

在 Python 中,迭代器就是只實現了__iter__魔術方法的物件。也就是說,你可以訪問物件中都包含的位置,但無法遍歷整個物件。一些物件實現了__next__魔術方法,但沒有實現__iter__魔術方法,比如集合(本文稍後會討論)。在本文中,我們假設涉及到的一切物件都是可迭代的物件。

現在我們知道了什麼是可迭代的物件,回過頭來討論下映射函式。映射可以對可迭代物件中的每個元素執行指定的函式。通常,我們對串列中的每個元素執行函式,但要知道映射其實可以針對絕大多數可迭代物件使用。

map(functioniterable)

假設有一個串列由以下數字組成:

[12345]

我們希望得到每個數字的平方,那麼代碼可以寫成這樣:

x = [12345]
def square(num):
    return num*num

print(list(map(square, x)))

Python中的函式式函式是懶惰的。如果我們不加“list()”,那麼函式只會將可迭代物件儲存下來,而不會儲存結果的串列。我們需要明確地告訴Python“把它轉換成串列”才能得到結果。

在Python中一下子從不懶惰的函式求值轉換到懶惰的函式似乎有點不適應。但如果你能用函式式的思維而不是過程式的思維,那麼最終會適應的。

這個“square(num)”的確不錯,但總覺得有點不對勁。難道為了僅使用一次的map就得定義整個函式嗎?其實我們可以使用lambda函式(匿名函式)。

Lambda 運算式

Lambda運算式就是只有一行的函式。比如下麵這個lambda運算式可以求出給定數字的平方:

square = lambda x: x * x

運行下麵的代碼:

>>> square(3)
9

你肯定在問:“引數去哪兒了?這究竟是啥意思?看起來根本不像函式啊?”

嗯,的確是不太容易懂……但還是應該能夠理解的。我們上面的代碼把什麼東西賦給了變數“square”。就是這個東西:

lambda x:

它告訴Python這是個lambda函式,輸入的名字為x。冒號後面的一切都是對輸入的操作,然後它會自動傳回操作的結果。

這樣我們的求平方的代碼可以簡化成一行:

x = [12345]
print(list(map(lambda numnum * num, x)))

有了lambda運算式,所有引數都放在左邊,操作都放在右邊。雖然看上去有點亂,但不能否認它的作用。實際上能寫出只有懂得函式式編程的人才能看懂的代碼還是有點小興奮的。而且把函式變成一行也非常酷。

歸納

歸納(reduce)是個函式,它把一個可迭代物件變成一個東西。通常,我們在串列上進行計算,將串列歸納成一個數字。歸納的代碼看起來長這樣:

reduce(functionlist)

上面的函式可以使用lambda運算式。

串列的乘積就是把所有數字乘到一起。可以這樣寫代碼:

product = 1
x = [1, 2, 3, 4]
for num in x:
    product = product * num

但使用歸納,可以寫成這樣:

from functools import reduce

product = reduce((lambda x, y: x * y),[1234])

這樣能得到同樣的結果。這段代碼更短,而且借助函式式編程,這段代碼更簡潔。

過濾

過濾(filter)函式接收一個可迭代物件,然後過濾掉物件中一切不需要的東西。

通常過濾接收一個函式和一個串列。它會針對串列中的每個元素執行函式,如果函式傳回True,則什麼都不做。如果函式傳回False,則從串列中去掉那個元素。

語法如下:

filter(functionlist)

我們來看一個簡單的例子。沒有過濾,代碼要寫成這樣:

x = range(-55)
new_list = []

for num in x:
    if num 0
:
        new_list.append(num)

使用過濾可以寫成這樣:

x = range(-55)
all_less_than_zero = list(filter(lambda num: num 0
, x))

高階函式

高階函式接收函式作為引數,傳回另一個函式。一個非常簡單的例子如下所示:

def summation(nums):
    return sum(nums)

def action(func, numbers):
    return func(numbers)

print(action(summation, [123]))

# Output is 6

或者更簡單“傳回函式”的例子:

def rtnBrandon():
    return "brandon"
def rtnJohn():
    return "john"

def rtnPerson():
    age = int(input("What's your age?"))

    if age == 21:
        return rtnBrandon()
    else:
        return rtnJohn()

還記得之前說過函式式編程語言沒有變數嗎?實際上高階函式能很容易做到這一點。如果你只需要在一系列函式中傳遞資料,那麼資料根本不需要儲存到變數中。

Python 中的所有函式都是頂級物件。頂級物件是擁有一個或多個以下特征的物件:

  • 在運行時生成

  • 賦值給某個資料結構中的變數或元素

  • 作為引數傳遞給函式

  • 作為函式的結果傳回

所以,所有 Python 中的函式都是物件,都可以用作高階函式。

部分函式

部分函式有點難懂,但非常酷。通過它,你不需要提供完整的引數就能呼叫函式。我們來看個例子。我們要創建一個函式,它接收兩個引數,一個是底,另一個是指數,然後傳回底的指數次冪,代碼如下:

def power(base, exponent):
  return base ** exponent

現在我們需要一個求平方的函式,可以這麼寫:

def square(base):
  return power(base, 2)

這段代碼沒問題,但如果需要立方函式怎麼辦?或者四次方函式呢?是不是得一直定義新的函式?這樣做也行,但是程式員總是很懶的。如果需要經常重覆一件事情,那就意味著一定有辦法提高速度,避免重覆。我們可以用部分函式實現這一點。下麵是使用部分函式求平方的例子:

from functools import partial

square = partial(power, exponent=2)
print(square(2))

# output is 4

這是不是很苦?我們事先告訴 Python 第二個引數,這樣只需要提供一個引數就能呼叫需要兩個引數的函式了。

還可以使用迴圈來生成直到能計算 1000 次方的所有函式。

from functools import partial

powers = []
for x in range(2, 1001):
  powers.append(partial(power, exponent = x))

print(powers[0](3))
# output is 9

函式式編程不夠 Python

你也許註意到了,我們這裡許多函式式編程都用到了串列。除了歸納和部分函式之外,所有其他函式都生成串列。Guido(Python發明人)不喜歡在 Python 中使用函式式的東西,因為 Python 有自己的方法來生成串列。

在 Python IDLE 中敲“import this”,可以看到下麵的內容:

>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one — and preferably only one — obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!

這就是Python之禪。這首詩表明瞭什麼叫做Python風格。我們要指出的是這句話:

There should be one — and preferably only one — obvious way to do it.

(任何事情應該有一個且只有一個方法解決。)

在 Python 中,映射和過濾能做到的事情,串列解析式(稍後介紹)也能做到。這就打破了 Python 之禪,因此我們說函式式編程的部分不夠“Python”。

另一個常被提及的地方就是lambda。在Python中,lambda函式就是個普通的函式。lambda只是個語法糖。這兩者是等價的:

foo = lambda a: 2

def foo(a):
  return 2

普通的函式能做到一切 lambda 能做到的事情,但反過來卻不行。lambda 不能完成普通函式能完成的一切事情。

關於為何函式式編程不適合Python生態系統曾有過一次討論。你也許註意到,我之前提到了串列解析式,我們現在就來介紹下什麼是串列解析式。

串列解析式

之前我說過,任何能用映射或過濾完成的事情都可以用串列解析式完成。這就是我們要學的東西。

串列解析式是 Python 生成串列的方式。語法如下:

[function for item in iterable]

要想求串列中每個數字的平方,可以這麼寫:

print([x * x for x in [1, 2, 3, 4]])

可以看到,我們給串列中的每個元素應用了一個函式。那麼怎樣才能實現過濾呢?先來看看之前的這段代碼:

x = range(-5, 5)

all_less_than_zero = list(filter(lambda num: num print(all_less_than_zero)

可以將它轉換成下麵這種使用串列解析式的方式:

x = range(-55)

all_less_than_zero = [num for num in x if num 0
]

像這樣,串列解析式支持 if 陳述句。這樣就不需要寫一堆函式來實現了。實際上,如果你需要生成某種串列,那麼很有可能使用串列解析式更方便、更簡潔。

如果想求所有小於 0 的數字的平方呢?使用 Lambda、映射和過濾可以寫成:

x = range(-55)

all_less_than_zero = list(map(lambda numnum * num, list(filter(lambda numnum 0
, x))))

看上去似乎很長,而且有點複雜。用串列解析式只需寫成:

x = range(-55)

all_less_than_zero = [num * num for num in x if num 0
]

不過串列解析式只能用於串列。映射和過濾能用於一切可迭代物件。那為什麼還要用串列解析式呢?其實,解析式可以用在任何可迭代的物件上。

其他解析式

可以在任何可迭代物件上使用解析式。

任何可迭代物件都可以用解析式生成。從 Python 2.7 開始,甚至可以用解析式生成字典(哈希表)。

# Taken from page 70 chapter 3 of Fluent Python by Luciano Ramalho

DIAL_CODES = [
    (86'China'),
    (91'India'),
    (1'United States'),
    (62'Indonesia'),
    (55'Brazil'),
    (92'Pakistan'),
    (880'Bangladesh'),
    (234'Nigeria'),
    (7'Russia'),
    (81'Japan'),
    ]

>>> country_code = {country: code for code, country in DIAL_CODES}
>>> country_code
{'Brazil'55'Indonesia'62'Pakistan'92'Russia'7'China'86'United States'1'Japan'81'India'91'Nigeria'234'Bangladesh'880}
>>> {code: country.upper() for country, code in country_code.items() if code 66
}
{1'UNITED STATES'7'RUSSIA'62'INDONESIA'55'BRAZIL'}

只要是可迭代物件,就可以用解析式生成。我們來看個集合的例子。如果你不知道集合是什麼,可以先讀讀這篇(https://medium.com/brandons-computer-science-notes/a-primer-on-set-theory-746cd0b13d13)文章。簡單來說就是:

  • 集合是元素的串列,但串列中沒有重覆的元素

  • 元素的順序不重要

# taken from page 87, chapter 3 of Fluent Python by Luciano Ramalho

>>> from unicodedata import name
>>> {chr(i) for i in range(32256if 'SIGN' in name(chr(i), '')}
{'×''¥''°''£''©''#''¬''%''µ''>''¤''±''¶''§'', '=''®''

可以看到,集合使用字典同樣的大括號。Python非常聰明。它會查看你是否在大括號中提供了額外的值,來判斷是集合解析式還是字典解析式。如果想瞭解更多關於解析式的內容,可以看看這個可視化的指南(http://treyhunner.com/2015/12/python-list-comprehensions-now-in-color/)。如果想瞭解更多關於解析式和生成器的內容,可以讀讀這篇文章(https://medium.freecodecamp.org/python-list-comprehensions-vs-generator-expressions-cef70ccb49db)。

結論

函式式編程很美、很純凈。函式式代碼可以寫得非常乾凈,但也可以寫得很亂。一些 Python 程式員不喜歡在 Python 中使用函式式的模型,不過大家可以根據自己的喜好,記得用最好的工具完成工作。

(完)

看完本文有收穫?請轉發分享給更多人

關註「Python那些事」,做全棧開發工程師

推薦閱讀(點擊標題可跳轉閱讀)


全面深入理解 Python 面向物件

12步輕鬆搞定 Python 裝飾器

賊好理解,這個專案教你如何用百行代碼搞定各類NLP模型

神經網絡入門

赞(0)

分享創造快樂