10分鐘快速入門Python函數式編程!保證你絕對能學會!

本文,你會了解到什麼是函數式編程,以及如何用 Python 進行函數式編程。你還會了解到列表解析和其他形式的解析。

▌編程式函數

10分鐘快速入門Python函數式編程!保證你絕對能學會!

在命令式編程中,你需要給計算機一系列任務,然後計算機會一一執行。在執行過程中,計算機可以改變其狀態。舉個例子,假設你將 A 的初始值設為 5,接下來你還可以改變 A 的值。在變量內部值變化的層面來講,你可以掌控這些變量。

在函數式編程中,你無需告訴計算機去做什麼,而是為它提供一些必要的信息。如什麼是一個數字的最大公約數,1 到 n 的乘積是多少等等。

由於這樣,變量就無法改變了。一旦你設置了一個變量,它就會永遠保持初始狀態(注意:在純函數式語言中,它們不叫作變量)。因此在函數式編程中,函數不會產生“副作用”。“副作用”是指函數可能會修改外部變量的值。讓我們通過一個典型的 Python 例子來看一下:

a = 3

私信小編007即可獲取數十套PDF哦!

def some_func():

global a

a = 5

some_func()

print(a)

這段代碼的輸出結果是5。在函數式編程中,改變變量是大忌,而且讓函數改變外部變量也是絕對禁止的。函數唯一能做的事是執行計算然後返回結果。

現在你可能在想:沒有變量,就沒有副作用嗎?為什麼這麼做很管用?好問題,下面我們簡單講一下這個問題。

如果一個函數伴隨著相同參數被調用兩次,它一定會返回一樣的結果。如果你對數學上的函數有所瞭解,你就會理解這裡的意義,這被稱作引用透明性。因為函數沒有副作用,如果你創建了一個可以執行計算的程序,你就可以使該程序提升性能。如果程序知道 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

若要理解 map,我們要先看看 iterable 是什麼。iterable 指一類可以進行迭代的對象。通常來看,它們是列表或數組,但 Python 有許多不同類型的 iterable。你甚至可以創建自己的 iterable 對象,來執行各種魔術方法 (magic method)。魔術方法可以是一個 API,來使你的對象更加 Pythonic。你需要用兩個魔術方法來使對象成為 iterable:

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__ ,或者說 "dunder"(指以雙下劃線 “__” 作為名字開頭和結尾的方法),返回了迭代對象,這常常也被當做循環的開端。dunder 方法接下來會返回下一個對象。

讓我們快速查看一下終端會話的結果:

for c in Counter(3, 8):

print(c)

輸出結果為:

3

4

5

6

7

8

在 Python 中,迭代器指只包含一個魔術方法 __iter__ 的對象。這意味著你可以訪問該對象的任何部分,但不能對其循環訪問。有些對象包含魔術方法 __next__,以及除了 __iter__ 以外的魔術方法,如 sets(下文會進行詳細討論)。在本文中,假定我們涉及的所有東西都是可迭代的對象。

那麼現在我們知道了什麼是可迭代對象,再回頭看一下 map 函數。map 函數可以讓我們在同一個 iterable 對象中,把函數作用在每一個元素上。我們通常將函數作用於列表中的每個元素,但這對大多數 iterable 對象也是可行的。Map 需要兩個輸入,分別是要執行的函數和 iterable 對象。

map(function, iterable)

假設我們有一個如下的數字列表:

[1, 2, 3, 4, 5]

然後計算每個數字的平方,我們可以寫下面一段代碼:

x = [1, 2, 3, 4, 5]

def square(num):

return num*num

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

Python 中的函數式函數也有“懶”的特性。如果我們不引入 "list()",那函數就會存取 iterable 對象,而不是存取列表本身。我們需要明確告訴 Python 程序 “將其轉換成列表” ,從而供我們使用。

聽起來可能有點奇怪,我們對 Python 的評價從 “一點也不懶” 突然轉變到 “懶”。如果你對函數式編程的感悟勝過指令式編程,最終你會習慣這種轉變的。

現在我們可以很容易寫出一個像 "square(num)" 這樣的函數了,但看起來不太合適。我們有必要定義一個函數僅僅為了在 map 中調用它一次嗎?好吧,我們可以基於 lambda 在 map 中定義一個函數。

▌Lambda表達式

lambda 表達式是一個單行的函數。以一個計算數字平方的 lambda 表達式為例:

square = lambda x: x * x

現在執行這行代碼:

>>> square(3)

9

我已經聽見你在問了,參數在哪?這到底是怎麼回事?它看起來並不像個函數?

這可能有點讓人困擾,但可以解釋得清楚。首先我們給變量 "square"賦一個值,例如:

lambda x:

我們告訴 Python 這是一個 lambda 函數,且輸入值為 x。冒號後面的部分代表對輸入要做的事情,然後它就會返回得到的結果。

我們可以將該計算平方值的程序簡化成一行:

x = [1, 2, 3, 4, 5]

print(list(map(lambda num: num * num, x)))

