>  기사  >  백엔드 개발  >  Pythonic이라는 함수는 무엇입니까?

Pythonic이라는 함수는 무엇입니까?

coldplay.xixi
coldplay.xixi앞으로
2020-11-09 17:16:352489검색

python 동영상 튜토리얼 칼럼에서는 Pythonic을 소개합니다.

Pythonic이라는 함수는 무엇입니까?

머신 러닝에서는 데이터 읽기를 위한 함수 정의, 데이터 전처리를 위한 함수, 모델 아키텍처 및 훈련 프로세스를 위한 함수 정의 등 모델의 다양한 부분을 정의하기 위해 클래스와 함수를 사용해야 하는 경우가 많습니다. . 그렇다면 어떤 기능이 아름답고 눈을 즐겁게 할까요? 이 튜토리얼에서는 네이밍, 코드 크기 등 6가지 측면에서 멋진 함수를 개발하는 방법을 논의합니다. 글 끝에는 모두를 위한 동영상 튜토리얼이 녹화되어 있습니다. 필요에 따라 학습할 수 있으며, 불분명한 경우 메시지를 남길 수도 있습니다!

대부분의 최신 프로그래밍 언어와 마찬가지로 Python에서 함수는 추상화 및 캡슐화의 기본 방법 중 하나입니다. 개발 단계에서 수백 개의 함수를 작성했을 수도 있지만 모든 함수가 동일하게 생성되는 것은 아닙니다. "나쁜" 함수를 작성하면 코드의 가독성과 유지 관리 가능성에 직접적인 영향을 미칩니다. 그렇다면 "나쁜" 기능이란 어떤 기능일까요? 더 중요한 것은 "좋은" 함수를 어떻게 작성하는가입니다.

간단한 검토

수학은 우리가 기억하지 못하더라도 함수로 가득 차 있습니다. 먼저, 모두가 좋아하는 주제인 미적분학을 떠올려 보겠습니다. 이 방정식을 기억하실 것입니다: f(x) = 2x + 3. 이것은 알 수 없는 숫자 x를 취하고 2*x+3을 "반환"하는 "f"라는 함수입니다. 이 함수는 Python에서 보는 것과 동일하지 않을 수 있지만 기본 아이디어는 컴퓨터 언어의 함수와 동일합니다.

함수는 수학에서 오랜 역사를 가지고 있지만 컴퓨터 과학에서는 훨씬 더 강력합니다. 하지만 이 기능에는 몇 가지 결함이 있습니다. 다음으로 "좋은" 함수가 무엇인지, 그리고 이를 리팩토링하려면 어떤 신호가 필요한지 논의하겠습니다.

함수가 좋은지 아닌지를 판단하는 열쇠

좋은 파이썬 함수와 형편없는 파이썬 함수의 차이점은 무엇인가요? "좋은" 기능에 대한 정의가 얼마나 많은지 놀랍습니다. 우리의 목적을 위해 나는 다음 목록에 있는 대부분의 규칙을 준수하는 함수로 좋은 Python 함수를 정의하겠습니다(일부는 구현하기가 더 어렵습니다).

    값 반환 ​​
  • 50줄 이하의 코드
  • 무능하고 순수한 기능은 가능할 때마다 사용하세요
  • 이 목록은 많은 사람들에게 다소 제한적일 수 있습니다. 하지만 여러분의 함수가 이러한 규칙을 따른다면 여러분의 코드가 아름답게 보일 것이라고 장담합니다. 아래에서는 각 규칙을 단계별로 설명하고 이러한 규칙이 어떻게 "좋은" 기능을 구성하는지 요약하겠습니다.
  • Naming
이 문제에 대해 제가 가장 좋아하는 인용문 중 하나는 다음과 같습니다(Phil Karlton의 말, 항상 Donald Knuth로 오해됨).

컴퓨터 과학에는 두 가지 어려운 문제가 있습니다. 바로 캐시 무효화와 명명 문제입니다.

좀 이상하게 들리지만 전체적으로 좋은 네이밍은 정말 어렵습니다. 다음은 잘못된 함수 이름 지정의 예입니다.

def get_knn(from_df):复制代码
기본적으로 모든 곳에서 잘못된 이름 지정을 본 적이 있지만 이 예는 실무자가 Jupyter 노트북에 항상 코드를 작성하는 데이터 과학(또는 기계 학습)에서 나온 것입니다. 단위를 이해하기 쉬운 프로그램으로 만듭니다.

이 함수의 이름을 지정할 때 첫 번째 문제는 두문자어/약어를 사용하는 것입니다. 완전한 영어 단어가 약어나 인기 없는 두문자어보다 낫습니다. 약어를 사용하는 유일한 이유는 입력 시간을 절약하기 위한 것이지만 최신 편집기에는 자동 완성 기능이 있으므로 전체 이름을 한 번만 입력하면 됩니다. 약어가 문제가 되는 이유는 대개 특정 분야에서만 사용되기 때문입니다. 위 코드에서 knn은 "K-Nearest Neighbors"를 나타내고 df는 유비쿼터스 Pandas 데이터 구조인 "DataFrame"을 나타냅니다. 이러한 약어에 익숙하지 않은 다른 프로그래머가 코드를 읽고 있다면 그 사람은 혼란스러워질 것입니다.

이 함수 이름과 관련된 두 가지 사소한 문제: "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-8:https://www.python.org/dev/peps/pep-0008/
  • PEP-257:https://www.python.org/dev/peps/pep-0257/

首先文档注释是在定义模块、函数、类或方法的第一段字符串声明,这一段字符串应该需要描述清楚函数的作用、输入参数和返回参数等。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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.im에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제