在函数式编程中,如何使用 Python 编写出优秀的代码?

作者 | Amandine Lee

译者 | 弯月

责编 | 屠敏

出品 | CSDN(ID:CSDNNews)

简介

Python 是一种功能丰富的高级编程语言。它有通用的标准库,支持多种编程语言范式,还有许多内部的透明度。如果你愿意,还可以查看 Python 的底层并修改,甚至能在程序运行的时候直接修改运行时。

我最近注意到一个有经验的 Python 程序员使用 Python 的新方法。就像许多 Python 新手一样,我在第一次看到 Python 时喜欢它的简单易懂的基本循环、函数和类定义的语法。在掌握了基础语法之后,我开始对高级功能感兴趣,如继承、生成器、元编程等。但是,我不太清楚它们的使用方法,经常会在不恰当的地方使用。有一段时间里我写的代码复杂又难理解。后来我反复修改,特别是需要长期在同一段代码上工作时,最终会将大部分代码慢慢改回使用基本的函数、循环、单例类。

尽管如此,那些高级功能一定有其存在的理由,它们也一定是非常重要的工具。很明显,“怎样编写优秀的代码”是个非常广泛的话题,甚至没有唯一的正确答案!相反,这篇文章的目标是一个特定的话题:Python 中函数式编程的应用。我将讨论函数式是什么,怎样在 Python 中使用,并根据我的经验介绍最佳使用方法。

什么是函数式编程?

函数式编程(简称 FP)是一种编程范式,其中最基本的元素是不可修改的值,以及不与其他函数共享状态的“纯函数”。纯函数对于给定的输入永远返回同样的输出,而且不会修改任何数据,也不会造成副作用。因此,纯函数经常与数学运算比较。例如,3+4 永远等于 7,不管同时进行了其他任何数学运算,也不管之前进行了多少次加法运算。

有了纯函数和不可修改的值,程序员就可以创建逻辑结构了。迭代可以用递归代替,因为递归才是让同一个动作多次执行的“函数式”做法。函数使用新的输入调用自己,直到参数满足某个终止条件。此外,还有高阶函数,它的输入是其他函数,返回另一个函数。我稍后会介绍这个概念。

尽管函数式编程从上世纪五十年代就出现了,而且许多语言也都实现了它,但它并没有完全地描述一门语言。Clojure、Common Lisp、Haskell 和 OCaml 都是以函数式为主的语言,也都融合了其他不同的编程语言概念,如类型系统、严格或懒惰求值等。大多数语言还用某种方法支持副作用,如写入文件、读取文件等,通常这些副作用都被仔细地标记为“不纯净”。

人们通常都认为函数式很深奥,而且与可实践性相比,它更看重优雅和简洁。大公司很少会在大规模项目上依赖于函数式为主的语言,即使要用也是在较小的范围内,远远不如其他 C++、Java、Python 等语言流行。但是,FP 实际上只是一种框架,一种考虑逻辑流的方式,它本身也有优点和缺点,而且也能与其他编程范式配合使用。

Python 支持什么?

尽管Python并不是以函数式为主的语言,但对它来说支持函数式编程也相对比较容易,因为Python中的一切都是对象。这意味着函数定义也可以赋给变量并传递。

def add(a, b):    return a + b

plus = add

plus(3, 4)  # returns 7

Lambda

通过 Lambda 表达式的语法,可以用声明式的方式创建函数。关键字 lambda 来自希腊字母,经常在正式的数学逻辑中用来描述函数和变量的虚拟绑定,即“lambda 演算”,它的历史比函数式编程还要久远。这一概念的另一个术语叫做“匿名函数”,因为 lambda 函数可以直接嵌入到行内使用,不需要事先指定名称。将匿名函数赋值给变量后,它的行为与正常函数完全一样。

(lambda a, b: a + b)(3, 4)  # returns 7

addition = lambda a, b: a + baddition(3, 4)  # returns 7

lambda 函数最常见的用法就是提供给那些接受可调用对象作为参数的函数。“可调用对象”是任何能够通过括号调用的东西,具体来说有类、函数和方法。其中最常见的用法就是在对数据结构进行排序时,通过参数的键指定排序的相对顺序。

authors = ['Octavia Butler', 'Isaac Asimov', 'Neal Stephenson', 'Margaret Atwood', 'Usula K Le Guin', 'Ray Bradbury']sorted(authors, key=len)  # Returns list ordered by length of author namesorted(authors, key=lambda name: name.split()[-1])  # Returns list ordered alphabetically by last name.

