前言
什么是函數式編程?
一句話總結:函數式編程(functional programming)是一種編程范式,之外還有面向對象(OOP)、面向過程、邏輯式編程等。
函數式編程是一種高度抽象的編程范式,它倡導使用純函數
,即那些不依賴于外部狀態
、沒有可變狀態
的函數。在純粹的函數式編程語言中,函數的輸出完全由輸入決定,因此相同的輸入總是產生相同的輸出,這樣的函數我們稱之為無副作用的
。
🔊 一個顯著的函數式編程特性是,函數可以作為參數傳遞給其他函數,或者作為結果被返回,這為編程帶來了額外的靈活性和表達力!
Python 提供了對函數式編程的部分支持。
雖然 Python 允許使用變量,使其不完全符合純函數式編程的標準,但它融合了函數式編程的一些元素,允許開發者在需要時采用函數式編程技術。
函數式編程特點
函數式編程關注的是:describe what to do, rather than how to do it。
圍繞這一關鍵,函數式編程一般具備的特點
主要有:
- 函數享有
一等公民
的地位,可以被賦值給變量、作為參數傳遞給其他函數,或作為函數的返回值。 - 函數是
引用透明
的,意味著它們的結果僅由輸入參數決定,不依賴于外部變量,更不易出錯。 - 每個輸入參數唯一對應一個輸出結果,確保了函數的
確定性
。 - 函數應
避免產生副作用
,如修改全局狀態或依賴外部狀態變化。 遞歸是函數式編程中常用的控制結構
,用于替代傳統的循環結構。遞歸的精髓是描述問題,而這正是函數式編程的精髓。
函數式編程不依賴于外部變量,而是返回一個新的值給你,所以沒有任何副作用。即保證每次輸入的值不變,輸出的值一定也不會發生改變。
🔊 接下來我們從以上幾方面切入,探討如何增強Python代碼的函數式編程技術。
遞歸(Recursion)
函數式編程傾向于采用遞歸而非循環來解決問題
,這一方法不僅能夠清晰地表達問題的本質
,還賦予了代碼一種簡潔而優雅的美感。
那什么是遞歸呢?
這就是遞龜??
開個玩笑~
言歸正傳,遞歸函數是指在函數的定義中調用自身的函數。
遞歸通常由兩個部分組成:
- 基準情況(Base Case):遞歸終止條件,避免無限遞歸。
- 遞歸情況(Recursive Case):函數調用自身以解決較小規模的問題。
示例1:快速排序
分而治之:快速排序可以使用遞歸實現,代碼提供清晰的自解釋性。
def pure_quick_sort(arr):"""使用純快速排序算法對列表進行排序。該算法選擇列表中的一個元素作為基準(pivot),將列表分為三部分:1. 小于基準的元素;2. 等于基準的元素;3. 大于基準的元素。然后對小于和大于基準的部分分別遞歸調用排序函數。參數:arr: 待排序的列表。返回值:排序后的列表。"""if len(arr) <= 1:return arrpivot = arr[len(arr) // 2]left = [x for x in arr if x < pivot]middle = [x for x in arr if x == pivot]right = [x for x in arr if x > pivot]return pure_quick_sort(left) + middle + pure_quick_sort(right)# 調用純快速排序函數對一組數字進行排序并打印結果
numbers = pure_quick_sort([11, 1, 3, 9, 7, 6, 8, 5, 10, 2, 4])
print(numbers)
示例2:二叉樹遍歷
避免復雜的循環邏輯:二叉樹的深度優先遍歷(前序遍歷)可以使用遞歸實現。
#! -*-conding: UTF-8 -*-class TreeNode:"""二叉樹節點類該類用于構建二叉樹的節點結構,每個節點包含一個值以及左右子節點的引用。"""def __init__(self, val=0, left=None, right=None):"""初始化節點值及左右子節點。參數:val: 節點的值,默認為0。left: 左子節點的引用,默認為None。right: 右子節點的引用,默認為None。"""self.val = valself.left = leftself.right = rightdef preorder_traversal(tree):"""前序遍歷二叉樹。該函數遞歸地遍歷二叉樹,并按照“根-左-右”的順序打印節點的值。參數:tree: 二叉樹的根節點。"""if tree:print(tree.val, end=' ')preorder_traversal(tree.left)preorder_traversal(tree.right)# 創建二叉樹
# 創建一個二叉樹
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
root.left.left = TreeNode(4)
root.left.right = TreeNode(5)# 進行前序遍歷
# 前序遍歷
preorder_traversal(root) # 輸出:1 2 4 5 3
遞歸通過將復雜問題分解為更小、更易于管理的子問題,極大地簡化了編程邏輯,使得解決方案更加直觀和易于理解。
不變性(Immutability)
在函數式編程范式中,數據的不可變性是一項核心原則:一旦數據被創建,它便是只讀的,其狀態在整個生命周期內保持恒定,不會被重新賦值。
函數式編程鼓勵不可變性,盡量減少或避免可變狀態和副作用。這可以通過使用元組、凍結集合和不可變對象等來實現。
避免可變默認參數
默認參數值應該是不可變的,或者使用None,并在函數內部設置默認值。
def append_to_list(element, my_list=None):if my_list is None:my_list = []my_list.append(element)return my_list
使用不變數據類
例如:使用@dataclass
裝飾器時,可以結合frozen=True
參數來創建不可變的數據類。
from dataclasses import dataclass@dataclass(frozen=True)
class MyDataClass:"""不可變數據類。這個類使用了 @dataclass 裝飾器,并設置了 frozen=True,表明這個類是不可變的,即創建后其屬性不能被修改。屬性:attribute: 類的唯一屬性,類型為整型(int)。"""attribute: int
返回新對象而不是修改原始對象
當需要修改數據時,創建一個新的對象,而不是修改現有的對象。
def new_list(original_list):return original_list + [1]# 測試
original = [1, 2, 3]
print("Original:", original) # 輸出原始列表new = new_list(original)
print("New:", new) # 輸出新列表# 檢查原始列表是否被修改
print("Original after new_list call:", original)
這種不變性原則大大減少了副作用的發生,因為變量一旦被設定,其值就不會改變。它還簡化了并發編程的復雜性,因為不必擔心多個線程或進程間的數據競爭問題。此外,不可變性提高了程序的可預測性和可靠性,因為程序狀態的變更是清晰和可控的,從而更容易進行推理和維護。
純函數(Pure Functions)
純函數定義了一種優雅的計算境界:它們
在給定相同輸入值的情況下,始終如一地產生相同的輸出結果,并且在整個執行過程中,對程序的狀態或全局變量秋毫無犯,不產生任何副作用
。
例如,下面是一個非純函數
:
# 假設這是一個全局變量,用于存儲計數狀態
counter = 0def increment():global countercounter += 1return counter# 使用示例
print(increment()) # 輸出: 1
print(increment()) # 輸出: 2
純函數:
def increment(counter):new_counter = counter + 1return new_counter# 使用示例
print(increment(0)) # 輸出: 1
print(increment(0)) # 輸出: 1
在Python中,非純函數通常指的是那些除了接受輸入參數外,還會依賴或修改外部狀態的函數。這些函數的輸出不僅取決于輸入參數,還可能受到外部狀態的影響,因此相同的輸入在不同時間或不同環境下可能會產生不同的輸出。
另外:lambda表達式
中不能包含賦值語句,所以它總為純函數,適?于函數式編程。
# 一個簡單的純函數lambda表達式,計算兩個數字的和
custom_add = lambda x, y: x + y# 調用lambda表達式
result = custom_add(5, 3)print(result) # 輸出: 8
函數式編程的準則:不依賴于外部的數據,而且也不改變外部數據的值,而是返回一個新的值給你。
純函數的特性不僅使純函數的行為可預測,而且極大地提升了它們的可理解性、可測試性,以及在并行計算環境中的適用性。純函數的這些優勢,讓它們成為構建可靠、高效和可維護軟件系統的基石。
前面的快速排序是不是純函數呢?不是的話怎么修改呢?
結構化模式匹配(Pattern Matching)
Python沒有內建的模式匹配語法,但可以使用
match-case
語句(Python 3.10+)。
示例1:快速排序
結構化模式匹配使條件更清晰,代碼更具聲明性。
from typing import Listdef quick_sort(data: List[int]) -> List[int]:match data:case [] | [_]: # 匹配空列表或只有一個元素的列表return data.copy() # 避免非純函數case [pivot, *rest]: # 匹配至少有兩個元素的列表,pivot 是第一個元素,rest 是剩余元素left = [x for x in rest if x <= pivot] # 將小于等于 pivot 的元素放入 left 列表right = [x for x in rest if x > pivot] # 將大于 pivot 的元素放入 right 列表return quick_sort(left) + [pivot] + quick_sort(right) # 遞歸排序 left 和 right,然后合并結果# 使用示例
list2 = quick_sort([1, 3, 5, 2, 33, 23])
print(list2)
示例2:二叉樹遍歷
from dataclasses import dataclass@dataclass
class TreeNode:val: int = 0left: 'TreeNode' = Noneright: 'TreeNode' = Nonedef preorder_traversal(tree: TreeNode):match tree:case None:return # 空樹,什么也不做case TreeNode(val, left, right):print(val, end=' ')preorder_traversal(left)preorder_traversal(right)# 創建一個二叉樹
root = TreeNode(val=1)
root.left = TreeNode(val=2)
root.right = TreeNode(val=3)
root.left.left = TreeNode(val=4)
root.left.right = TreeNode(val=5)# 前序遍歷
preorder_traversal(root) # 輸出:1 2 4 5 3
高階函數(Higher-order Functions),函數是一等公民
高階函數是一種強大的編程構造,它不僅
能夠接受其他函數作為輸入參數,還能將函數作為結果返回
。這種獨特的能力使得函數本身可以像數據一樣被傳遞、操作和組合,從而為編程帶來了無與倫比的靈活性和表現力。
Python提供了多個內置的高階函數,這些函數可以接受其他函數作為參數或返回函數作為結果。以下是一些常見的Python內置高階函數:
-
map(func, *iterables):
map
函數接受一個函數和一個或多個可迭代對象,將函數應用于每個元素,并返回一個新的迭代器。 -
filter(func, iterable):
filter
函數接受一個函數和一個可迭代對象,函數返回布爾值。filter
創建一個迭代器,包含所有使得函數返回True的元素。 -
reduce(func, iterable[, initializer]):
reduce
函數位于functools
模塊中,它接受一個函數和一個可迭代對象,將函數累積地應用到元素上,返回一個單一的結果;如果提供了initializer
,則作為初始累積值。 -
all(iterable):
all
函數接受一個可迭代對象,如果所有元素都為True
(或都非零、非空等),則返回True
。 -
any(iterable):
any
函數接受一個可迭代對象,如果至少有一個元素為True
,則返回True
。 -
sorted(iterable, *, key=None, reverse=False):
sorted
函數接受一個可迭代對象,返回一個新的排好序的列表。可以通過key
參數指定一個函數,用于從每個元素中提取比較鍵。 -
enumerate(iterable, start=0):
enumerate
函數接受一個可迭代對象,返回一個包含元素及其索引的迭代器。 -
zip(*iterables):
zip
函數接受一個或多個可迭代對象,返回一個由元組組成的迭代器,每個元組包含來自每個可迭代對象的對應元素。 -
lambda arguments: expression:
lambda
是一個匿名函數的聲明方式,它可以接受任意數量的參數,并返回單個表達式的值。 -
functools.partial(func, /, *args, **keywords):
partial
函數來自functools
模塊,它返回一個被調用函數的分區版本,該版本已經用給定參數和關鍵字參數進行了預填充。 -
functools.lru_cache(maxsize=128, typed=False):
lru_cache
是一個裝飾器,可以將函數的結果緩存起來,以加快重復調用的速度。 -
itertools.chain(*iterables):
chain
函數來自itertools
模塊,它接受一系列可迭代對象,并返回一個迭代器,該迭代器將所有輸入的可迭代對象串聯起來。 -
itertools.starmap(function, iterable):
starmap
函數接受一個函數和一個可迭代對象,該可迭代對象的元素是多個參數的序列。starmap
將這些序列解包并應用函數。
內置的高階還有很多,他們是Python函數式編程的補充和關鍵,使代碼更加簡潔、靈活和表達力強。
Python的
itertools
、functools
、operator
模塊中定義了很多?階函數。
示例1,max 函數的應用
Python 的max
函數是一個內置函數,用于返回給定參數中的最小值。它可以處理多種類型的參數,包括數字、字符串、元組等,并且可以與一個可選的 key 函數一起使用,以自定義比較的邏輯。
# 根據長度找出最長的字符串
words = ['apple', 'banana', 'grape', 'cherry']
longest_word = max(words, key=len)
print(longest_word) # 輸出: 'banana'# 從Python 3.4開始,max 函數接受一個 default 參數,如果你提供了一個空的可迭代對象,max 將返回 default 參數的值。
empty_list = []
result = max(empty_list, default='No items available')
print(result) # 輸出: 'No items available'list1 = [4, 24, 3]
list2 = [4, 5, 6]
max_from_both = max(list1, list2)
print(max_from_both) # 輸出: [4, 24, 3]
示例2,map 函數的應用
Python 的map
函數是一種高階函數,它接受一個函數和一個或多個可迭代對象作為參數,將指定的函數應用于可迭代對象的每個元素,并返回一個新的迭代器。map
函數在函數式編程中非常有用,因為它可以簡潔地表達對集合的轉換操作。
厭倦了常規循環?試試 map() 函數,讓數據處理更加簡潔高效:
# 將平方函數應用于列表中的每個元素
numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x ** 2, numbers)# map 返回的是一個map對象,可以使用list轉換為列表
print(list(squared_numbers)) # 輸出: [1, 4, 9, 16, 25]# 將加法函數應用于兩個列表的對應元素
list1 = [1, 2, 3]
list2 = [4, 5, 6]
added = map(lambda x, y: x + y, list1, list2)print(list(added)) # 輸出: [5, 7, 9]
示例3,reduce 函數的應用
reduce
函數位于functools
模塊中,它接收一個函數和一個序列,將函數累積地應用到序列的元素上,返回一個單一的結果。reduce
可以用于計算序列的累積值,如求和、乘積等。
積微成著,力聚無窮。
from functools import reducenumbers = [1, 2, 3, 4]
result = reduce(lambda x, y: x + y, numbers)
print(result) # 輸出: 10
示例4,filter 函數的應用
filter
函數接收一個函數和一個序列,函數返回布爾值。filter
創建一個迭代器,包含所有使得傳入函數返回True
的元素。
有了filter(),你可以輕松地從一堆數據中挑出符合條件的寶藏:
numbers = [1, 2, 3, 4, 5]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # 輸出: [2, 4]
示例5,偏函數(partial)的應用
Python 的functools模塊
中包含了一個名為partial
的函數,它用于創建一個新的函數,這個新函數是原有函數的變體,可以預設原有函數的一些參數值,而其他參數則在調用時傳遞。
定制函數,輕松調用:
# -*- coding:utf-8 _*-
# __author__:lianhaifeng
from functools import partialdef power(base, exponent):return base ** exponent# 創建一個新函數,將 exponent 參數預設為 2
square = partial(power, exponent=2)# 使用新函數
result = square(base=3) # 相當于 power(3, 2),返回 9
print(result)# 繼續創建一個新函數,將 base 參數預設為 2
square_two = partial(power, base=2)# 使用新函數
result_two = square_two(exponent=3) # 相當于 power(2, 3),返回 8
print(result_two)
示例6:使?Python匿名函數
Python 中的匿名函數,也稱為lambda
函數,是一種簡潔的定義函數的方法,它允許你在一個語句中快速創建函數。這種函數沒有名稱,因此被稱為“匿名”。
當你需要一個小巧的函數,而又不想大費周章定義時,lambda閃亮登場。
# 使用 lambda
get_first_lambda = lambda x: x[0]
print(get_first_lambda((1, 2, 3))) # 輸出: 1
示例7:使用operator模塊替代匿名函數
模塊
operator
的作?是簡化一些簡單的匿名函數。例如:可以使?operator.add
?法代替add=lambda a, b: a+b
?法等。
大G都有了,還自己造啥自行車啊!
# 使用 itemgetter
from operator import itemgetter
get_first = itemgetter(0)
print(get_first((1, 2, 3))) # 輸出: 1
示例8:zip函數的應用
zip()
函數是 Python 中的一個內置函數,它接受任意數量的可迭代對象作為輸入,然后將這些可迭代對象中的對應元素打包成元組,從而創建一個元組的迭代器。
# 當提供的可迭代對象長度不相等時,zip() 函數會以最短的那個為準。多余的元素會被忽略
names = ['海鴿', 'Alice', 'Bob', 'Charlie']
ages = [18, 24, 30]zipped = zip(names, ages)
print(list(zipped)) # 輸出: [('海鴿', 18), ('Alice', 24), ('Bob', 30)]# 可以將配對的元素解壓縮回原來的序列
paired = [(1, 'a'), (2, 'b'), (3, 'c')]
unzipped = zip(*paired) # 解壓縮
print(list(unzipped)) # 輸出:[(1, 2, 3), ('a', 'b', 'c')]# 如果你需要保留所有元素,即使某些可迭代對象比其他的短,你可以使用 itertools.zip_longest() 函數,它會填充缺失的值
from itertools import zip_longestzipped = zip_longest(names, ages, fillvalue=18)
print(list(zipped)) # 輸出: [('海鴿', 18), ('Alice', 24), ('Bob', 30), ('Charlie', 18)]
示例9: 方法的偏函數partialmethod
Python3.4開始添加了partialmethod
函數,作用類似于partial
函數,但僅作用于方法。
functools.partialmethod
是 Python 的functools
模塊中提供的一種特殊工具,用于創建部分應用方法(即綁定默認值給方法的一部分參數)。這個特性在類的方法中特別有用,允許你在類的上下文中創建方法的變體,其中某些參數已經被預設。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:海哥Pythonfrom functools import partialmethodclass Logger:"""使用functools.partialmethod實現日志記錄器"""def log(self, level, message):print(f"[{level}] {message}")info = partialmethod(log, 'INFO')warning = partialmethod(log, 'WARNING')error = partialmethod(log, 'ERROR')logger = Logger()
logger.info("This is an informational message.")
logger.warning("This is a warning message.")
logger.error("This is an error message.")
通過高階函數,我們可以構建更加抽象和強大的代碼結構,它們不僅易于復用,而且能夠以聲明式
的方式表達復雜的邏輯,讓代碼更加簡潔和富有表現力。
函數式編程的代碼更注重描述要干什么,而不是怎么干,這種聲明式的具有更強的自解釋性,使代碼更易讀,即:describe what to do, rather than how to do it。
柯里化 Currying
柯里化(Currying)是一種將函數轉換的技術,在函數式編程中非常常見。它涉及到將一個接受多個參數的函數轉換為一系列嵌套的函數,每個函數只接受一個參數。這種轉換允許函數調用時可以逐步提供參數,而不是一次性提供所有參數。
具體來說,柯里化將一個函數f(a, b, c)
轉換為一個形式為f(a)(b)(c)
的函數,其中每個括號內的函數調用只接收原函數的一個參數。這樣做的好處包括:
- 靈活性:函數可以被部分應用,意味著可以先傳遞一些參數,稍后再傳遞剩余的參數。
- 重用:部分應用的函數可以被多次使用,每次只需傳遞剩余的參數即可,這有助于代碼的復用。
- 組合:柯里化的函數更容易與其他函數組合,構建更復雜的函數。
柯里化得名于邏輯學家哈斯凱爾·加里(Haskell Curry),但實際上是由 Moses Sch?nfinkel 和戈特洛布·弗雷格(Gottlob Frege)首先提出的。在現代編程語言中,如 Haskell、JavaScript 和 Python(使用第三方庫如toolz
或內置函數如functools.partial
)中,柯里化是一個常見的概念和編程技術。
使用functools.partial
實現柯里化
例如,在 Python 中,使用functools.partial
可以實現類似柯里化的效果:
from functools import partialdef add(a, b):return a + badd_5 = partial(add, 5) # 這里實現了部分應用,add_5 現在是一個只接受一個參數的函數
print(add_5(10)) # 輸出: 15
然而,真正的柯里化會涉及函數的連續調用,如下所示:
def curried_add(a):def inner(b):return a + breturn inneradd_5 = curried_add(5)
print(add_5(10)) # 輸出: 15
或者在一個函數中實現完整的柯里化:
def curried_add(a):return lambda b: a + badd_5 = curried_add(5)
print(add_5(10)) # 輸出: 15
在上述例子中,curried_add
函數接受一個參數a
并返回一個新的函數,這個新函數接受參數 b 并返回a + b
的結果。這就是柯里化的基本思想。
使用toolz.curry
實現柯里化
toolz
是一個 Python 庫,它提供了函數式編程工具,包括 curry 函數,用于創建柯里化(Currying)的函數。
柯里化是指將多參數函數轉換為一系列單參數函數的過程。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:Python
# __time__:2024/7/7from toolz import curry# 使用裝飾器方式柯里化函數
@curry
def multiply(a, b):return a * b# 使用 curry() 函數柯里化函數
multiply_curried = curry(lambda a, b: a * b)# 一次性傳遞所有參數
print(multiply(2, 3)) # 輸出 6
print(multiply_curried(2, 3)) # 輸出 6# 逐步傳遞參數
double = multiply(2) # 創建一個新函數,固定a為2
result = double(3) # 調用新函數,結果為6
print(result)# 或者
triple = multiply_curried(3) # 創建一個新函數,固定a為3
result = triple(2) # 調用新函數,結果為6
print(result)
這樣,multiply
和multiply_curried
就可以作為柯里化函數使用,允許你以靈活的方式調用它們。
toolz 庫中的 curry 功能與 Python 標準庫 functools 中的 partial 功能都是為了使函數調用更加靈活,但是它們之間有一些關鍵的區別:
- functools.partial 主要用于“綁定”函數的一部分參數,從而創建一個新的函數,這個新函數在調用時只需要傳入剩下的參數即可。
partial
不會改變函數的簽名,它只是預填充了一些參數。 - toolz.curry 實現的是柯里化(Currying),這是一種函數式編程的概念,它允許你將一個多參數函數轉化為一系列的單參數函數。這意味著你可以逐步應用參數,直到所有的參數都被提供后才進行計算。
- 應用場景:
partial
更適合需要固定某些參數的場景;curry
則更適合需要鏈式調用或逐步構建函數的情況。
裝飾器,為函數擴展額外的功能
把某個函數加上裝飾器后,就可以為函數擴展額外的功能
。本質上就是把函數作為參數傳遞的過程,如:
def log_decorator(func):def wrapper(*args, **kwargs):print(f"Calling function: {func.__name__}")result = func(*args, **kwargs)print(f"{func.__name__} returned {result}")return resultreturn wrapper@log_decorator
def add(a, b):return a + bprint(add(3, 4))# 輸出:
# Calling function: add
# add returned 7
# 7
函數組合(Function Composition)
函數組合是一種優雅的編程技巧,它通過串聯多個函數,形成一個工作流,從而生成一個全新的函數。這種技巧涉及將一個函數的輸出直接"傳遞"給下一個函數作為輸入,一層接一層,像搭積木一樣構建起復雜的處理邏輯。
嵌套函數調用,簡單pipeline
實現
函數組合通常通過將一個函數的輸出作為另一個函數的輸入來實現。在Python中,你可以使用嵌套函數調用來完成這一點。
def add_one(x):return x + 1def multiply_by_two(x):return x * 2# 組合兩個函數
def compose(f, g):return lambda x: f(g(x))# 創建一個組合函數,先執行 multiply_by_two,然后執行 add_one
composed_function = compose(add_one, multiply_by_two)# 使用組合函數
result = composed_function(3) # 相當于 (3 * 2) + 1
print(result) # 輸出: 7
更靈活的pipline
實現
如下程序,提供一種實現pipeline
效果更靈活、優雅的方案:
- 將列表元素先乘以
2
- 將列表元素再加
10
from typing import List, Callable
from functools import partial, reduce# 不咋地道的組合方式:
# def compose(*functions):
# def composed(value):
# for fn in functions:
# value = fn(value)
# print(value)
# print("---------")
# return value
#
# return composed# 定義一個類型別名,用于類型注解
Composable = Callable[[List[int]], List[int]]# 更地道的組合方式:
def compose(*functions: Composable) -> Composable:def apply(value: List[int], fn: Composable) -> List[int]:return fn(value)return lambda data: reduce(apply, functions, data)# 修改函數以只接受一個參數(列表)
def add_x(data: List[int], x: int) -> List[int]:print("add_x ...")# return list(map(lambda y: y + x, data))return [y + x for y in data] # 使用列表推導式def multiply_by_x(data: List[int], x: int) -> List[int]:print("multiply_by_x ...")# return list(map(lambda y: y * x, data))return [y * x for y in data] # 使用列表推導式# 使用 partial 預先綁定參數
multiply_by_2 = partial(multiply_by_x, x=2)
add_10 = partial(add_x, x=10)# 正確地組合函數
do_operations = compose(multiply_by_2, add_10) # 注意參數的順序resource_data = [1, 9, 3, 5, 2]
result = do_operations(resource_data)
print(result) # 輸出: [12, 28, 16, 20, 14]print("----------compose_right_to_left---------------")def compose_right_to_left(*functions):return compose(*reversed(functions))# 使用從右到左的 compose
do_operations_right_to_left = compose_right_to_left(multiply_by_2, add_10)result_right_to_left = do_operations_right_to_left(resource_data)
print(result_right_to_left) # 輸出: 22, 38, 26, 30, 24],這是先加 10 再乘以 2 的結果
shell風格的python pipeline
利用函數式編程,我們也能簡單實現shell
風格的python pipeline
。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:網友S142857
# __time__:2024/7/7
class Pipe(object):"""管道類,用于實現函數管道操作。通過將函數封裝在Pipe實例中,可以使用'|'操作符連接多個函數,形成一個處理管道。"""def __init__(self, func):"""初始化管道對象。參數:func - 要封裝的函數。"""self.func = funcdef __ror__(self, other):"""實現管道操作的重載操作符。參數:other - 要與當前Pipe實例連接的可迭代對象。返回:一個生成器,用于逐個處理other中的元素,并應用func函數。"""def generator():for obj in other:if obj is not None:yield self.func(obj)return generator()@Pipe
def even_filter(num):"""過濾器函數,保留偶數,過濾掉奇數。參數:num - 要檢查的數字。返回:如果num是偶數,則返回num;否則返回None。"""return num if num % 2 == 0 else None@Pipe
def multiply_by_three(num):"""數字乘以三的函數。參數:num - 要乘以3的數字。返回:num乘以3的結果。"""return num * 3@Pipe
def convert_to_string(num):"""將數字轉換為字符串的函數。參數:num - 要轉換的數字。返回:包含數字的字符串。"""return 'The Number: %s' % num@Pipe
def echo(item):"""打印項目的函數。參數:item - 要打印的項目。返回:item本身,用于管道繼續傳遞。"""print(item)return itemdef force(sqs):"""用于強制執行管道的函數。參數:sqs - 一個可迭代對象,通常是一個管道。該函數遍歷sqs,但不返回任何值,主要用于觸發管道中的操作。"""for _ in sqs:passnums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
#
force(nums | even_filter | multiply_by_three | convert_to_string | echo)# 管道操作的另一種用法,直接遍歷管道結果
# for _ in nums | even_filter | multiply_by_three | convert_to_string | echo:
# pass # 或者對輸出執行其他任何操作# 輸出:
# The Number: 6
# The Number: 12
# The Number: 18
# The Number: 24
# The Number: 30
函數式編程在一定程度上是關于構建一個通用的、可重用的、可組合的函數庫:
合理的利用函數組合進行函數式編程不僅可以使代碼保持模塊化,而且通過減少中間變量和冗余步驟,可以增強代碼的簡潔性和可讀性,提升代碼的復用性,讓邏輯的構建變得直觀而高效。
聲明式編程,而不是命令式編程
聲明式編程(Declarative Programming )描述"要做什么"而不是"怎么做",通常更易于維護和更新。
假設現在我們在寫這樣一個程序:有3輛車,擲篩子前進,共5輪,每輪每輛車都能擲篩子,每次有70%的概率向前移動一步的過程。經過5輪后,比賽結束并打印結果。
我們很容易就能寫出下面這樣的命令式編程代碼:
from random import randomtime = 5
car_positions = [1, 1, 1]while time:# decrease timetime -= 1print('')for i in range(len(car_positions)):# move carif random() > 0.3:car_positions[i] += 1# draw carprint('*' * car_positions[i])print(car_positions)
代碼是命令式編寫的,更多是講述了如何做。如果只掃一眼代碼,我們很難
直觀的知道他要做什么。
而使程序更具聲明式則是使代碼更具可讀性的一種很好的、低腦力的方法。
import copy
from random import randomdef move_car(position, move_chance):"""根據給定的概率移動汽車位置。"""return position + 1 if random() <= move_chance else positiondef draw_car(position):"""繪制汽車在當前位置的圖形表示。"""print('*' * position)def simulate_race(car_positions, move_chance, steps):"""模擬賽車比賽,打印每一步的結果。"""new_positions = copy.copy(car_positions)for _ in range(steps):new_positions = [move_car(pos, move_chance) for pos in new_positions]for pos in new_positions:draw_car(pos)print(new_positions)# 初始設置
MOVE_CHANCE = 0.7 # 70%的概率向前移動
STEPS = 5 # 總步數
CAR_POSITIONS = [1, 1, 1] # 初始位置# 運行模擬
simulate_race(CAR_POSITIONS, MOVE_CHANCE, STEPS)
修改后的代碼明顯更加清晰可讀。
遞歸的本質就是描述問題是什么。使用遞歸取代for
循環,某種程度下可以實現更純粹的函數式編程。
from random import randomdef move_cars(car_positions: list):return list(map(lambda x: x + 1 if random() > 0.3 else x, car_positions))def output_car(car_position: int):return '*' * car_positiondef run_step_of_race(state: dict):new_positions = move_cars(state['car_positions'])print(new_positions)return {'time': state['time'] - 1, 'car_positions': new_positions}def draw(state: dict):print(f"Time: {state['time']}")print('\n'.join(map(output_car, state['car_positions'])))def race(state):if state['time'] > 0:draw(state)race(run_step_of_race(state)) # Tail recursionrace({'time': 5, 'car_positions': [1, 1, 1]}) # 初始位置改為0
惰性求值(Lazy Evaluation),需要數據時才返回數據
在函數式編程中,惰性求值(Lazy Evaluation)是一種計算策略,其中
表達式的求值被推遲到其結果真正需要時才進行
。這意味著某些計算可能會延遲執行,直到它們對于程序的其余部分變得必要。
"短路"運算符
Python中,邏輯運算符and
、or
是惰性求值的。
- and:當使用
and
時,如果第一個操作數是False
(例如,0
、None
、False
、空序列
等),則不會計算第二個操作數,整個表達式的結果就是第一個操作數。 - or:當使用
or
時,如果第一個操作數是True
,同樣不會計算第二個操作數,整個表達式的結果就是第一個操作數。
>>> 0 and print("Python")
0
>>> True and print("Python")
Python
生成器表達式和生成器函數
Python的?成器表達式和?成器函數是惰性的,在求值時,這些表達式不會?上計算出所有的可能結果。
按需生產,內存友好:
# 1. 生成器表達式
from typing import List, Iterable, Callable
from functools import partial, reduce# 定義一個類型別名,用于類型注解
Composable = Callable[[List[int] | Iterable[int]], List[int]]def compose(*functions: Composable) -> Composable:def apply(value: List[int] | Iterable[int], fn: Composable) -> Iterable[int]:return fn(value)return lambda data: reduce(apply, functions, data)# 修改函數以只接受一個參數(列表)
def add_x(data: List[int] | Iterable[int], x: int) -> Iterable[int]:print("add_x ...")return (item + x for item in data)def multiply_by_x(data: List[int] | Iterable[int], x: int) -> Iterable[int]:print("multiply_by_x ...")return (item * x for item in data)# 使用 partial 預先綁定參數
multiply_by_2 = partial(multiply_by_x, x=2)
add_10 = partial(add_x, x=10)# 正確地組合函數
do_operations = compose(multiply_by_2, add_10) # 注意參數的順序resource_data = [1, 9, 3, 5, 2]
result = do_operations(resource_data)
print(list(result)) # 輸出: [12, 28, 16, 20, 14]print("----------compose_right_to_left---------------")def compose_right_to_left(*functions):return compose(*reversed(functions))# 使用從右到左的 compose
do_operations_right_to_left = compose_right_to_left(multiply_by_2, add_10)result_right_to_left = do_operations_right_to_left(resource_data)
print(result_right_to_left) # <generator object multiply_by_x.<locals>.<genexpr> at 0x0000021469D399A0>
for i in result_right_to_left:print(i) # 輸出: 22, 38, 26, 30, 24],這是先加 10 再乘以 2 的結果# 2. 生成器函數
def fibonacci_sequence(n):a, b = 0, 1for _ in range(n):yield aa, b = b, a + b# 使用生成器打印前5個斐波那契數
for fib_num in fibonacci_sequence(5):print(fib_num)
類型注解,聊勝于無
類型注解在一定程度上也能提高函數式編程代碼可讀性和可維護。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:海哥Pythonfrom typing import List, Tuple
from itertools import zip_longestnames: List[str] = ['海鴿', 'Alice', 'Bob', 'Charlie']
ages: List[int] = [18, 24, 30]# 使用類型注解的 zipped
zipped: List[Tuple[str, int]] = list(zip(names, ages))
print(zipped) # 輸出:[('海鴿', 18), ('Alice', 24), ('Bob', 30)]# 使用類型注解的 unzipped
unzipped: List[Tuple[str, int]] = list(zip(*zipped))
print(unzipped) # 輸出:[('海鴿', 'Alice', 'Bob'), (18, 24, 30)]# 使用類型注解的 zip_longest()
zipped_longest: List[Tuple[str, int]] = list(zip_longest(names, ages, fillvalue=18))
print(zipped_longest) # 輸出: [('海鴿', 18), ('Alice', 24), ('Bob', 30), ('Charlie', 18)]
第三方庫
如果對原生函數式編程不滿足,可使用第三方庫提供的語法糖簡化代碼。
#!usr/bin/env python
# -*- coding:utf-8 _*-
# __author__:公眾號:海哥Python
# __time__:2024/7/7
from funcy import walk_values, ignored = {}
request = {'age': 18,'height': '180','weight': ""
}
for k, v in request.items():try:d[k] = int(v)except (TypeError, ValueError):d[k] = 0print(d) # {'age': 18, 'height': 180, 'weight': 0}# 可以使用funcy簡化上面的代碼
dd = walk_values(ignore((TypeError, ValueError), default=0)(int), request)print(dd) # {'age': 18, 'height': 180, 'weight': 0}
fancy庫:一系列專注于實用性的花哨FP功能工具。
安裝:
pip install funcy
遍歷集合,創建其轉換(如 map,但保留類型):
from funcy import walk, walk_keys, walk_values# 定義 double 和 inc 函數
def double(x):return x * 2def inc(x):return x + 1# 示例代碼
print(walk(str.upper, {'a', 'b'})) # 輸出: {'A', 'B'}
print(walk(reversed, {'a': 1, 'b': 2})) # 輸出: {1: 'a', 2: 'b'}
print(walk_keys(double, {'a': 1, 'b': 2})) # 輸出: {'aa': 1, 'bb': 2}
print(walk_values(inc, {'a': 1, 'b': 2})) # 輸出: {'a': 2, 'b': 3}
選擇集合的一部分:
from funcy import compact, select_keys, selectdef even(n):"""判斷給定的整數是否為偶數。參數:n (int): 需要判斷的整數。返回:bool: 如果n是偶數返回True,否則返回False。"""return n % 2 == 0# 使用select函數過濾出集合中滿足even條件的元素(即偶數)
# 注意:此處的even應是一個函數,用于判斷數字是否為偶數,但在示例中未給出具體實現
# 示例輸出: {2, 10, 20}
print(select(even, {1, 2, 3, 10, 20}))# 使用select函數篩選出元組中以'a'開頭的字符串
# 示例輸出: ('a', 'ab')
print(select(r'^a', ('a', 'b', 'ab', 'ba')))# 使用select_keys函數選擇字典中值為可調用對象的鍵值對
# 示例輸出: {<class 'str'>: ''}
print(select_keys(callable, {str: '', None: None}))# 使用compact函數移除集合中的None和0值
# 注意:在Python中,集合不能包含0和None,因此實際輸出可能與預期不同
# 示例輸出: {1, 2}
print(compact({2, None, 1, 0}))
fancy庫的功能遠不止這些,感興趣的小伙伴可以自行翻讀其官方文檔。
fn.py: 在 Python 中享受函數式編程
fn.py
是一個Python庫,它提供了函數式編程的一些特性,如柯里化(Currying
)、函數組合、偏函數(Partial application
)等。這個庫旨在讓函數式編程風格更容易融入到Python的命令式編程中。
安裝 fn.py
首先,你需要通過pip安裝fn.py庫:
pip install fn.py
使用 fn.py
一旦安裝完成,你可以開始使用fn.py中
的功能。以下是一些基本的使用示例:
1. 柯里化(Currying) 柯里化允許你將接受多個參數的函數轉換為一系列接受單個參數的函數。
from fn.func import curried# 使用裝飾器@curried,將函數sum5轉換為一個可部分應用的函數
# 該函數接受五個參數,并返回它們的總和
# 通過逐步調用這個函數并傳遞參數,可以最后得到五個參數的和
@curried
def sum5(a, b, c, d, e):return a + b + c + d + e# 調用sum5函數,通過連續調用傳遞參數
# 展示了curried函數的使用,可以分步傳遞參數
print(sum5(1)(2)(3)(4)(5)) # 15# 展示了另一種調用curried函數的方式,可以一次性傳遞多個參數
print(sum5(1, 2, 3)(4, 5)) # 15
2. 函數組合 fn.py
支持函數組合,可以使用>>
和<<
操作符來鏈接函數,這類似于Unix shell
的管道操作。
from fn import F# 定義函數
double = F(lambda x: x * 2)
increment = F(lambda x: x + 1)# 組合函數
pipeline = double >> increment# 使用組合后的函數
result = pipeline(5) # 結果為 11
print(result)
3. 提供避免大量if-else
的鏈式調用思路
假設我們有一個名為Request
的類,繼承自dict,我們會對處理請求參數做一系列操作,如下:
class Request(dict):"""請求類,繼承自字典,用于處理請求中的參數。該類旨在提供一種簡潔的方法來獲取請求中的特定參數,并對參數進行基本的處理,如去除空白字符和轉換為大寫。"""def parameter(self, name):"""獲取請求參數的值。如果參數不存在,則返回None。參數:name: 參數的名稱。返回:參數的值,如果不存在則為None。"""return self.get(name, None)# 初始化一個Request實例
r = Request(testing=" Fixed ", empty=" ")# 通過parameter方法獲取參數testing的值
param = r.parameter("testing")# 根據參數值的存在與否及內容進行處理
if param is None:fixed = ""
else:param = param.strip()if len(param) == 0:fixed = ""else:fixed = param.upper()# 輸出處理后的參數值
print(fixed) # FIXED
print(len(fixed)) # 5
這樣處理略顯丑陋。fn.py
則為我們帶來另一種參考:
from operator import methodcaller
from fn.monad import optionableclass Request(dict):"""表示一個HTTP請求的類,繼承自dict,用于方便地訪問請求參數。方法:- parameter: 以可選方式獲取請求參數,如果參數不存在,則返回None。"""@optionabledef parameter(self, name):"""嘗試獲取請求中的參數值。參數:- name: 參數的名稱。返回:- 如果參數存在,則返回參數值;否則返回None。"""return self.get(name, None)# 創建一個Request實例,并初始化一些參數
r = Request(testing=" Fixed ", empty=" ")# 輸出參數"testing"的長度
print(len(r.get("testing"))) # 7# 通過一系列的map和filter操作,處理參數"testing"的值,去除空格,轉換為大寫,如果結果非空則返回,否則返回空字符串
fixed = r.parameter("testing").map(methodcaller("strip")).filter(len).map(methodcaller("upper")).get_or("")
print(fixed) # FIXED
print(len(fixed)) # 5r2 = Request(testing=" ", empty=" ")
fixed2 = r2.parameter("testing").map(methodcaller("strip")).filter(len).map(methodcaller("upper")).get_or("")
print(fixed2) # ""
parameter 方法使用了 optionable 裝飾器,使其支持鏈式調用。
以上是fn.py
庫的一些基本使用示例。你可以根據實際需求,探索更多的函數和特性。在使用fn.py
時,建議查閱官方文檔或源代碼以獲得更詳細的說明和示例。
PyFunctional庫:用于使用鏈函數式編程創建數據管道的 Python 庫
pyfunctional
庫是Python中用于函數式編程的工具包,它提供了一系列的功能,如映射(map)、過濾(filter)、折疊(fold/reduce)等,以幫助你以函數式的方式處理數據。
安裝
首先,確保你已經安裝了pyfunctional庫。如果尚未安裝,可以通過以下命令安裝:
pip install pyfunctional
使用場景
一旦安裝完成,你可以開始使用pyfunctional庫。
# 過濾賬戶交易的列表
from collections import namedtuplefrom functional import seqTransaction = namedtuple('Transaction', 'reason amount')# 初始化一個交易列表,包含多個交易實例
transactions = [Transaction('github', 7),Transaction('food', 10),Transaction('coffee', 5),Transaction('digitalocean', 5),Transaction('food', 5),Transaction('riotgames', 25),Transaction('food', 10),Transaction('amazon', 200),Transaction('paycheck', -1000)
]# 使用函數式編程風格,過濾出所有食品交易并計算總金額
# 使用Scala/Spark風格的API
food_cost = seq(transactions) \.filter(lambda x: x.reason == 'food') \.map(lambda x: x.amount).sum()print(food_cost) # 25# 使用LINQ(Language Integrated Query)風格的API
food_cost2 = seq(transactions) \.where(lambda x: x.reason == 'food') \.select(lambda x: x.amount).sum()print(food_cost2) # 25# 使用fn模塊的函數式編程風格,過濾出所有食品交易并計算總金額
from fn import _# 驗證filter操作
filtered_transactions = seq(transactions).filter(_.reason == 'food')# 驗證map操作
mapped_amounts = filtered_transactions.map(_.amount)# 驗證sum操作
food_cost3 = mapped_amounts.sum()# 檢查過濾后的交易
print(list(filtered_transactions))
print(list(mapped_amounts)) # 檢查映射后的金額 [10, 5, 10]
print(food_cost3) # 最終結果 25
PyFunctional的主要特性是它的seq類,它允許你以一種鏈式調用的方式處理序列。
更多高級功能和詳細文檔,可以參考PyFunctional
的官方文檔。
小結
函數式編程風格以其代碼的簡潔、可讀和可復用性而著稱。但在Python
中,過分偏向函數式編程并不總是最佳選擇。Python
的設計哲學并非基于純粹的函數式編程,而是采用了一種包容性的多范式方法,賦予開發者自由選擇最適合手頭任務的工具和技術的靈活性。因此,在采納函數式編程風格時,我們應該追求清晰性與效率之間的平衡,以確保代碼既優雅又高效。
作者:暴走的海鴿
鏈接:https://juejin.cn/post/7388532214060171279