网站LOGO
公爵书房 | 技术分享
页面加载中
10月4日
网站LOGO 公爵书房 | 技术分享
以指键之轻,承载知识之重
菜单
  • 公爵书房 | 技术分享
    以指键之轻,承载知识之重
    用户的头像
    首次访问
    上次留言
    累计留言
    我的等级
    我的角色
    打赏二维码
    打赏博主
    Python学习笔记(四)·函数式编程
    点击复制本页地址
    微信扫一扫
    文章二维码
    文章图片 文章标题
    创建时间
  • 一 言
    确认删除此评论么? 确认
  • 本弹窗介绍内容来自,本网站不对其中内容负责。

    Python学习笔记(四)·函数式编程

    公爵 · 原创 ·
    笔记 · 学习笔记python函数式编程
    共 25832 字 · 约 18 分钟 · 19
    本文最后更新于2023年09月02日,已经过了31天没有更新,若内容或图片失效,请留言反馈

    函数是 Python 内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程的程序设计的基本单元。

    而函数式编程(请注意多了一个“式”字)—— Functional Programming,虽然也可以归结到面向过程的程序设计,但其思想更接近数学计算。

    我们首先要搞明白计算机(Computer)和计算(Compute)的概念。

    在计算机的层次上,CPU 执行的是加减乘除的指令代码,以及各种条件判断和跳转指令,所以,汇编语言是最贴近计算机的语言。

    而计算则指数学意义上的计算,越是抽象的计算,离计算机硬件越远。

    对应到编程语言,就是越低级的语言,越贴近计算机,抽象程度低,执行效率高,比如 C 语言;越高级的语言,越贴近计算,抽象程度高,执行效率低,比如 Lisp 语言。

    ?? 诞生 50 多年之后,函数式编程(functional programming)开始获得越来越多的关注。不仅最古老的函数式语言 Lisp 重获青春,而且新的函数式语言层出不穷,比如 Erlang、clojure、Scala、F# 等等。目前最当红的 Python、Ruby、Javascript,对函数式编程的支持都很强,就连老牌的面向对象的 Java、面向过程的 PHP,都忙不迭地加入对匿名函数的支持。越来越多的迹象表明,函数式编程已经不再是学术界的最爱,开始大踏步地在业界投入实用。也许继"面向对象编程"之后,"函数式编程"会成为下一个编程的主流范式(paradigm)。

    4.1 什么是函数式编程

    函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。

    ? 函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

    Python 对函数式编程提供部分支持。由于 Python 允许使用变量,因此,Python 不是纯函数式编程语言。

    4.1.1 定义

    简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。它属于"结构化编程"的一种,主要思想是把运算过程尽量写成一系列嵌套的函数调用。

    举例来说,现在有这样一个数学表达式:

    (1 + 2) * 3 - 4

    传统的过程式编程,可能这样写:

    var a = 1 + 2;
    var b = a * 3;
    var c = b - 4;

    函数式编程要求使用函数,我们可以把运算过程定义为不同的函数,然后写成下面这样:

    var result = subtract(multiply(add(1,2), 3), 4);

    这就是函数式编程。

    4.1.2 特点

    函数式编程具有五个鲜明的特点。

    ① 函数是"第一等公民"

    所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

    举例来说,下面代码中的 print 变量就是一个函数,可以作为另一个函数的参数。

    var print = function(i){ 
        console.log(i);
    };
    [1,2,3].forEach(print);

    ② 只用"表达式",不用"语句"

    "表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

    原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。

    当然,实际应用中,不做 I/O 是不可能的。因此,编程过程中,函数式编程只要求把 I/O 限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

    ③ 没有"副作用"

    所谓"副作用")(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

    函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

    ④ 不修改状态

    上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

    在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。

    function reverse(string) {
        if(string.length == 0) {
          return string;
        } else {
          return reverse(string.substring(1, string.length)) + string.substring(0, 1);
        }
      }

    由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。

    ⑤ 引用透明

    引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

    有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。

    4.1.3 好处

    函数式编程到底有什么好处,为什么会变得越来越流行?

    ① 代码简洁,开发快速

    函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

    ② 接近自然语言,易于理解

    函数式编程的自由度很高,可以写出很接近自然语言的代码。

    前文曾经将表达式 (1 + 2) * 3 - 4,写成函数式语言:

    subtract(multiply(add(1,2), 3), 4)

    对它进行变形,不难得到另一种写法:

    add(1,2).multiply(3).subtract(4)

    这基本就是自然语言的表达了。再看下面的代码,大家应该一眼就能明白它的意思吧:

    merge([1,2],[3,4]).sort().search("2")

    因此,函数式编程的代码更容易理解。

    ③ 更方便的代码管理

    函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

    ④ 易于"并发编程"

    函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。

    请看下面的代码:

    var s1 = Op1();
    var s2 = Op2();
    var s3 = concat(s1, s2);

    由于 s1 和 s2 互不干扰,不会修改变量,谁先执行是无所谓的,所以可以放心地增加线程,把它们分配在两个线程上完成。其他类型的语言就做不到这一点,因为 s1 可能会修改系统状态,而 s2 可能会用到这些状态,所以必须保证 s2 在 s1 之后运行,自然也就不能部署到其他线程上了。

    多核 CPU 是将来的潮流,所以函数式编程的这个特性非常重要。

    ⑤ 代码的热升级

    函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,也不需要停机。Erlang) 语言早就证明了这一点,它是瑞典爱立信公司为了管理电话系统而开发的,电话系统的升级当然是不能停机的。

    下面进行具体函数具体示例介绍:

    4.2 高阶函数

    高阶函数英文叫 Higher-order function。什么是高阶函数?我们以实际代码为例子,一步一步深入概念。

    (1)变量可以指向函数

    以 Python 内置的求绝对值的函数abs()为例,调用该函数用以下代码:

    abs(-10) # 10

    但是,如果只写abs呢?

     ></cat_post_image><p>可见,<code>abs(-10)</code>是函数调用,而<code>abs</code>是函数本身。</p><p>要获得函数调用结果,我们可以把结果赋值给变量:</p><pre><code>x = abs(-10)