行内嵌入式 lambda 函数的缺点在于它不会在栈跟踪中显示名称,可能会给调试带来麻烦。

Functools

高阶函数是函数式编程的精华,部分由 Python 直接提供,部分通过 functools 函数库提供。你可能在大规模分布式数据分析方面听说过 map 和 reduce,但实际上它们也是最重要的两个高阶函数。map 在给定序列的每个元素上执行函数,然后返回结果的序列;reduce 使用一个函数收集序列中的每个元素,然后返回单个值。

val = [1, 2, 3, 4, 5, 6]

# Multiply every item by twolist(map(lambda x: x * 2, val)) # [2, 4, 6, 8, 10, 12]# Take the factorial by multiplying the value so far to the next itemreduce(lambda: x, y: x * y, val, 1) # 1 * 1 * 2 * 3 * 4 * 5 * 6

还有许多高阶函数能用其他方式操作函数,其中最值得一提的就是 partial,它能锁定函数的一部分参数。这种方式也叫做“currying”,这个术语来自函数式编程的先驱者 Haskell Curry:

def power(base, exp):     return base ** expcube = partial(power, exp=3)cube(5)  # returns 125

关于 Python 中的 FP 概念的具体介绍,以及怎样优先使用函数式进行编程,我推荐 Mary Rose Cook 的这篇文章(https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming)。

这些函数可以将许多行的循环转变成极其精简的一行代码。但是,一般的程序员也很难理解这些代码,特别是 Python 原本与英语十分类似的语法流。个人经验,我永远都记不住参数的顺序,以及每个函数的功能,尽管我查了这么多次手册。但我强烈建议尝试一下这些函数,以了解一些 FP 的概念,而且有时候我认为它们才是正确的选择,如下一节的例子所示。

修饰器

高阶函数也以修饰器的形式融入了日常的 Python 编程中。定义修饰器的方法就反映了这一点,而@符号实际上只是个语法糖,将被修饰的函数传递给修饰器作为参数。下面就定义了一个简单的修饰器,它会将给定的代码重试三次,返回第一个成功的值,或者在三次尝试都失败之后放弃并抛出最后的异常。

def retry(func):    def retried_function(*args, **kwargs):        exc = None        for _ in range(3):            try:               return func(*args, **kwargs)            except Exception as exc:               print("Exception raised while calling %s with args:%s, kwargs: %s. Retrying" % (func, args, kwargs).

        raise exc     return retried_function

@retrydef do_something_risky():    ...

retried_function = retry(do_something_risky)  # No need to use `@`

这个修饰器的输入和输出的类型和值完全一样,但这并不是必须的。修饰器可以添加或减少参数,也可以改变参数的类型。它们也可以通过本身的参数进行配置。我想指出的是,修饰器本身不一定是“纯函数”,它们可以(而且经常会)有副作用,只不过是恰巧使用了高阶函数而已。

就像许多中级或高级 Python 技巧一样,这个功能非常强大,但也很容易造成混乱。你必须使用 functools.wrap 修饰器进行修饰,否则调用的函数和栈跟踪中看到的函数名字会不一样。我见过一些修饰器会做一些非常复杂或非常重要的事情,如解析 json blob 中的值,或者处理认证。我还见过同一个函数或方法定义上的多层修饰器,必须掌握修饰器的应用次序才能正确理解。我认为通过利用内置的修饰器如“staticmethod”可以帮助理解,或者编写最简单的修饰器来避免大量样板代码,但如果你想让你的代码符合类型检查的话,那么尽量不要去修改输入或输出的类型。

我的建议

函数式编程很有趣,而且学习舒适区之外的编程范式能够为你带来灵活性,而且也可以让你从另一个角度考虑问题。但是,我不推荐使用 Python 时以函数式为主,特别是在旧的代码库中不要这么做。除了上面我提到的那些坑之外,还有下面的理由:

  • 开始使用 Python 不需要理解 FP。这样做很可能会迷惑其他阅读者,或者迷惑未来的自己。

  • 你无法保证任何你依赖的代码(通过 pip 安装的模块,或其他同事的代码)是函数式的,是纯净的。你也不知道你自己的代码是否像你想象的那么纯净。与函数式为主的语言不同,Python 的语法或编译器不会帮你强制纯净,也不会帮你消灭某些 Bug。将副作用和高阶函数混合在一起回导致巨大的混乱,因为你需要论证两种不同的复杂性,其难度是两者的乘积。

  • 使用带有类型注释的高阶函数是高级技巧。类型签名通常是又长又笨拙的“Callable”的嵌套。例如,一个简单的返回输入函数的高阶修饰器,其定义是“F = TypeVar[‘F’, bound=Callable[..., Any]]”,然后标注是“def transparent(func: F) -> F: return func”。也许你懒得研究正确的签名的写法,而直接使用“Any”代替了。

那么,我们应该使用函数式编程的哪部分呢?

纯函数

只要可能并且合理,就应该尽量保持函数“纯净”,并仔细考虑应当在何处保持改变了的状态,并仔细地标记好。这样能让单元测试变得更容易,你不需要做太多 set-up 和 tear-down,也不需要太多 mocking,而且测试用例不论执行顺序如何,都会产生预期中的结果。

下面是个非函数式的例子。

dictionary = ['fox', 'boss', 'orange', 'toes', 'fairy', 'cup']def puralize(words):   for i in range(len(words)):       word = words[i]       if word.endswith('s') or word.endswith('x'):           word += 'es'       if word.endswith('y'):           word = word[:-1] + 'ies'       else:           word += 's'       words[i] = word

def test_pluralize():    pluralize(dictionary)    assert dictionary == ['foxes', 'bosses', 'oranges', 'toeses', 'fairies', 'cups']

第一次运行 test_pluraize 时该测试能够通过,但以后每次运行都会失败,因为它会反复添加“s”和“es”。为了让它变成纯函数, 可以这样写:

dictionary = ['fox', 'boss', 'orange', 'toes', 'fairy', 'cup']def puralize(words):   result = []   for word in words:       word = words[i]       if word.endswith('s') or word.endswith('x'):           plural = word + 'es')       if word.endswith('y'):           plural = word[:-1] + 'ies'       else:           plural = +  's'       result.append(plural)    return result

def test_pluralize():    result = pluralize(dictionary)    assert result == ['foxes', 'bosses', 'oranges', 'toeses', 'fairies', 'cups']

注意这里并没有使用任何 FP 特有的概念,只是创建并返回了一个新的对象,而不是重用并修改已有的旧对象。这样输入的内容也会保持不变。

虽然这个例子像个玩具,但想象一下,如果你传递并改变了某个复杂的对象,或者通过数据库连接进行了某些操作。当编写很多很多测试用例时就会发现,你必须非常小心地处理测试用例的顺序,或者花大量代价在每个测试用例之后清除并重新创建状态。这些工作应该是在 e2e 集成测试阶段的活儿,不应该在比较小的单元测试阶段进行。

理解(并避免)可修改性

先来个调查,你认为哪些数据结构是可修改的?

为什么这一点很重要?有些时候列表和元组可以互换使用,因此人们经常会在代码中随机使用两者之一。于是当你试图修改一个元组(比如给其中一个元素赋值)时就会出错。或者试图用列表作为字典的键,也会导致 TypeError,因为列表是可修改的。元组和字符串可以作为字典的键使用,因为它们不可修改,可以得到确定的哈希值,而其他数据结构都不行,因为它们的对象标识即使保持不变,值也会改变。

最重要的是,在传递字典、列表或集合时,它们可能会在其他上下文中被意料之外地改变。这种问题非常难以调试。可修改的默认参数就是个经典的例子:

def add_bar(items=[]):    items.append('bar')    return items

l = add_bar()  # l is ['bar']l.append('foo')add_bar() # returns ['bar', 'foo', 'bar']

字典、集合和列表很强大、效率很高、非常 Python,而且非常有用。写代码时完全不使用它们是不明智的。但即使如此,我永远会在默认参数的位置使用元组或 None(代替空字典或空列表),并且在缺乏足够的防御代码的情况下,避免将可修改的数据结构在不同的上下文中传递。

减少类的使用

类(及其实例)的可修改性是把双刃剑。随着写的 Python 代码越来越多,我开始倾向于仅在绝对必要时才使用类,而且我几乎从不使用可修改的类属性。对于那些高度面向对象的语言(如 Java)的程序员来说这一点可能很难做到,但许多其他语言中在类层面完成的东西,在 Python 可以在模块层面完成。例如,如果需要将函数或常量或命名空间分组,那么可以把它们一起放到另一个 .py 文件中。

我经常看到一些类的目的是保存几个命名变量的值,这种情况下 namedtuple(其类型是 typing.NamedTuple)就足够,而且还是不可改变的。

from collections import namedtupleVerbTenses = namedtuple('VerbTenses', ['past', 'present', 'future'])# versusclass VerbTenses(object):    def __init__(self, past, present, future):        self.past = past,        self.present = present        self.future = future

如果确实需要状态的来源,而且多个视图都需要改变该状态,那么类是绝佳的选择。此外,与静态方法相比,我更倾向于单例纯函数,这样它们能在其他上下文中组合使用。

可修改的类属性非常危险,因为它们属于类定义而不是类实例,因此可能会不小心修改到同一个类的多个实例中的状态!

class Bus(object):     passengers = set()     def add_passenger(self, person):        self.passengers.add(person)

bus1 = Bus()bus2 = Bus()bus1.add_passenger('abe')bus2.add_passenger('bertha')bus1.passengers  # returns ['abe', 'bertha']bus2.passengers  # also ['abe', 'bertha']

幂等性

任何实际的大规模复杂系统都可能会失败,而失败就要重试。矩阵代数中的“幂等性”的概念也存在于 API 设计中,但对于函数式编程来说,传递之前的输出给幂等函数,永远会返回相同的值。因此,重做某件事情会收敛到相同的值。因此,上述 pluralize 函数更理想的写法为:,首先检查输入是否已是复数,再考虑怎样计算出复数形式。

lambda 和高阶函数使用上的注意点

我发现,在进行短小的操作(如获取排序的键供 sort 使用)时使用 lambda 非常方便。但如果 lambda 超过一行,那么使用普通的函数定义可能更好。通常传递函数可以避免重复,但我在使用时经常提醒自己,额外的结构是否会让代码清晰度下降。通常,将其分解成更小的辅助函数会更清晰。

在需要时使用生成器和高阶函数

有时候你会遇到抽象的生成器和迭代器,它们可能会返回巨大或者无限的序列。一个例子就是 range。在 Python 3 中,range 默认是生成器(相当于Python 2 中的 xrange),避免在迭代大数字时出现内存不足的错误,如range(10 ** 10)。如果要在一个可能很大的生成器的每个元素上执行某个操作,那么使用 map、filter 之类的工具可能是最好的选择。

与此相似,如果不知道你新写的迭代器可能会返回多少结果,但可能会很大,那就应该定义一个生成器。但是,并不是每个人都愿意去使用生成器,他们可能更希望使用列表解析式(list comprehension),从而导致你一开始想要避免的内存不足错误。生成器是 Python 对于流式编程的实现,它也不一定是函数式的,所以它也有其他 Python 编程方式拥有的安全性缺陷。

结论

通过浏览功能、库和内部代码来理解自己选择的编程语言,毫无疑问能帮你在调试和阅读代码方面提高速度。理解其他语言或编程语言理论方面的思想也很有意思,而且能让你成为更强大、无所不通的程序员。但是,成为Python的高级程序员意味着你不仅要知道能做什么,更要理解哪种才是最有效的方式。在Python中应用函数式编程可能很容易。为了保持优雅,特别是在共享的代码中保持优雅,我认为最好是使用纯粹的函数式思想,让代码更容易预测,从而更容易维护,并且具有幂等性。

原文:https://kite.com/blog/python/functional-programming

作者:Amandine Lee,任职于Dropbox。

本文为 CSDN 翻译,如需转载,请注明来源出处。

 热 文 推 荐 

☞ 程序员崩溃了,想拿的年终奖怎么说黄就黄?!

☞ Google AI 骗过了 Google,工程师竟无计可施?

☞ 清华北大“世界排名断崖式下跌”?

☞ 资源 | 最新版区块链术语表(中英文对照)

☞ 程序员有话说 | 程序猿在乘地铁的时候都在想什么?

☞ 2018全球50大最佳发明名单

☞ QQ卖手办,用AI分析用户评论

☞ 春运抢票靠加速包?试试这个Python开源项目吧

print_r('点个好看吧!');
var_dump('点个好看吧!');
NSLog(@"点个好看吧!");
System.out.println("点个好看吧!");
console.log("点个好看吧!");
print("点个好看吧!");
printf("点个好看吧!\n");
cout << "点个好看吧!" << endl;
Console.WriteLine("点个好看吧!");
fmt.Println("点个好看吧!");
Response.Write("点个好看吧!");
alert("点个好看吧!")
echo "点个好看吧!"

点击“阅读原文”,打开 CSDN App 阅读更贴心!

喜欢就点击“好看”吧

