【Python】编写更优秀的Python函数
发布日期:2021-09-18 21:55:51 浏览次数:2 分类:技术文章

本文共 6435 字,大约阅读时间需要 21 分钟。

在Python中,与大多数现代编程语言一样,该函数是抽象和封装的主要方法。作为开发人员,您可能已经编写了数百个函数。但并非所有函数都是平等的。编写“坏”函数会直接影响代码的可读性和可维护性。那么,什么是“坏”函数,更重要的是什么使“好”函数


目录


 

回顾

虽然我们可能不记得它们,但数学在函数方面很糟糕,所以让我们回想一下每个人最喜欢的主题:微积分。您可能还记得看到如下公式f(x) = 2x + 3。这是一个名为的函数,f它接受一个参数x,并且“返回”两次x+ 3.虽然它可能看起来不像我们在Python中习惯的函数,但它直接类似于以下代码:

函数在数学中长期存在,但在计算机科学中具有更强大的函数。然而,有了这种力量,就会出现各种陷阱。现在让我们讨论什么是“好”函数和可能需要重构的函数警告标志。

 

好函数的关键

什么使“好”的Python函数与​​糟糕的函数区分开来?你会惊讶于“好”的定义有多少可以使用。出于我们的目的,我会认为Python函数“好”,如果它可以勾选此核对表上的大部分项目(有些并非总是可行):

  • 明智地命名
  • 有一个责任
  • 包含文档字符串
  • 返回一个值
  • 不超过50行
  • 幂等的,如果可能的话,是纯粹的

对于你们中的许多人来说,这份清单看起来过于严苛。不过,我向你保证,如果你的函数遵循这些规则,你的代码会非常漂亮,会让独角兽哭泣。下面,我将为每个项目投入一个部分,然后将它们如何协调工作以创建“好”函数。

 

命名

关于这个问题我最喜欢的一句话,经常误导给Donald Knuth,但实际上来自:

计算机科学中只有两件事:缓存失效和命名事物。 - 菲尔卡尔顿

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

def get_knn(from_df):

现在,我已经在各处看到了坏名字,但这个例子来自数据科学(真的,机器学习),其实践者通常在Jupyter笔记本中编写代码,然后尝试将这些不同的单元格转换为易于理解的程序。

这个函数名称的第一个问题是它使用了首字母缩写词/缩写词。首选完整的英语单词缩写和非普遍知道的首字母缩略词。可能缩写单词的唯一原因是保存输入,但每个现代编辑器都有自动完成函数,因此您只需输入一次全名。缩写是一个问题,因为它们通常是特定于域的。在上面的代码中,knn指的是“K-Nearest Neighbors”,并且df指的是“DataFrame”,无处不在的数据结构。如果另一个不熟悉这些首字母缩略词的程序员正在阅读这些代码,那么她的名字几乎一无所知。

关于这个函数的名字还有另外两个小抱怨:“get”这个词是无关紧要的。对于大多数命名良好的函数,很明显函数会返回一些东西,它的名字会反映出来。这个from_df位也是不必要的。函数的docstring或(如果生活在边缘上)类型注释将描述参数的类型,如果参数的名称尚未明确它

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

def k_nearest_neighbors(dataframe):

现在,即使是非专业人员也可以清楚地知道这个函数计算了什么,并且参数的name(dataframe)清楚地表明应该将哪种类型的参数传递给它。

 

单一功能

直接来自“叔叔”鲍勃·马丁,同样适用于函数和类和模块(马丁先生的原始目标)。它指出(在我们的例子中)一个职能应该只有一个责任。也就是说,它应该做一件事而且只做一件事。一个重要的原因是,如果每个函数只做一件事,那么只有一个原因可以改变它:如果它做这件事的方式必须改变。当一个函数可以被删除时也变得清晰:如果在其他地方进行更改时,很明显不再需要该函数的单一职责,只需将其删除即可。

一个例子会有所帮助。这是一个执行多个“事物”的函数:

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。该函数违反了更改函数应该只有一个原因的规则。此函数需要更改有两个明显的原因:可能需要计算新的或不同的统计信息,或者可能需要更改输出的格式。这个函数最好写成两个独立的函数:一个执行并返回计算结果,另一个执行结果并打印它们。一个函数有多个功能的致命漏洞是函数名包含单词and

这种分离还允许更容易地测试函数的行为,并且还允许将两个部分分离成不同的模块中的两个函数,但是如果合适的话可以完全存在于不同的模块中。这也导致更清洁的测试和更容易的维护。

找到只做件事的函数实际上很少见。更常见的是,你会发现做很多很多事情的函数。同样,出于可读性和可测试性的目的,这些万能交易函数应该分解为更小的函数,每个函数都封装一个单独的工作单元。

 

文档注释

虽然每个人似乎都知道,定义了Python的样式指南,但似乎很少有人知道,它对于文档字符串也是如此。而不是简单地重复PEP-257的内容,随意阅读它。然而,主要的要点是:

  • 每个函数都需要一个docstring
  • 使用正确的语法和标点符号; 写完整的句子
  • 开头用一句话总结了这个函数的作用
  • 使用说明性而非描述性语言

在编写函数时,这很容易勾选。只是养成了总是编写文档字符串的习惯,并编写函数代码之前尝试编写它们。如果你不能写一个清晰的文档字符串来描述函数将要做什么,那么你需要更多地思考你为什么要编写函数。

 

返回值

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

❯ python3Python 3.7.0 (default, Jul 23 2018, 20:22:55)[Clang 9.1.0 (clang-902.0.39.2)] on darwinType "help", "copyright", "credits" or "license" for more information.>>> def add(a, b):...   print(a + b)...>>> b = add(1, 2)3>>> b>>> b is NoneTrue

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

我甚至会发表以下声明:每个函数都应该返回一个有用的值,即使仅用于可测试性目的。你应该测试你编写的代码(这不是辩论)。试想一下add上面的函数是如何粗略测试的(暗示:你必须重定向I / O,然后很快就会从那里向南移动)。此外,返回一个值允许方法链接,这个概念允许我们编写如下代码:

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'):有效,因为每个字符串methods(strip(), lower(), endswith()返回一个字符串作为调用该函数的结果。

 

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

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

我不同意。True如果操作成功完成,该函数可以返回。

 

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

这里有两点。首先,尽力避免这种做法。对于其他人来说,提供一些东西作为你的函数的一个参数只是为了发现它已被改变,在最好的情况下可能会令人惊讶,而在最坏的情况下则是彻头彻尾的危险。相反,与字符串方法非常相似,更喜欢返回应用了更改的参数的新实例。即使这是不可行的,因为制作一些参数的副本非常昂贵,你仍然可以回到旧的“ 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()返回610

幂等性的真实例子是在电梯前面点击“向上”按钮。第一次推动时,电梯被“通知”你想要上升。因为按下按钮是幂等的,所以一遍又一遍地按下它是无害的。结果总是一样的。

 

为什么幂等性很重要?

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

 

什么是“纯粹”函数?

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

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

add_three_calls = 0def add_three(number):    """Return *number* + 3."""    global add_three_calls    print(f'Returning {number + 3}')    add_three_calls += 1    return number + 3def num_calls():    """Return the number of times *add_three* was called."""    return add_three_calls

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

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

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

简而言之,它们无法被爱因斯坦称为“远距离的幽灵行动”(在计算机科学背景下)。它们不会以任何方式修改程序或系统的其余部分。在(你编写Python代码时所做的那种)中,它们是所有人中最安全的函数。他们绝对可测试性和可维护性和,甚至比单纯的幂函数,测试它们是保证基本上是尽可能快地执行它们。测试本身很简单:没有数据库连接或其他外部资源可供模拟,不需要设置代码,之后无需清理。

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

 

总结

就是这样了。写出好的函数的秘诀根本不是秘密。它只涉及遵循一些既定的最佳实践和经验法则。我希望你发现这篇文章很有帮助。现在出去告诉你的朋友!让我们都同意在所有情况下总是编写出色的代码。或者至少尽力不要将更多“坏”代码放入世界。我能活下去......

 

原文:

 

 

 

转载地址:https://blog.csdn.net/ChenVast/article/details/83058444 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【大数据】InfoWorld的2018年最佳开源数据平台奖公布
下一篇:【数据科学】进行数据分析之前的70个问题

发表评论

最新留言

很好
[***.229.124.182]2024年04月03日 20时00分22秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章