和大多数现代编程语言一样,在 Python 中函数是抽象和封装的主要方法。作为开发人员,你平时可能写过几百个函数,但不是所有函数的作用都一样。如果写了个“坏”函数,会直接影响代码的可读性和可维护性。那么,什么是“坏”函数,更重要的是,怎么写出“好”函数?

《Writing Idiomatic Python》一书的作者、Python 库 Sandman 的创作者 Jeff Knupp 最近分享了一篇博客,就如何写出更好的 Python 函数给出了 6 点建议。以下内容编译自 Jeff 的这篇博文,相信对于大家的 Python 开发会有所帮助。

写出好函数的关键

好的 Python 函数与不好的 Python 函数之间的区别在哪?当然对于什么是“好”函数,每个人都有自己的看法。我(作者 Jeff Knupp)个人认为,如果一个 Python 函数能满足下面大部分条件,就能称为一个好函数:

命名合理

单一职责

包含文档字符串

返回一个值

不超过 50 行

是幂等函数或者纯函数

对于大多数人来说,这些条件看起来可能非常苛刻。不过,我向你保证,如果你写函数时遵循了这些条件,你的代码一定会非常漂亮,隔壁码农怕是都要馋哭了。

下面,我挨个讲讲这些几个规则,然后总结它们之间如何相互协调缔造出一个优美的函数。

命名

关于命名,我非常喜欢 Phil Karlton 说过的一句话:在计算机科学中,只有两个难题——缓存失效和命名。

虽然听起来很傻,但命名确实很难。举一个“坏”函数名称的示例:

def get knn(from_df):

现在,我到处都能看到糟糕的命名,但这个例子来自数据科学(真的,机器学习),这个领域的程序员通常在 Jupyter notebook 中编写代码,然后将这些不同的单元转换为易于理解的程序。

这个函数名称的第一个问题是它使用了首字母缩写。相较于缩写词和大多数人都不熟悉的缩写,优先选择完整的英语单词。可能不少人使用缩写的唯一原因是保存输入,但每个现代编辑器都有自动完成功能,因此你只需输入一次全名就行了。

缩写是个重要问题,因为它们通常是特定于某个领域。在上面的代码中,knn 指的是“K-Nearest Neighbors”(K 最近邻),而 df 指的是“DataFrame”,无处不在的 pandas 数据结构。如果另一个不熟悉这些首字母缩略词的程序员阅读这些代码,那么他几乎是懵逼的。

关于这个函数的命名还有另外两个小问题:“get”这个词无关紧要。对于大多数命名良好的函数,很明显函数会返回一些东西,这点它的名字就会反映出来。 from_df 位也是不必要的。如果参数的名称尚未对其明确,函数的文档字符串或类型注释会描述参数的类型。

那么我们如何重命名这个函数呢?简单:

def k_nearest_neighbors(dataframe):

现在哪怕是个外行,也很清楚这个函数计算的内容,参数的名称(Dataframe)同样清楚地表明应该向它传递什么类型的参数。

单一职责

这句话直接来自世界级软件开发大师,被后辈程序员尊称为“Bob大叔”的 Robert C. Martin。

单一职责原则不仅适用于类和模型,同样适用于函数。它是指一个函数应该只有一个职责。也就是说,它应该做一件事而且只做一件事。一个重要的原因是,如果每个函数只做一件事,那么只有一个原因可以改变它:它做这件事的方式必须改变。而且,这样删除一个函数时也很清晰明了:在其他地方进行更改时,如果很明显不再需要该函数的单一职责,只需将其删除即可。

举个例子。这是一个能做多个“事情”的函数:

def calculate_and print_stats(list_of_numbers):

sum = sum(list_of_numbers)

mean = statistics.mean(list_of_numbers)

median = statistics.median(list_of_numbers)

mode = statistics.mode(list_of_numbers)

print('-----------------Stats-----------------')

print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean)