在 Python 中使用函数式编程的最佳实践!相关推荐

  1. python支持函数式编程吗_利用Fn.py库在Python中进行函数式编程

    尽管Python事实上并不是一门纯函数式编程语言,但它本身是一门多范型语言,并给了你足够的自由利用函数式编程的便利.函数式风格有着各种理论与实际上的好处(你可以在Python的文档中找到这个列表): ...

  2. 『 迷你教程 』Python中的函数式编程全方法详解

    Python 是一种很棒的编程语言,是开发机器学习或数据科学应用程序的首选.Python 也很有趣,因为是一种多范式编程语言,可用于面向对象和命令式编程.具有简单的语法,易于阅读和理解. 在计算机科学 ...

  3. 【笔记】效率之门——Python中的函数式编程技巧

    文章目录 Python函数式编程 1. 数据 2. 推导式 3. 函数式编程 3.1. Lambda函数 3.2. python内置函数 3.3. 高阶函数 4. 函数式编程的应用 Python函数式 ...

  4. 【script】python中的函数式编程

    函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用.而允许使用变量的程序设计语言,由 ...

  5. less 函数_Python中的函数式编程教程,学会用一行代码搞定所有内容

    前言 在本文中,您将了解什么是函数范型,以及如何在Python中使用函数式编程.在Python中,函数式编程中的map和filter可以做与列表相同的事情.这打破了Python的禅宗规则之一,因此函数 ...

  6. wpf绑定 dictionary 给定关键字不再字典中_为什么要在 JavaScript 中学习函数式编程?...

    请忘掉你认为你知道的有关 JavaScript 的任何东西,以初学者心态来接触这份资料. 为帮助你这样做,我们打算从头开始复习 JavaScript 的基础知识, 就好像你以前从来没有看到过 Java ...

  7. 六、Go编程语言中的函数式编程

    @Author:Runsen 任何编程语言都是众所周知的面向对象编程,还有日渐流行的函数式编程,当然Go也不例外,这也是本文的重点..我可以这么说,Go的功力深不深完全就是看函数式编程和面向对象编程. ...

  8. python采用面向对象编程模式吗_如何理解 Python 中的面向对象编程?

    现如今面向对象编程的使用非常广泛,本文我们就来探讨一下Python中的面向对象编程. 作者 | Radek Fabisiak 译者 | 弯月,责编 | 郭芮 以下为译文: Python支持多种类型的编 ...

  9. C#中面向对象编程中的函数式编程

    目录 介绍 面向对象编程中仿真的函数式编程技术 粒度不匹配 面向对象的函数式编程构造 相互关系函数式编程/面向对象程序设计 C#中的函数式编程集成 函数级别的代码抽象 操作组合 函数部分应用和局部套用 ...

最新文章

  1. hdu 5366 简单递推
  2. CVPR2020夜间行人检测挑战赛两冠一亚:DeepBlueAI获胜方案解读
  3. Spring Mybatis实例SqlSessionDaoSupport混用xml配置和注解
  4. Java接口和Java抽象类的认识
  5. 精密空调主要部件是干啥用的?
  6. Java判断工作日计算,计算随意2个日期内的工作日
  7. 01 | 基础架构:一条 SQL 查询语句是如何执行的
  8. 基于表的数据字典构造MySQL建表语句
  9. setsockopt中参数之SO_REUSEADDR的意义
  10. indesign图片规定在左下角_InDesign如何为目录模板设置母版
  11. 【视频目标检测数据集收集】B站、YouTube等各大网站视频下载工具:Annie(现更名为lux)的下载与安装教程
  12. DisplayPort-DP接口知识
  13. 通过IMAP方式迁移U-Mail邮件到Exchange 2013之Exchange 2007 系统搭建!
  14. SUSE11挂载目录seems to be mounted read-only错误 2022_11_08
  15. Chrome浏览器滚动条样式设置
  16. 通过VM虚拟机安装linux系统(centos版本)
  17. 实验11 虚函数与多态
  18. zoomlt屏幕放大画画工具
  19. 郑州商品交易所:数智一体化助力交易所数字化转型
  20. 2021-07-28 关于软件测试从业人员的几个误解

热门文章

  1. Flutter基础—绘画效果之装饰容器
  2. BigDecimal 工具类
  3. 国外程序员薪资曝光,美国最高,均年薪95879美元
  4. 英特尔新任CEO的“开挂”人生
  5. AIoT、DevOPS、数据平台、开源,你不可不知的微软 Azure 黑科技大公开
  6. 2020年接近尾声,我选择来鲲鹏开发者技术峰会学点干货!
  7. 基于“中国架构”,为政企数字化转型而生,中国电子云自带“三大光环”
  8. 助力企业实现新增长,腾讯企点发布全新数字化客户运营解决方案
  9. 微信被指监听用户,腾讯回应;谷歌意外推送 Android 11 Beta 更新;Linux 5.7 发布 | 极客头条...
  10. 28 岁裸辞转行程序员,一年的心路历程大曝光