1. 程式人生 > >10分鐘快速入門Python函數語言程式設計

10分鐘快速入門Python函數語言程式設計

640?wx_fmt=jpeg

作者 | Brandon Skerritt

譯者 | 王天宇、琥珀

編輯 | 琥珀

出品 | AI科技大本營

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

▌程式設計式函式

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

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

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

a = 3

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(38):

    print(c)

輸出結果為:

3
4
5
6
7
8

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

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

map(function, iterable)

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

[12345]

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

x = [12345]

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 = [12345]

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

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

▌Reduce

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

reduce(function, list)

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

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

product = 1

x = [1234]

for num in x:

    product = product * num

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

from functools import reduce


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

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

▌Filter

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

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

語法如下:

filter(function, list)



案例:(不使用 filter)

x = range(-55)

new_list = []



for num in x:

    if num < 0:

        new_list.append(num)



(使用 filter)

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

▌偏函式應用

偏函式應用(又叫閉包)有點難理解,但超級酷。你可以呼叫一個函式而無需提供它所需要的全部引數。先來看看這個例子:我們想建立一個函式,傳入的兩個引數分別是 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(21001):

  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 [1234]])

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

x = range(-55)



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

print(all_less_than_zero)



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

x = range(-55)



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



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

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

x = range(-55)



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



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

x = range(-55)



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(32256if 'SIGN' in name(chr(i), '')}

{'×''¥''°''£''©''#''¬''%''µ''>''¤''±''¶''§''<''=''®''$''÷''¢''+'}



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

▌總結

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

--【完】--