由此可見,在一個 lambda 表達式中,所有參數都在左邊,你想要對其執行的指令都在右邊。不得不承認這樣看起來有點雜亂。實際上這是一種很受歡迎的編程方式,只有其他的函數式程序員可以讀懂代碼。同時,把一個函數轉化成單行表達式真的很酷。

▌Reduce

Reduce 是將 iterable 轉換成一個結果的函數。通常用作來對一個列表進行計算,將其縮減為一個數字。如下:

reduce(function, list)

我們可以將 lambda 表達式用作函數,事實上我們通常也是這麼做的。

一個列表的乘積為每個單獨的數字相乘在一起的結果。你可以通過如下程序實現:

product = 1

x = [1, 2, 3, 4]

for num in x:

product = product * num

但基於 reduce 你可以將上面程序寫作:

from functools import reduce

product = reduce((lambda x, y: x * y),[1, 2, 3, 4])

得到的乘積結果是一樣的。基於對函數式編程的理解,代碼量減小了,代碼也變得更簡潔。

▌Filter

filter 函數用於傳入一個 iterable,並過濾掉這個 iterable 中所有你不想要的序列。

通常,filter 函數傳入一個函數和一個列表。將該函數作用在列表中的任意一個元素上,如果該函數返回 True,不做任何事情。如果返回 False,將該元素從列表中刪除。

語法如下:

filter(function, list)

案例:(不使用 filter)

x = range(-5, 5)

new_list = []

for num in x:

if num < 0:

new_list.append(num)

(使用 filter)

x = range(-5, 5)

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, [1, 2, 3]))

# 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 中的所有函數都是第一類且可以作為高階函數使用。

▌偏函數應用

偏函數應用(又叫閉包)有點難理解,但超級酷。你可以調用一個函數而無需提供它所需要的全部參數。先來看看這個例子:我們想創建一個函數,傳入的兩個參數分別是 base 和 exponet,然後返回 base 的 exponent 次方。如下:

def power(base, exponent):

return base ** exponent

現在,我們想要一個專用的平方函數,使用 power 函數得到一個數字的平方。如下:

def square(base):

return power(base, 2)

這個方式可以,但如果我們想要一個三次方函數呢?或者是四次方?我們能一直這樣寫嗎?當然,你是可以的。但程序員可沒那麼勤快。如果你一遍又一遍地重複做一件事,那麼你就需要用一種更高效率的方式做事,無需重複。因此,我們採用了閉包的方法。以下是一個採用閉包的平方函數的例子:

from functools import partial

square = partial(power, exponent=2)

print(square(2))

# output is 4

這樣是不是很酷!通過告訴 Python 第二個參數是什麼,我們只用一個參數就能調用需要兩個參數的函數。

我們還能用一個 loop,產生一個乘方函數以實現從三次方到 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相匹配

你可能已經注意到了,我們想要在函數式編程中完成的事情都會列表相關。除了 reduce 函數和偏函數應用外,所有你看到的函數都會產生列表。Python 之父 Guido 不喜歡 Python 當中的函數式編程部分,因為 Python 已經產生自己列表的方式。

如果你在 Python 的命令執行環境中輸入「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 環境中,map&filter 可以實現列表解析式同樣的事情。這個打破了 Python 的一條規則,於是這部分的函數式編程看起來不那麼 Pythonic了。

另一個需要討論的是 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]])

這樣,可以看到如何將一個函數作用於列表中的每一個元素。如果利用 filter 呢?請先看下此前出現過的代碼:

x = range(-5, 5)

all_less_than_zero = list(filter(lambda num: num < 0, x))

print(all_less_than_zero)

然後將其轉換成列表解析式:

x = range(-5, 5)

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

列表解析式支持這樣的 if 表達式。你不需要通過上百萬個函數最終得到你想要的。

那麼如果想要將列表中的所有數字進行平方呢?採用 lambda、map 、 filter 你會這麼寫:

x = range(-5, 5)

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

這樣看起來相當冗長且複雜。如果用列表解析式只需要這樣:

x = range(-5, 5)

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

▌其他解析式

你可以創建任何一個 iterable 的解析式。

通過解析式可產生任何一個 iterable。從 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'}

如果這是一個 iterable,那麼就能實現。我們最後來看一個關於集合(sets)的例子。

  • 集合是元素列表,且沒有重複出現兩次的元素。
  • 集合的排序無關緊要。

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

>>> from unicodedata import name

>>> {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i), '')}

{'×', '¥', '°', '£', '©', '#', '¬', '%', 'µ', '>', '¤', '±', '¶', '§', '

你可能會注意到集合具有和字典中一樣的花括號。這就在於 Python 的智能性,它會根據你是否提供了額外的值以判斷你寫的是 dictionary comprehension 還是 set comprehension。

▌總結

函數式編程是優雅而簡潔的。函數式代碼可以非常簡潔,但也可以非常凌亂。一些 Python 程序員不喜歡用 Python 函數式解析。因此,你應該用你想用的,用最好的工具完成任務。


分享到:


相關文章: