Python 是一种出色的编程语言。它是您开发机器学习或数据科学应用程序的首选。Python之所以有趣,是因为它是一种多范式编程语言,可用于面向对象编程和命令式编程。它具有简单易读易懂的语法。
在计算机科学和数学中,使用函数式编程风格可以更轻松、更自然地表达许多问题的解决方案。在本教程中,我们将讨论 Python 对函数式编程范式的支持以及有助于您以这种风格进行编程的 Python 类和模块。
完成本教程后,您将了解:
- 函数式编程的基本思想
itertools库functools库- Map-reduce 设计模式及其在 Python 中的可能实现
通过我的新书 Python for Machine Learning 开始您的项目,其中包括分步教程和所有示例的Python源代码文件。
让我们开始吧。教程概述
本教程分为五个部分;它们是:
- 函数式编程的理念
- 高阶函数:Filter、map 和 reduce
- Itertools
- Functools
- Map-reduce 模式
函数式编程的理念
如果您有编程经验,您可能学过命令式编程。它是由语句和操作变量构建的。函数式编程是一种声明式范式。它与命令式范式不同,后者通过应用和组合函数来构建程序。这里的函数应该更接近数学函数的定义,即没有副作用,或者根本没有对外部变量的访问。当您使用相同的参数调用它们时,它们始终会给出相同的结果。
函数式编程的好处是让您的程序不易出错。没有副作用,它更具可预测性,也更容易看到结果。我们也不需要担心程序的一部分干扰另一部分。
许多库都采用了函数式编程范式。例如,以下示例使用了 pandas 和 pandas-datareader
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import pandas_datareader as pdr import pandas_datareader.wb df = ( pdr.wb .download(indicator="SP.POP.TOTL", country="all", start=2000, end=2020) .reset_index() .filter(["country", "SP.POP.TOTL"]) .groupby("country") .mean() ) print(df) |
这将为您提供以下输出
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
SP.POP.TOTL country Afghanistan 2.976380e+07 Africa Eastern and Southern 5.257466e+08 Africa Western and Central 3.550782e+08 Albania 2.943192e+06 Algeria 3.658167e+07 ... ... West Bank and Gaza 3.806576e+06 World 6.930446e+09 Yemen, Rep. 2.334172e+07 Zambia 1.393321e+07 Zimbabwe 1.299188e+07 |
pandas-datareader 是一个有用的库,可以帮助您实时从 Internet 下载数据。上面的示例是从世界银行下载人口数据。结果是一个 pandas DataFrame,其中国家和年份作为索引,一个名为“SP.POP.TOTL”的列用于人口。然后我们一步一步地操作 DataFrame,最后,我们找到所有国家在多年内的平均人口。
我们可以这样写,因为在 pandas 中,DataFrame 的大多数函数不是修改 DataFrame,而是生成一个新的 DataFrame 来反映函数的结果。我们称这种行为为不可变,因为输入 DataFrame 永远不会改变。其结果是,我们可以将函数链接起来,一步一步地操作 DataFrame。如果我们必须用命令式编程的风格来打破它,上面的程序与下面的程序相同
|
1 2 3 4 5 6 7 8 9 10 |
import pandas_datareader as pdr import pandas_datareader.wb df = pdr.wb.download(indicator="SP.POP.TOTL", country="all", start=2000, end=2020) df = df.reset_index() df = df.filter(["country", "SP.POP.TOTL"]) groups = df.groupby("country") df = groups.mean() print(df) |
高阶函数:Filter、map 和 reduce
Python 不是一种严格的函数式编程语言。但是,用函数式风格编写 Python 代码非常简单。在可迭代对象上有三个基本函数,它们允许我们以非常简单的方式编写强大的程序:filter、map 和 reduce。
Filter 用于选择可迭代对象(如列表)中的某些元素。Map 用于逐个转换元素。最后,reduce 用于将整个可迭代对象转换为不同的形式,例如所有元素的总和或将列表中的子字符串连接成更长的字符串。为了说明它们的用法,让我们考虑一个简单的任务:给定来自 Apache Web 服务器的日志文件,找出发送 404 错误代码请求最多的 IP 地址。如果您不知道 Apache Web 服务器的日志文件是什么样子,以下是一个示例
|
1 2 3 4 |
89.170.74.95 - - [17/May/2015:16:05:27 +0000] "HEAD /projects/xdotool/ HTTP/1.1" 200 - "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:24.0) Gecko/20100101 Firefox/24.0" 95.82.59.254 - - [19/May/2015:03:05:19 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/articles/dynamic-dns-with-dhcp/" "Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0" 155.140.133.248 - - [19/May/2015:06:05:34 +0000] "GET /images/jordan-80.png HTTP/1.1" 200 6146 "http://www.semicomplete.com/blog/geekery/debugging-java-performance.html" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)" 68.180.224.225 - - [20/May/2015:20:05:02 +0000] "GET /blog/tags/documentation HTTP/1.1" 200 12091 "-" "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)" |
上面的是来自一个更大的文件,该文件位于此处。这些是日志中的几行。每行都以客户端(即浏览器)的 IP 地址开头,而“HTTP/1.1”后面的代码是响应状态码。通常,如果请求已满足,则为 200。但是,如果浏览器请求了服务器上不存在的内容,则代码将为 404。要找到与 404 请求最多的 IP 地址对应的 IP 地址,我们可以简单地逐行扫描日志文件,找到带有 404 的行,然后计算 IP 地址以确定出现次数最多的 IP 地址。
在 Python 代码中,我们可以这样做。首先,我们来看看如何读取日志文件并从一行中提取 IP 地址和状态码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import urllib.request import re # Read the log file, split into lines logurl = "https://raw.githubusercontent.com/elastic/examples/master/Common%20Data%20Formats/apache_logs/apache_logs" logfile = urllib.request.urlopen(logurl).read().decode("utf8") lines = logfile.splitlines() # using regular expression to extract IP address and status code from a line def ip_and_code(logline): m = re.match(r'([\d\.]+) .*? \[.*?\] ".*?" (\d+) ', logline) return (m.group(1), m.group(2)) print(ip_and_code(lines[0])) |
然后我们可以使用几个 map() 和 filter() 以及其他一些函数来查找 IP 地址
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
... import collections def is404(pair): return pair[1] == "404" def getIP(pair): return pair[0] def count_ip(count_item): ip, count = count_item return (count, ip) # transform each line into (IP address, status code) pair ipcodepairs = map(ip_and_code, lines) # keep only those with status code 404 pairs404 = filter(is404, ipcodepairs) # extract the IP address part from each pair ip404 = map(getIP, pairs404) # count the occurrences, the result is a dictionary of IP addresses map to the count ipcount = collections.Counter(ip404) # convert the (IP address, count) tuple into (count, IP address) order countip = map(count_ip, ipcount.items()) # find the tuple with the maximum on the count print(max(countip)) |
在这里,我们没有使用 reduce() 函数,因为我们已经内置了一些专门的 reduce 操作,例如 max()。但实际上,我们可以用列表推导式来编写一个更简单的程序
|
1 2 3 4 5 6 7 |
... ipcodepairs = [ip_and_code(x) for x in lines] ip404 = [ip for ip,code in ipcodepairs if code=="404"] ipcount = collections.Counter(ip404) countip = [(count,ip) for ip,count in ipcount.items()] print(max(countip)) |
或者甚至可以将其写成一个语句(但可读性较差)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import urllib.request import re import collections logurl = "https://raw.githubusercontent.com/elastic/examples/master/Common%20Data%20Formats/apache_logs/apache_logs" print( max( [(count,ip) for ip,count in collections.Counter([ ip for ip, code in [ip_and_code(x) for x in urllib.request.urlopen(logurl) .read() .decode("utf8") .splitlines() ] if code=="404" ]).items() ] ) ) |
想开始学习机器学习 Python 吗?
立即参加我为期7天的免费电子邮件速成课程(附示例代码)。
点击注册,同时获得该课程的免费PDF电子书版本。
Python 中的 Itertools
上面关于 filter、map 和 reduce 的示例说明了可迭代对象在 Python 中的普遍性。这包括列表、元组、字典、集合,甚至生成器,它们都可以使用 for 循环进行迭代。在 Python 中,我们有一个名为 itertools 的模块,它提供了更多用于操作(但不修改)可迭代对象的函数。根据Python 官方文档
该模块标准化了一组核心的快速、内存高效的工具,这些工具本身或组合使用都很有用。总而言之,它们构成了一个“迭代器代数”,使得用纯 Python 简洁高效地构建专用工具成为可能。
在本教程中,我们将讨论 itertools 的一些函数。在尝试下面的示例时,请确保导入 itertools 和 operator,如下所示
|
1 2 |
import itertools import operator |
无限迭代器
无限迭代器可帮助您创建无限长度的序列,如下所示。
| 构造 + 示例 | 输出 | ||||
count()
|
|
||||
cycle()
|
|
||||
repeat()
|
|
组合迭代器
您可以使用这些迭代器创建排列、组合等。
| 构造 + 示例 | 输出 | ||||
product()
|
|
||||
permutations()
|
|
||||
combinations()
|
|
||||
combinations_with_replacement()
|
|
更多有用的迭代器
还有其他迭代器会在传递给它们的两个列表之一结束时停止。 其中一些将在下面描述。这不是一个详尽的列表,您可以在此处查看完整列表。
Accumulate()
自动创建一个迭代器,该迭代器累积给定运算符或函数的结果并返回该结果。您可以从 Python 的 operator 库中选择一个运算符,或编写自己的自定义运算符。
|
1 2 3 4 5 6 7 8 9 10 11 |
# Custom operator def my_operator(a, b): return a+b if a>5 else a-b x = [2, 3, 4, -6] mul_result = itertools.accumulate(x, operator.mul) print("After mul operator", list(mul_result)) pow_result = itertools.accumulate(x, operator.pow) print("After pow operator", list(pow_result)) my_operator_result = itertools.accumulate(x, my_operator) print("After customized my_operator", list(my_operator_result)) |
|
1 2 3 |
After mul operator [2, 6, 24, -144] After pow operator [2, 8, 4096, 2.117582368135751e-22] After customized my_operator [2, -1, -5, 1] |
Starmap()
将相同的运算符应用于项对。
|
1 2 3 4 5 6 7 8 9 10 |
pair_list = [1, 2), (4, 0.5), (5, 7), (100, 10)] starmap_add_result = itertools.starmap(operator.add, pair_list) print("Starmap add result: ", list(starmap_add_result)) x1 = [2, 3, 4, -6] x2 = [4, 3, 2, 1] starmap_mul_result = itertools.starmap(operator.mul, zip(x1, x2)) print("Starmap mul result: ", list(starmap_mul_result)) |
|
1 2 |
Starmap add result: [3, 4.5, 12, 110] Starmap mul result: [8, 9, 8, -6] |
filterfalse()
根据特定标准过滤数据。
|
1 2 3 4 5 |
my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] even_result = itertools.filterfalse(lambda x: x%2, my_list) small_terms = itertools.filterfalse(lambda x: x>=5, my_list) print('Even result:', list(even_result)) print('Less than 5:', list(small_terms)) |
|
1 2 |
Even result: [2, 4, 6, 8, 10] Less than 5: [1, 2, 3, 4] |
Python 中的 Functools
在大多数编程语言中,将函数作为参数传递或函数返回另一个函数可能会令人困惑或难以处理。Python 包含了 functools 库,可以轻松地处理这些函数。根据 Python 官方 functools 文档
functools 模块用于高阶函数:对函数进行操作或返回其他函数的函数。通常,任何可调用对象都可以被视为函数。
在这里,我们解释了这个库的一些有用的功能。您可以在 这里 查看 functools 函数的完整列表。
使用 lru_cache
在命令式编程语言中,递归的开销非常大。每次调用函数时,它都会被评估,即使它以相同的参数集调用。在 Python 中,lru_cache 是一个装饰器,可用于缓存函数求值的结果。当函数再次以相同的参数集调用时,将使用存储的结果,从而避免了与递归相关的额外开销。
让我们看下面的例子。我们有使用和不使用 lru_cache 计算第 n 个斐波那契数的相同实现。我们可以看到 fib(30) 有 31 次函数调用,正如我们使用 lru_cache 所预期的那样。fib() 函数仅针对 n=0,1,2…30 调用,结果会存储在内存中并在以后使用。这明显少于 fib_slow(30),后者有 2692537 次调用。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import functools @functools.lru_cache def fib(n): global count count = count + 1 return fib(n-2) + fib(n-1) if n>1 else 1 def fib_slow(n): global slow_count slow_count = slow_count + 1 return fib_slow(n-2) + fib_slow(n-1) if n>1 else 1 count = 0 slow_count = 0 fib(30) fib_slow(30) print('With lru_cache total function evaluations: ', count) print('Without lru_cache total function evaluations: ', slow_count) |
|
1 2 |
With lru_cache total function evaluations: 31 Without lru_cache total function evaluations: 2692537 |
值得注意的是,lru_cache 装饰器在您尝试使用 Jupyter notebook 进行机器学习问题实验时特别有用。如果您有一个从 Internet 下载数据的函数,将其包装在 lru_cache 中可以使您的下载保持在内存中,并避免重复下载相同的文件,即使您多次调用下载函数。
使用 reduce()
Reduce 类似于 itertools.accumulate()。它将一个函数反复应用于列表的元素并返回结果。下面是一些带有注释的示例,用于解释这些函数的工作原理。
|
1 2 3 4 5 6 7 |
# 计算 ((1+2)+3)+4 list_sum = functools.reduce(operator.add, [1, 2, 3, 4]) print(list_sum) # 计算 (2^3)^4 list_pow = functools.reduce(operator.pow, [2, 3, 4]) print(list_pow) |
|
1 2 |
10 4096 |
reduce() 函数可以接受任何“运算符”和可选的初始值。例如,前一个示例中的 collections.Counter 函数可以实现如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import functools def addcount(counter, element): if element not in counter: counter[element] = 1 else: counter[element] += 1 return counter items = ["a", "b", "a", "c", "d", "c", "b", "a"] counts = functools.reduce(addcount, items, {}) print(counts) |
|
1 |
{'a': 3, 'b': 2, 'c': 2, 'd': 1} |
使用 partial()
有时你会遇到一个函数需要多个参数,其中一些参数需要反复使用。partial() 函数会返回该函数的一个新版本,并减少参数数量。
例如,如果您需要反复计算 2 的幂,您可以创建一个 numpy 的 power() 函数的新版本,如下所示:
|
1 2 3 4 5 |
import numpy power_2 = functools.partial(np.power, 2) print('2^4 =', power_2(4)) print('2^6 =', power_2(6)) |
|
1 2 |
2^4 = 16 2^6 = 64 |
Map-Reduce 模式
在前面的章节中,我们提到了 filter、map 和 reduce 函数作为高阶函数。使用 map-reduce 设计模式确实有助于我们轻松构建高度可伸缩的程序。map-reduce 模式是许多处理列表或对象集合的计算的抽象表示。map 阶段接受输入集合并将其映射到中间表示。reduce 步骤接受此中间表示并从中计算单个输出。这种设计模式在函数式编程语言中非常流行。Python 还提供了构建此设计模式的高效工具。
Python 中的 Map-Reduce
作为 map-reduce 设计模式的一个示例,让我们来看一个简单的例子。假设我们要计算列表中可被 3 整除的数字的个数。我们将使用 lambda 定义一个匿名函数,并使用它来将列表中的所有项 map() 为 1 或 0,具体取决于它们是否通过我们的整除测试。map() 函数将函数和可迭代对象作为参数。接下来,我们将使用 reduce() 来累积总体结果。
|
1 2 3 4 5 6 7 8 9 10 |
# 1 到 20 的所有数字 input_list = list(range(20)) # 使用 map 查看哪些数字可以被 3 整除 bool_list = map(lambda x: 1 if x%3==0 else 0, input_list) # 将 map 对象转换为列表 bool_list = list(bool_list) print('bool_list =', bool_list) total_divisible_3 = functools.reduce(operator.add, bool_list) print('Total items divisible by 3 = ', total_divisible_3) |
|
1 2 |
bool_list = [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0] Total items divisible by 3 = 7 |
虽然上面的例子很简单,但它说明了在 Python 中实现 map-reduce 设计模式是多么容易。您可以使用 Python 中令人惊讶的简单易懂的结构来解决复杂而冗长的问题。
进一步阅读
如果您想深入了解,本节提供了更多关于该主题的资源。
书籍
- 《像计算机科学家一样思考 Python》 作者:Allen B. Downey
- Mark Summerfield 的《Python 3 编程:Python 语言完全入门》
- John Zelle 的《Python 编程:计算机科学入门》
Python 官方文档
总结
在本教程中,您将了解 Python 支持函数式编程的特性。
具体来说,你学到了:
- 使用
itertools在 Python 中返回有限或无限序列的迭代器 functools支持的高阶函数- Map-Reduce 设计模式在 Python 中的实现
您对本文讨论的 Python 有任何疑问吗?请在下方评论区提问,我将尽力回答。








暂无评论。