print('MEDIAN: {}'.format(median)

print('MODE: {}'.format(mode)

此函数执行两项操作:计算一组有关数字列表的统计信息,以及将它们打印到STDOUT。

该函数违反了更改函数应该只有一个原因的规则。有两个明显的原因这个函数需要改动:可能需要计算新的或不同的统计信息,或者可能需要更改输出的格式。这个函数最好写成两个独立的函数:一个执行并返回计算结果,另一个获取结果并打印它们。

这种分离处理能让我们更容易地测试函数的行为,并且不仅能将两个部分分离成不同模块中的两个函数,但是如果合适的话还可以一起应用于不同的模块中。这样能让测试更简单,维护更容易。

只做两件事的函数实际上很少见。更常见的是,你会遇到做很多很多事情的函数。同样,出于可读性和可测试性考虑,这些“万金油”类型的函数实际上应该分解为更小的函数,每个函数都封装一个单独的工作单元,只有一个职责。

文档字符串

虽然好像每个 Python 程序员都知道定义了 Python 样式指南的 PEP-8,但似乎很少有人知道 PEP-257,它定义了文档字符串的样式指南。这里不再重复它的内容,自己有空了看一下。不过我说几个里面的要点:每个函数都需要一个文档字符串

使用正确的语法和标点符号;写完整的句子

开头用一句话总结函数的作用

使用说明性而非描述性语言

在写函数时,要养成写文档字符串的习惯,而且在写函数代码之前就试着写它们。如果你不能写一个清晰的文档字符串来描述函数的作用,那么你就得多花点时间想想你为什么要写函数。

返回值

函数可以(并且应该)被看作一个小的自包含程序。它们以参数的形式获取一些输入并返回一些结果。当然,参数是可选的。但是从 Python 内部角度来看,返回值不是可选的。即使你尝试创建一个不返回值的函数,也无法做到。如果函数不返回值,则 Python 解释器会“强制它”返回 None。不信?可以自己试试:

❯ python3

Python 3.7.0 (default, Jul 23 2018, 20:22:55)

[Clang 9.1.0 (clang-902.0.39.2)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

>>> def add(a, b):

... print(a + b)

...

>>> b = add(1, 2)

3

>>> b

>>> b is None

True

你会看到 b 的值确实是 None。所以,即使你编写一个没有 return 语句的函数,它仍然会返回一些东西。它应该返回一些东西,毕竟,这是一个小程序,对吧。没有输出的程序有多大用处,包括它们是否正确执行?但最重要的是,你会如何测试这样的程序?

我认为:每个函数都应该返回一个有用的值,即使仅用于可测试性目的。你应该测试你编写的代码(别杠)。试想一下测试上面的 add 函数会有多难。而且返回值也能实现链式编程,所以我们应该这样写代码:

with open('foo.txt', 'r') as input_file:

for line in input_file:

if line.strip().lower().endswith('cat'):

# ... do something useful with these lines

if line.strip().lower().endswith('cat'): 这行代码之所以有效,是因为每个字符串方法(strip(),lower(),endswith())都返回一个字符串作为调用函数的结果。

以下是人们在被问及为什么他们写的函数没有返回值时给出的一些常见原因:

“它所做的就是[与I / O相关的一些事情,比如将值保存到数据库中]。我无法返回任何有用的东西。”

这么说我不同意。如果操作顺利完成,函数可以返回 True。

“我们修改其中一个参数,像用参考参数一样用它。”

这里有两点。首先,尽量避免这种做法。对于其他人来说,把一些内容作为参数传入你写的函数,结果却发现发生了改变,往好了说会是个意外惊喜,往坏了说会把程序搞砸。相反,和字符串方法一样,优先返回应用了更改的参数的新实例。即使这不可行,复制一些参数的成本很高,仍然可以回到第一条建议。

“我需要返回多个值。没单独返回一个值没有意义。”

这话听着是没道理,但我确实听过这样的说法。其实他想做的就是:使用元组返回多个值。

可能总能返回一个有用值的最有利证据就是调用者始终可以自由忽略它们。简而言之,从函数中返回一个值几乎肯定是个好主意,不太可能会破坏任何东西,即便是在代码库中。

函数长度

我很多次说过我这人很笨,如果你让我阅读一个 200 行的函数,还要说说它是干嘛的,我怕是看上 10 秒钟就上头了。函数的长度直接影响可读性,从而影响可维护性。所以要让函数保持简短。 50 行是一个很随意的数字,对我来说似乎是合理的。你编写的大多数函数(希望)应该短一些。

如果某个函数遵守了单一职责原则,那么可能会很短。如果它是纯函数或幂等函数(下面讨论),那也可能很短。

那么如果函数太长,该怎么办?重构!重构是你可能一直都需要做的事情,即使你不熟悉这个术语。它只是改变程序的结构但不会改变其行为。从长函数中提取几行代码并将它们转换为自己的函数就是一种重构。它也恰好是以最有效的方式缩短长行数的最快和最常用的方法。

幂等性和函数纯度

虽然看着很吓人,但其实概念很简单。无论调用多少次,幂等函数总是在给定相同参数集的情况下返回相同的值。结果不依赖于非局部变量,参数的可变性或来自任何 I / O 流的数据。下面这个 add_three(number)函数就是个幂等函数:

def add_three(number):

"""Return *number* + 3."""

return number + 3

无论一次调用 add_three(7)多少次,答案总是为 10。以下是个非幂等函数:

def add_three():

"""Return 3 + the number entered by the user."""

number = int(input('Enter a number: '))

return number + 3

这个函数并不是幂等的,因为函数的返回值取决于 I / O,即用户输入的数字。显然不是每次调用 add_three()都会返回相同的值。如果它被调用两次,则用户可以第一次输入 3,第二次输入 7,使得对 add_three()的调用分别返回 6 和 10。

幂等性的现实相似例子是在电梯前面点击“向上”按钮。第一次按时,“通知”电梯你想上去。因为按按钮是幂等的,所以一遍又一遍地按下它没有什么关系,结果总是一样的。

为什么幂等性很重要?

可测试性和可维护性。幂等函数很容易测试,因为当使用相同的参数调用时,它们始终保证返回相同的结果。测试只是检查由函数的各种不同调用返回的值是否返回预期值。更重要的是,这些测试速度很快,是单元测试中一个重要且经常被忽视的问题。处理幂等函数时的重构是轻而易举的。无论你如何在函数外部更改代码,使用相同参数调用它的结果将始终相同。

什么是“纯粹”函数?

在函数式编程中,如果函数既是幂等的又没有可观察到的副作用,则认为它是纯函数。记住,如果函数始终为给定的参数集返回相同的值,则该函数是幂等函数。函数外部的任何内容都不能用于计算该值。但是,这并不意味着该函数不会影响非局部变量或 I / O 流等。例如,如果上面的幂等版本的 add_three(number)在返回之前打印了结果,仍认为它是幂等的,因为当它访问 I / O 流时,该访问与函数返回的值无关。对 print() 的调用只是一个副作用:除了返回值之外,还与程序的其余部分或系统本身进行一些交互。

我们把 add_three(number)示例更进一步讲。我们可以编写以下代码片段来确定调用add_three(number)的次数:

add_three_calls = 0

def add_three(number):

"""Return *number* + 3."""

global add_three_calls

print(f'Returning {number + 3}')

add_three_calls += 1

return number + 3

def num_calls():

"""Return the number of times *add_three* was called."""

return add_three_calls

我们现在打印到控制台(副作用)并修改非局部变量(另一个副作用),但由于这些都不会影响函数返回的值,因此它仍然是幂等的。

纯函数没有副作用。它不仅不使用任何“外部数据”来计算它的值,除了计算和返回所述值之外,它不与系统/程序的其余部分交互。因此,虽然我们的新 add_three(number)定义仍然是幂等函数,但它不再是纯函数。

纯函数没有日志语句或 print() 调用,它们不使用数据库或网络连接。它们不访问或修改非局部变量,并且不会调用任何其他非纯函数。

简而言之,它们不会以任何方式修改程序或系统的其余部分。在命令式编程(你编写 Python 代码时所做的那种)中,它们是所有函数中最安全的函数。不仅非常易于测试和维护,甚至比仅仅幂等函数更易于测试和维护,测试它们基本上与执行它们一样快。测试本身很简单:无需数据库连接或其他外部资源可供模拟,不需要设置代码,之后无需清理。

明确地说,幂等性和纯函数只是我们的一种期待,不是必需这样。也就是说,由于以上提到的显而易见的好处,我们想编写纯函数或幂等函数,但这并不总是可行的。但关键是,我们自然会开始安排代码来隔离副作用和外部依赖。这样可以让我们编写的每一行代码都更容易测试,即使我们写的函数并非纯函数或幂等函数。

结语

写出好的 Python 函数的秘密就这些,希望能对你有用。

课程 | 景略集智​jizhi.ai

python定界符有哪些_Python 开发中有哪些高级技巧?相关推荐

  1. 收藏,Python 开发中有哪些高级技巧?

    Python 开发中有哪些高级技巧?这是知乎上一个问题,我总结了一些常见的技巧在这里,可能谈不上多高级,但掌握这些至少可以让你的代码看起来 Pythonic 一点.如果你还在按照类C语言的那套风格来写 ...

  2. python 通用数据库类型_Python开发基础之Python常用的数据类型

    博文大纲 一.Python介绍 二.Python的变量 三.Python常用的数据类型 1.数字 2.字符串 3.元组 4.列表 5.字典 一.Python介绍 Python是一种动态解释型的编程语言 ...

  3. python编辑器是什么_python开发用什么编辑器

    如果你想做得好,你必须首先提高工具.据估计,大部分pythoners开始python从python IDE.有许多python IDE工具,但这是一个艰巨的任务选择一个IDE使用.您可以使用最基本的文 ...

  4. python编译环境搭建_python开发环境搭建

    python的开发环境是比较简单的,本来也没打算要写,不过现在想想还是写出来,一方面保证来我博客上python内容的完整性,一方面也可能有些人真的不会,毕竟我是用了很多其他语言之后才转到python的 ...

  5. python定界符有哪些_Python字符串

    第二讲 list 列表 一.列表的基本介绍 什么是列表? 列表由一系列按特定顺序排列的元素组成,使用[]作为定界符,用逗号作为元素分隔符. List的特点 有序可变,可以是任意类型数据. 列表的数据结 ...

  6. python视频网站项目_Python开发教育网站项目实例教学(105集视频课程含源码)

    Python开发教育网站项目实例教学(105集视频课程含源码)课程简介: Python开发教育网站项目实例教学(105集视频课程含源码)通过这105集Python实例视频课程的学习,学员可以掌握到使用 ...

  7. python 自定义模块加密_Python开发【第一篇】Python基础之自定义模块和内置模块...

    为什么要有模块,将代码归类.模块,用一砣代码实现了某个功能的代码集合. Python中叫模块,其他语言叫类库. 类似于函数式编程和面向过程编程,函数式编程则完成一个功能,其他代码用来调用即可,提供了代 ...

  8. python读写文件函数_Python开发【第三篇】:函数读写文件

    三元运算 三元运算,是条件语句的简单的写法.如果条件为真,则返回值1,否则,返回值2. ret = 值1 if 条件 else 值2 深浅拷贝 对于数字(int)和字符串(str)而言,赋值.深拷贝. ...

  9. python写webservice接口_Python开发WebService系列教程之REST,web.py,eurasia,Django

    在Bioinformatics(生物信息学)领域,WebService是很重要的一种数据交换技术,未来必将更加重要.目前EBI所提供的WebService就分别有SOAP和REST两种方式的服务,不管 ...

最新文章

  1. python 中参数 ,解包和变量的入门
  2. zookeeper 运维
  3. Reporting Service 2012 体系结构
  4. MOCTF-Web-我想要钱
  5. AT3557 Four Coloring 切比雪夫距离 + 四色构造
  6. 【Lucy-Richardson去卷积】迭代加速算法
  7. 小A点菜(洛谷-P1164)
  8. ai驱动数据安全治理_人工智能驱动的Microsoft工具简介
  9. curl 使用 ~/.netrc ( Windows 上是 _netrc ) 问题
  10. Django DetailView 多重继承 关系整理
  11. 系统集成项目管理工程师 案例题【2021上】 总结
  12. win10系统自动安装应用商店(Microsoft Store)方法步骤
  13. 嵌入式学习(一)嵌入式c语言
  14. 使用electron开发了一个excel对比工具
  15. 初中女生数学不好能学计算机,初中女生必看:学好数学的方法及窍门
  16. block使用时的一些情况以及防止循环引用
  17. python lamba表达式
  18. RN cannot add a child that doesnot have a YogoNode to a parent without a measure function!
  19. 客户贷后还款提醒优化项目
  20. 夫唯不争——世赛网络系统管理赛项小记

热门文章

  1. python标准库之fnmatch,dis,timeit
  2. linux运行级别与服务
  3. react apollo_Apollo GraphQL:如何使用React和Node Js构建全栈应用
  4. ds--8600使用手册_我如何用57行代码复制一个价值8600万美元的项目
  5. python 提升工作效率_Python那些事——5个提升生产效率的Python开发和配置的小技巧...
  6. Python高级——mini_web框架(实现web框架接口,数据库连接)
  7. Jinja2模板与模板继承
  8. leetCode 题 - 100. 相同的树
  9. Dokcer值得学习吗
  10. 微信公众平台开发者文档