x # 10</code></pre><p>但是,如果把函数本身赋值给变量呢?</p><cat_post_image><img src= ></cat_post_image><p>? 编写高阶函数,就是让函数的参数能够接收别的函数。</p><blockquote>小结:</blockquote><p>把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。</p><h3>4.2.1 map/reduce</h3><p>Python 内建了<code>map()</code>和<code>reduce()</code>函数。</p><p>如果你读过 Google 的那篇大名鼎鼎的论文“<a href= >

    ? 编写高阶函数,就是让函数的参数能够接收别的函数。

    小结:

    把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。

    4.2.1 map/reduce

    Python 内建了map()reduce()函数。

    如果你读过 Google 的那篇大名鼎鼎的论文“MapReduce: Simplified Data Processing on Large Clusters”,你就能大概明白 map/reduce 的概念。

    ① map

    我们先看 map。map()函数接收两个参数,一个是函数,一个是Iterablemap将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

    举例说明,比如我们有一个函数 $ f(x)=x^2$,要把这个函数作用在一个 list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

     ></cat_post_image><p>现在,我们用 Python 代码实现:</p><pre><code>def f(x):
    return x * x
r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9])
list(r)</code></pre><cat_post_image><img src=素数的一个方法是埃氏筛法,它的算法理解起来非常简单:

    首先,列出从2开始的所有自然数,构造一个序列:

    2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...

    取序列的第一个数2,它一定是素数,然后用2把序列的2的倍数筛掉:

    3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...

    取新序列的第一个数3,它一定是素数,然后用3把序列的3的倍数筛掉:

    5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...

    取新序列的第一个数5,然后用5把序列的5的倍数筛掉:

    7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...

    不断筛下去,就可以得到所有的素数。

    用 Python 来实现这个算法,可以先构造一个从3开始的奇数序列:

    def _odd_iter():
        n = 1
        while True:
            n = n + 2
            yield n

    注意这是一个生成器,并且是一个无限序列。

    然后定义一个筛选函数:

    def _not_divisible(n):
        return lambda x: x % n > 0

    最后,定义一个生成器,不断返回下一个素数:

    def primes():
        yield 2
        it = _odd_iter() # 初始序列
        while True:
            n = next(it) # 返回序列的第一个数
            yield n
            it = filter(_not_divisible(n), it) # 构造新序列

    这个生成器先返回第一个素数2,然后,利用filter()不断产生筛选后的新的序列。

    由于primes()也是一个无限序列,所以调用时需要设置一个退出循环的条件:

    # 打印1000以内的素数:
    for n in primes():
        if n < 1000:
            print(n)
        else:
            break

    注意到Iterator是惰性计算的序列,所以我们可以用 Python 表示“全体自然数”,“全体素数”这样的序列,而代码非常简洁。

    练习题:

    回数是指从左向右读和从右向左读都是一样的数,例如12321909。请利用filter()筛选出回数:

    def is_palindrome(n):
        return str(n) == str(n)[::-1]

    测试:

    # 测试:
    output = filter(is_palindrome, range(1, 1000))
    print('1~1000:', list(output))
    if list(filter(is_palindrome, range(1, 200))) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66, 77, 88, 99, 101, 111, 121, 131, 141, 151, 161, 171, 181, 191]:
        print('测试成功!')
    else:
        print('测试失败!')
    小结:

    filter()的作用是从一个序列中筛出符合条件的元素。由于filter()使用了惰性计算,所以只有在取filter()结果的时候,才会真正筛选并每次返回下一个筛出的元素。

    4.2.3 sorted

    ① 排序算法

    排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序,排序的核心是比较两个元素的大小。如果是数字,我们可以直接比较,但如果是字符串或者两个 dict呢?直接比较数学上的大小是没有意义的,因此,比较的过程必须通过函数抽象出来。

    Python 内置的sorted()函数就可以对 list 进行排序:

    sorted([36, 5, -12, 9, -21])
     ></cat_post_image><p>此外,<code>sorted()</code>函数也是一个高阶函数,它还可以接收一个<code>key</code>函数来实现自定义的排序,例如按绝对值大小排序:</p><pre><code>sorted([36, 5, -12, 9, -21], key=abs)</code></pre><cat_post_image><img src= >

    此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

    sorted([36, 5, -12, 9, -21], key=abs)

    key 指定的函数将作用于 list 的每一个元素上,并根据 key 函数返回的结果进行排序。对比原始的 list 和经过key=abs处理过的 list:

    list = [36, 5, -12, 9, -21]
    
    keys = [36, 5,  12, 9,  21]

    然后sorted()函数按照 keys 进行排序,并按照对应关系返回 list 相应的元素:

     ></cat_post_image><p>我们再看一个字符串排序的例子:</p><pre><code>sorted(['bob', 'about', 'Zoo', 'Credit'])</code></pre><cat_post_image><img src= >

    我们再看一个字符串排序的例子:

    sorted(['bob', 'about', 'Zoo', 'Credit'])

    默认情况下,对字符串排序,是按照 ASCII 的大小比较的,由于'Z' < 'a',结果,大写字母Z会排在小写字母a的前面。

    现在,我们提出排序应该忽略大小写,按照字母序排序。要实现这个算法,不必对现有代码大加改动,只要我们能用一个 key 函数把字符串映射为忽略大小写排序即可。忽略大小写来比较两个字符串,实际上就是先把字符串都变成大写(或者都变成小写),再比较。

    这样,我们给sorted传入 key 函数,即可实现忽略大小写的排序:

    sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
     ></cat_post_image><p>要进行反向排序,不必改动 key 函数,可以传入第三个参数<code>reverse=True</code>:</p><pre><code> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)</code></pre><cat_post_image><img src= >

    要进行反向排序,不必改动 key 函数,可以传入第三个参数reverse=True

     sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower, reverse=True)

    从上述例子可以看出,高阶函数的抽象能力是非常强大的,而且,核心代码可以保持得非常简洁。

    小结:

    sorted()也是一个高阶函数。用sorted()排序的关键在于实现一个映射函数。

    练习题:

    假设我们用一组 tuple 表示学生名字和成绩:

    L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]

    请用sorted()对上述列表分别按名字升序排序,再按成绩从高到低排序。

    (1)按名字升序排序

    L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
    def by_name(t):
        return t[0]
    L2 = sorted(L, key=by_name)
    print(L2)
     ></cat_post_image><p><strong>(2)按成绩从高到低排序</strong></p><pre><code>L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]
def by_name(t):
    return -t[1]
L2 = sorted(L, key=by_name)
print(L2)</code></pre><cat_post_image><img src= ></cat_post_image><blockquote>小结:</blockquote><p>当函数的参数个数太多,需要简化时,使用<code>functools.partial</code>可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。</p><h2>4.7 参考资料</h2><ul><li><a href= >
    小结:

    当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

    4.7 参考资料

    声明:本文由 公爵(博主)原创,依据 CC-BY-NC-SA 4.0 许可协议 授权,转载请注明出处。

    还没有人喜爱这篇文章呢

    发一条! 发一条!
    博客logo 公爵书房 | 技术分享 以指键之轻,承载知识之重 51统计 百度统计
    MOEICP 萌ICP备20226257号 ICP 赣ICP备2022001242号-1 ICP 闽公网安备35020502000606号 又拍云 本站由又拍云提供CDN加速/云存储服务

    🕛

    本站已运行 1 年 257 天 5 小时 46 分

    🌳

    自豪地使用 Typecho 建站,并搭配 MyLife 主题
    公爵书房 | 技术分享. © 2022 ~ 2023.
    网站logo

    公爵书房 | 技术分享 以指键之轻,承载知识之重
     
     
     
     
    壁纸