实际上,“
写惯用Python ”一书的作者Jeff Knapp撰写的这篇精彩文章的标题充分体现了其本质。 仔细阅读并随时发表评论。
由于我们确实不想在文本中使用拉丁字母表示重要的术语,因此我们已经在
多个 俄语语言中发现了“ docstring”一词,将其翻译为“ docstring”。
与大多数现代编程语言一样,在Python中,函数是抽象和封装的主要方法。 作为开发人员,您可能已经编写了数百个函数。 但是功能对功能-不和谐。 此外,如果编写“不良”函数,它将立即影响代码的可读性和支持。 那么,什么是“坏”功能,更重要的是,如何使其成为“好”功能?
刷新主题
数学充满了功能,但是很难回忆起它们。 因此,让我们回到我们最喜欢的学科:分析。 您可能已经看过
f(x) = 2x + 3
公式。 这是一个称为
f
的函数,该函数接受参数
x
,然后两次“返回”
x + 3
。 尽管它与我们在Python中使用的功能不太相似,但与以下代码完全相似:
def f(x): return 2*x + 3
函数早就存在于数学中,但是在计算机科学中,它们已经完全转变了。 但是,这种权力并非徒劳:您必须克服各种陷阱。 让我们讨论什么是“好”功能,以及对于可能需要重构的功能来说典型的“钟声”。
机能的秘诀
普通的Python函数与普通的Python函数有什么区别? 您会惊讶于“好”一词有多少种解释。 在本文的框架中,如果Python函数满足以下列表中的
大多数项(有时无法为特定函数执行所有项),我将认为它是“好”的:
- 明确命名
- 符合专人负责的原则
- 包含码头
- 返回值
- 包含不超过50行
- 她是等幂的,如果可能的话,也是纯正的
对你们中的许多人来说,这些要求似乎过于苛刻。 但是,我保证:如果您的功能符合这些规则,它们将变得非常漂亮,甚至会泪流满面。 下面,我将专门介绍上述列表中的每个元素,然后通过讲述它们如何相互协调并帮助创建良好的功能来完成本故事。
命名这是我在这个问题上最喜欢的语录,通常被误认为是唐纳德(Donald),但实际上是
菲尔·卡尔顿 (
Phil Carleton)所有 :
计算机科学面临两个挑战:缓存失效和命名。
无论听起来多么愚蠢,命名都是一件棘手的事情。 这是“错误”函数名称的示例:
def get_knn_from_df(df):
现在,几乎到处都有坏名子出现,但是这个例子取材于数据科学领域(更准确地说是机器学习),从业者通常在Jupyter笔记本中编写代码,然后尝试从这些单元中汇编可消化的程序。
该函数名称的第一个问题是它使用缩写。
最好使用完整的英语单词,而不是缩写词和不知名的缩写词 。 我要缩短单词的唯一原因是不要浪费时间输入太多文本,但是
任何现代编辑器都具有自动补全功能 ,因此您只需键入该功能的全名即可。 缩写是一个问题,因为它通常是特定于主题领域的。 在上面的代码中,
knn
表示“ K近邻”,而
df
表示“ DataFrame”,这是
熊猫库中常用的数据结构。 如果不知道这些缩写的程序员读了代码,那么他将几乎不了解函数名称。
此函数的名称还有两个小缺陷。 首先,
"get"
一词
"get"
多余的。 在大多数称职的函数中,很明显该函数返回某些内容,这在名称中有特别体现。 也不需要
from_d
f元素。 在功能底座中,或者在类型注释中(如果在外围),如果该信息
从参数名称中还不明显,则将描述该参数的类型。
那么我们如何重命名此功能? 只是:
def k_nearest_neighbors(dataframe):
现在,即使是外行也可以理解此函数正在计算什么,并且参数名称
(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
。 函数违反规则:必须有一个特定的原因才能更改它。 在这种情况下,有两个明显的原因需要这样做:要么需要计算新的统计信息,要么需要计算不同的统计信息,或者需要更改输出格式。 因此,最好以两个单独的函数的形式重写此函数:一个函数将执行计算并返回其结果,另一个函数将接收这些结果并将其显示在控制台中。
具有内脏杂物的功能(或更确切地说,它有两个职责)给出和的名称 。
这种分离还极大地简化了功能的测试,还使您不仅可以将其拆分为同一模块中的两个功能,而且还可以根据需要甚至将这两个功能拆分为完全不同的模块。 这进一步有助于更清洁的测试并简化了代码支持。
实际上,执行两种功能的功能很少见。 更多时候,您会遇到执行更多,更多操作的函数。 同样,出于可读性和可测试性的原因,此类“多工作站”功能应分为单任务处理,每个功能都包含工作的一个方面。
字串
似乎每个人都知道有一个
PEP-8文档提供了有关Python代码样式的建议,但是在我们当中了解
PEP-257的人却很少,其中对码头字符串的建议也相同。 为了不重述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'):
if line.strip().lower().endswith('cat'):
字符串,因为每个字符串方法(
strip()
,
lower()
,
endswith()
)都返回一个字符串作为调用该函数的结果。
以下是程序员在解释其编写的函数为何不返回值的原因时可能会给您的一些常见原因:
“这只是[与输入/输出有关的某种操作,例如,将值存储在数据库中]。 在这里,我无法返回任何有用的信息。”
我不同意。 如果操作成功完成,该函数可以返回True。
“在这里,我们更改可用参数之一,将其用作参考参数。”
这里有两点。 首先,请尽量不要这样做。 其次,为函数提供某种参数只是为了发现它已更改,这充其量是令人惊讶的,而在最坏的情况下仅仅是危险的。 相反,与字符串方法一样,尝试返回已经反映了对其应用更改的参数的新实例。 即使无法做到这一点,由于创建某些参数的副本会花费大量成本,因此您仍然可以回滚到上面建议的选项“如果操作成功完成,则返回
True
”。
“我需要返回多个值。 在这种情况下,不建议返回任何单一值。”
这个说法有些牵强,但我已经听到了。 答案当然正是作者想要做的-但不知道如何:
使用元组返回多个值 。
最后,最强烈的论据是在任何情况下都最好返回一个有用的值,即调用方始终可以合理地忽略这些值。 简而言之,从函数返回值几乎可以肯定是一个好主意,而且即使在现有的代码库中,我们也不大可能以此方式破坏任何东西。
功能长度
我不止一次承认自己很傻。 我可以同时保留三件事。 如果您让我阅读200行功能并询问其功能,我可能会盯着它看至少10秒钟。
函数的长度直接影响其可读性,进而影响其支持 。 因此,请尝试使您的功能简短。 50行-这个值完全取自上限,但对我来说似乎很合理。 (我希望)您碰巧要编写的大多数函数会更短。
如果某个功能符合“唯一责任原则”,那么它可能足够简短。 如果是在下面阅读或幂等(我们将在此讨论),那么它可能也很短。 所有这些想法相互和谐地结合在一起,有助于编写良好,简洁的代码。
那么,如果您的函数过长怎么办?
推荐人! 即使您不知道该术语,您也可能必须一直进行重构。
重构只是改变程序的结构,而不改变其行为。 因此,从长函数中提取几行代码并将其转换为独立的函数是重构的类型之一。 事实证明,这也是有效缩短长功能的最常见,最快的方法。 由于您为这些新函数指定了适当的名称,因此生成的代码更易于阅读。 我写了一整本关于重构的书(实际上,我一直都在做),所以这里不再赘述。 只知道如果您的函数太长,则应该对其进行重构。
幂等性和功能清洁度
本节的标题似乎有些吓人,但从概念上讲,该节很简单。 具有相同参数集的幂等函数始终返回相同的值,而不管其被调用了多少次。 结果不取决于非局部变量,参数的可变性,也不取决于来自输入/输出流的任何数据。 以下
add_three(number)
函数是幂等的:
def add_three(number): """ ** + 3.""" return number + 3
无论我们调用
add_three(7)
多少次,答案始终为10。但是另一种情况是一个函数不是幂等的:
def add_three(): """ 3 + , .""" number = int(input('Enter a number: ')) return number + 3
坦率地说,该函数不是幂等的,因为该函数的返回值取决于输入/输出,即取决于用户输入的数字。 当然,通过对
add_three()
不同调用
add_three()
返回值将有所不同。 如果我们两次调用此函数,则在第一种情况下,用户可以输入3,在第二种情况下,用户可以输入7,然后对
add_three()
两次调用将分别返回6和10。
在编程之外,还有幂等的示例-例如,电梯的向上按钮是根据此原理设计的。 通过第一次按下它,我们“通知”我们要上升的电梯。 由于该按钮是幂等的,因此无论您稍后再按多少按钮,都不会发生任何不良情况。 结果将始终相同。
为什么幂等如此重要
可测试性和可用性支持。 幂等函数易于测试,因为如果您使用相同的参数调用它们,则可以保证在任何情况下都返回相同的结果。 测试归结为通过各种调用来验证该函数始终返回期望值。 而且,这些测试将很快:测试速度是一个重要的问题,在单元测试中经常被忽略。 使用幂等函数进行重构通常很容易。 无论如何在函数外更改代码都无所谓-使用相同参数调用它的结果将始终相同。
什么是“纯”功能?
在函数式编程中,如果函数
首先是等幂的,
其次它不会引起观察到的
副作用 ,则该函数被视为纯函数。 不要忘记:如果函数始终返回带有一组特定参数的相同结果,则该函数是幂等的。 但是,这并不意味着该函数不能影响其他组件-例如,非局部变量或输入/输出流。 例如,如果上述
add_three(number)
函数的幂等版本将结果输出到控制台,然后仅返回结果,则仍将其视为幂等,因为当它访问输入/输出流时,此访问操作不会影响返回的值从功能。
print()
调用只是一个
副作用 :与返回值一起发生的与程序或系统其余部分的交互。
让我们用
add_three(number)
开发示例。 您可以编写以下代码来确定
add_three(number)
被调用了多少次:
add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """, *add_three*.""" return add_three_calls
现在,我们将输出执行到控制台(这是一个副作用)并更改非局部变量(另一个副作用),但是由于这些都不影响函数返回的值,因此无论如何它都是幂等的。
纯功能没有副作用。 它不仅在计算值时不使用任何“外部数据”,而且不与程序/系统的其余部分进行交互,仅计算并返回指定的值。 因此,尽管我们对
add_three(number)
新定义仍然是幂等的,但此函数不再是纯函数。
在纯函数中,没有日志记录指令或
print()
调用。 在工作时,他们不访问数据库,也不使用Internet连接。 不要访问或修改非局部变量。
并且不要调用其他非纯函数 。
简而言之,它们没有爱因斯坦的话所表达的“可怕的远程作用”(但在计算机科学而非物理的情况下)。 它们不会以任何方式改变程序或系统的其余部分。 在
命令式编程 (这是用Python编写代码时所做的事情)中,此类函数是最安全的。 它们以可测试性和易于支持而著称。 此外,由于它们是幂等的,因此可以保证对这些功能的测试与执行一样快。 测试本身也很简单:您不必连接到数据库或模拟任何外部资源,无需准备代码的初始配置,并且在工作结束时无需清理任何内容。
老实说,幂等和清洁是非常理想的,但不是必需的。 , , , . , , , , . , , .
结论
仅此而已。 , – . . , . – ! . , , , « ». .