#機械学習では、読み取り関数の定義など、モデルのさまざまな部分を定義するためにクラスと関数を使用する必要がよくあります。データ、データを前処理する関数、モデルのアーキテクチャとトレーニング プロセスの関数など。では、美しく、目に心地よい機能とはどのようなものでしょうか?このチュートリアルでは、名前やコード サイズなど 6 つの側面から素晴らしい関数を開発する方法について説明します。
記事の最後には、全員向けのビデオ チュートリアルが収録されています。必要に応じて学習でき、不明な点がある場合はメッセージを残すこともできます。
最新のプログラミング言語と同様、Python では関数は抽象化とカプセル化の基本的な方法の 1 つです。開発段階で何百もの関数を作成したかもしれませんが、すべての関数が同じように作成されるわけではありません。 「悪い」関数を作成すると、コードの可読性と保守性に直接影響します。では、「悪い」関数とはどのような関数でしょうか?さらに重要なのは、「良い」関数をどうやって書くかということです。簡単なおさらい
数学には、覚えていなくても関数がたくさんあります。まず、誰もが大好きなトピックである微積分を思い出してみましょう。次の方程式を覚えているかもしれません: f(x) = 2x 3。これは、未知の数 x を受け取り、2*x 3 を「返す」「f」という関数です。この関数は Python で見られるものと同じようには見えないかもしれませんが、基本的な考え方はコンピューター言語の関数と同じです。 関数は数学において長い歴史がありますが、コンピューター サイエンスではさらに強力です。それでも、この機能にはいくつかの欠陥があります。次に、「良い」関数とは何か、そしてそれをリファクタリングする必要がある兆候について説明します。関数の良し悪しを判断する鍵
良い Python 関数と質の悪い Python 関数の違いは何でしょうか? 「良い」関数の定義がどれほどたくさんあるのかには驚かされます。ここでは、優れた Python 関数を、次のリストのルールのほとんどに準拠する関数と定義します (実装がより難しいものもあります)。関数を 1 つ持つドキュメントのコメントを含む少し奇妙に聞こえますが、適切な名前を付けるのは本当に難しいです。不適切な関数名の例を次に示します。
def get_knn(from_df):复制代码
不適切な名前付けは基本的にどこでも見てきましたが、この例はデータ サイエンス (または機械学習) から来ており、実務者は常に Jupyter ノートブックを使用します。それにコードを記述してから、それらの異なる単元をわかりやすいプログラムに変えてみてください。
この関数に名前を付ける際の最初の問題は、頭字語/略語を使用していることです。完全な英単語は、略語や普及していない頭字語よりも優れています。略語を使用する唯一の理由は入力時間を節約することですが、最新のエディタにはオートコンプリート機能があるため、フルネームを 1 回入力するだけで済みます。略語が問題となる理由は、略語が通常、特定の分野でのみ使用されるためです。上記のコードでは、knn は「K-最近傍」を指し、df はユビキタス Pandas データ構造である「DataFrame」を指します。これらの略語に慣れていない別のプログラマがコードを読んでいると、その人は混乱するでしょう。 この関数名には他にも 2 つの小さな問題があります。「get」という単語は問題ではありません。ほとんどの適切な名前の関数では、その関数が何かを返すことは明らかであり、その名前はそれを反映しています。 from_dfも不要です。パラメーター名が十分に明確でない場合は、関数のドキュメントのコメントまたは型の注釈でパラメーターの型が説明されます。
では、この関数の名前を変更するにはどうすればよいでしょうか?例:
def k_nearest_neighbors(dataframe):复制代码
これで、素人でもこの関数が何を計算しているのかがわかり、パラメータ (データフレーム) の名前によって、どのタイプのパラメータを渡すべきかが明確にわかります。
単一機能の原則「单一功能原则」来自 Bob Martin「大叔」的一本书,不仅适用于类和模块,也同样适用于函数(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。该函数违反了只有一个原因能让函数改变的原则。显然有两个原因可以让该函数做出改变:新的或不同的数据需要计算或输出的格式需要改变。最好将该函数写成两个独立的函数:一个用来执行并返回计算结果;另一个用来接收结果并将其打印出来。函数有多重功能的一个致命漏洞是函数名称中含有单词「and」
这种分离还可以简化针对函数行为的测试,而且它们不仅被分离成一个模块中的两个函数,还可能在适当情况下存在于不同的模块中。这使得测试更加清洁、维护更加简单。
只做两件事的函数其实非常罕见。更常见的情况是一个函数负责许多许多任务。再次强调一下,为可读性、可测试性起见,我们应该将这些「多面手」函数分成一个一个的小函数,每个小函数只负责一项任务。
文档注释
很多 Python 开发者都知道 PEP-8,它定义了 Python 编程的风格指南,但很少有人了解定义了文档注释风格的 PEP-257。在这里并不会详细介绍 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 语句的函数,它仍然会返回某些东西。不过函数也应该要返回一些东西,因为它也是一个小程序。没有输出的程序又会有多少用,我们又如何测试它呢?
我甚至希望发表以下声明:每一个函数都应该返回一个有用的值,即使这个值仅可用来测试。我们写的代码应该需要得到测试,而不带返回值的函数很难测试它的正确性,上面的函数可能需要重定向 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') 能够正常运行,因为字符串方法 (strip(), lower(), endswith()) 会返回一个字符串以作为调用函数的结果。
以下是人们在被问及为什么他们写的函数没有返回值时给出的一些常见原因:
「函数所做的就是类似 I/O 的操作,例如将一个值保存到数据库中,这种函数不能返回有用的输出。」
我并不同意这种观点,因为在操作成功完成时,函数可以返回 True。
「我需要返回多个值,因为只返回一个值并不能代表什么。」
当然也可以返回包含多个值的一个元组。简而言之,即使在现有的代码库中,从函数返回一个值肯定是一个好主意,并且不太可能破坏任何东西。
函数长度
函数的长度直接影响了可读性,因而会影响可维护性。因此要保证你的函数长度足够短。50 行的函数对我而言是个合理的长度。
如果函数遵循单一功能原则,一般而言其长度会非常短。如果函数是纯函数或幂等函数(下面会讨论),它的长度也会较短。这些想法对于构造简洁的代码很有帮助。
那么如果一个函数太长该怎么办?代码重构(refactor)!代码重构很可能是你写代码时一直在做的事情,即使你对这个术语并不熟悉。它的含义是:在不改变程序行为的前提下改变程序的结构。因此从一个长函数提取几行代码并转换为属于该函数的函数也是一种代码重构。这也是将长函数缩短最快和最常用的方法。只要适当给这些新函数命名,代码的阅读将变得更加容易。
幂等性和函数纯度
幂等函数(idempotent function)在给定相同变量参数集时会返回相同的值,无论它被调用多少次。函数的结果不依赖于非局部变量、参数的易变性或来自任何 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,即用户输入的数字。每次调用这个函数时,它都可能返回不同的值。如果它被调用两次,则用户可以第一次输入 3,第二次输入 7,使得对 add_three() 的调用分别返回 6 和 10。
为什么幂等很重要?
可测试性和可维护性。幂等函数易于测试,因为它们在使用相同参数的情况下会返回同样的结果。测试就是检查对函数的不同调用所返回的值是否符合预期。此外,对幂等函数的测试很快,这在单元测试(Unit Testing)中非常重要,但经常被忽视。重构幂等函数也很简单。不管你如何改变函数以外的代码,使用同样的参数调用函数所返回的值都是一样的。
什么是「纯」函数?
在函数编程中,如果函数是幂等函数且没有明显的副作用(side effect),则它就是纯函数。记住,幂等函数表示在给定参数集的情况下该函数总是返回相同的结果,不能使用任何外部因素来计算结果。但是,这并不意味着幂等函数无法影响非局部变量(non-local variable)或 I/O stream 等。例如,如果上文中 add_three(number) 的幂等版本在返回结果之前先输出了结果,它仍然是幂等的,因为它访问了 I/O stream,这不会影响函数的返回值。调用 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() 调用,不使用数据库或互联网连接,不访问或修改非局部变量。它们不调用任何其它的非纯函数。
总之,纯函数无法(在计算机科学背景中)做到爱因斯坦所说的「幽灵般的远距效应」(spooky action at a distance)。它们不以任何形式修改程序或系统的其余部分。在命令式编程中(写 Python 代码就是命令式编程),它们是最安全的函数。它们非常好测试和维护,甚至在这方面优于纯粹的幂等函数。测试纯函数的速度与执行速度几乎一样快。而且测试很简单:没有数据库连接或其它外部资源,不要求设置代码,测试结束后也不需要清理什么。
显然,幂等和纯函数是锦上添花,但并非必需。即,由于上述优点,我们喜欢写纯函数或幂等函数,但并不是所有时候都可以写出它们。关键在于,我们本能地在开始部署代码的时候就想着剔除副作用和外部依赖。这使得我们所写的每一行代码都更容易测试,即使并没有写纯函数或幂等函数。
总结
写出好的函数的奥秘不再是秘密。只需按照一些完备的最佳实践和经验法则。希望本期教程能够帮助到大家。
関連する無料学習の推奨事項: Python ビデオ チュートリアル
以上がPythonicとはどのような機能なのでしょうか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。