以下是文章的整体概览:
首先,强调遵循单一职责原则,确保每个函数只执行一个任务。接下来,讨论了类型提示(type hints)为增强代码清晰度和长期可维护性带来的好处。
接着文章探讨了仅关键字参数(keyword-only)这一Python功能,它可以通过强制使用参数名称来最小化出错的几率。另一个建议是严格控制参数数量,仅使用函数功能所必要的参数,这样做可以减少复杂性和潜在的 bug。
最后,文章倡导使用生成器(Generator)这种内存高效利用的技术来返回可迭代数据,而不是一次性构造并返回整个列表。
无论你是新手还是经验丰富的开发者,都建议从现在开始应用这5个技巧,并在日常工作中不断实践,相信你可以在未来的编程生涯中编写出更高效、更具可读性和可维护性的函数,从而产生更高质量的代码,从而提高你的生产力。
1. 你的函数应该只做一件事
“你的函数应该只做一件事”原则是整洁代码和高效编程的核心原则。这一原则也被称为单一职责原则(Single Responsibility Principle, SRP),它的宗旨是建议一个函数应该只有一个职责或任务。这会让你的代码更易于阅读、测试、调试和维护。以下是应用此原则的一些优点:
- 可读性(Readability):当一个函数只做一件事时,会更容易理解,初一瞥就可以一目了然。函数名可以清楚地描述其目的(命名时尽可能做到见名知意👍),实现也很简单。
- 可重用性(Reusability):单一目的的函数可以在程序的不同部分或其他项目中重复使用,避免重复造轮子。
- 可测试性(Testability):编写针对一个单一功能函数的测试更容易,这种测试也更可靠。
- 可维护性(Maintainability):如果一个函数只负责一个任务,影响该任务的需求变更将局限在该函数内,减少了代码其他部分出现bug的风险。
假设你正在开发一个Python程序,处理一个数字列表,需要实现:
- 过滤掉负数。
- 对剩余的数字进行平方运算。
- 计算平方数的总和。
def filter_negative_numbers(numbers):
"""Filters out negative numbers from the list."""
return [num for num in numbers if num >= 0]
def square_numbers(numbers):
"""Return a list of squared numbers."""
return [num ** 2 for num in numbers]
def sum_numbers(numbers):
"""Return the sum of numbers."""
return sum(numbers)
def data_processing(numbers):
"""Process the list of nubers: filter, square, and sum."""
positive_numbers = filter_negative_numbers(numbers)
squared_numbers = square_numbers(positive_numbers)
total = sum_numbers(squared_numbers)
return total
if __name__ == '__main__':
numbers = [-2, -3, 4, -1, -2, 1, 5, -3]
result = data_processing(numbers)
print(result) # Output: 42
2. 增加类型提示以提高可读性和可维护性
类型提示(Type hints)是Python中一项非常棒的功能,允许你指定变量、函数参数和返回值的预期类型。该特征在 PEP 484 中引入,类型提示不会影响程序运行时的行为,而是提供了一种对代码执行静态类型检查的方式。它们提高了代码的可读性,并帮助开发人员理解预期的输入和输出类型,使代码更易于维护。添加类型提示可以改善以下几点:
- 提高可读性:通过明确指出函数期望的参数类型和返回值类型,类型提示使代码更具可读性,并且更容易理解,一目了然。
- 错误检测:可以使用像 mypy 这样的工具进行静态类型检查,在开发过程的早期就捕捉潜在的 bug。
- 文档:类型提示可以作为一种文档形式,提供有关如何使用函数和方法的宝贵信息。
在单一职责原则中我们提供了一个没有类型提示的示例,接下来我们将提供一个具有类型提示的版本,功能一样:
from typing import List
def filter_positive_numbers(numbers: List[int]) -> List[int]:
"""Filters out negative numbers from the list."""
return [num for num in numbers if num >= 0]
def square_positive_numbers(numbers: List[int]) -> List[int]:
"""Return a list of squared numbers."""
return [num ** 2 for num in numbers]
def sum_numbers(numbers: List[int]) -> int:
"""Return the sum of numbers."""
return sum(numbers)
def data_processing(numbers: List[int]) -> int:
"""Process the list of nubers: filter, square, and sum."""
positive_numbers = filter_positive_numbers(numbers)
squared_numbers = square_positive_numbers(positive_numbers)
total = sum_numbers(squared_numbers)
return total
if __name__ == '__main__':
numbers = [-2, -3, 4, -1, -2, 1, 5, -3]
result = data_processing(numbers)
print(result) # Output: 42
比较这两段代码,我们可以发现:
- 可读性(Readability)****
没有类型提示:阅读函数定义时,不太清楚期望什么类型的参数或函数返回什么类型。
有类型提示:函数签名明确指出它们使用整数列表,并返回整数列表或单个整数。
- 错误检测(Error Detection)
没有类型提示:类型相关的错误可能只会在运行时被捕获,这可能导致难以追踪的 bug。
有类型提示:像 mypy 这样的工具可以在编译时检查类型,在代码执行之前捕捉错误。
3. 强制使用仅关键字参数以最小化出错几率
强制使用“仅关键字参数”(keyword-only)是Python中的一种技术,该技术表明,调用函数时某些参数必须通过名称指定。
这是通过在函数定义中使用特殊语法来完成的,该语法可以防止这些参数以位置方式传递。这种方法可以显著提高代码的清晰度并减少错误。
在Python函数中强制使用“仅关键字参数”可以大大增强代码的清晰度和正确性。关键字参数只能使用参数名称指定。这种强制执行有助于:
- 防止错误:通过要求参数按名称传递,可以减少以错误顺序传递参数的风险,从而避免出现微不可查的 bug。
- 提高可读性:它使函数调用更具可读性,明确表示每个参数的含义。
- 增强灵活性:它允许你在不破坏现有代码的情况下,将来可以向函数添加更多参数,因为参数是显式命名的。
- 提高清晰度:它使代码的意图更加清晰,因为每个参数的目的都在调用点指定。
下面是一个邮件发送的示例。send_email 函数接受一个可选的 cc 字符串:
def send_email(recipient: str, subject: str, body: str, cc: str = None):
print(f"Sending email to {recipient}...")
print(f"Subject: {subject}")
print(f"Body: {body}")
if cc:
print(f"CC: {cc}")
if __name__ == '__main__':
send_email('jackzhang@example.com', '编写高效Python函数的5个技巧',
'本文将向大家分享5个让你编写高效Python函数的技巧...', 'cc@example.com')
假设您想将可选的 cc 参数设置为关键字参数。可以这样做:
# Make the optional 'cc' argument keyword-only
def send_email(recipient: str, subject: str, body: str, *, cc: str = None):
print(f"Sending email to {recipient}...")
print(f"Subject: {subject}")
print(f"Body: {body}")
if cc:
print(f"CC: {cc}")
if __name__ == '__main__':
send_email('jackzhang@example.com', '编写高效Python函数的5个技巧',
'本文将向大家分享5个让你编写高效Python函数的技巧...', cc='cc@example.com')
只需要在可选参数 cc 前使用 * 号将前后隔开即可将其后的参数变成仅关键字参数。如果你想了解更多关于关键字参数和位置参数的细节,请阅读我的另一篇文章(Python效率秘籍:使用“*” 和“/” 让你的函数参数看起来更整洁)。
现在我们再次调用函数时可选参数 cc 就必须用定义时的名称进行值传递:
send_email('jackzhang@example.com', '编写高效Python函数的5个技巧',
'本文将向大家分享5个让你编写高效Python函数的技巧...', cc='cc@example.com')
Sending email to jackzhang@example.com...
Subject: 编写高效Python函数的5个技巧
Body: 本文将向大家分享5个让你编写高效Python函数的技巧...
CC: cc@example.com
如果我们尝试像之前一样所有参数都采用位置参数传递:
send_email('jackzhang@example.com', '编写高效Python函数的5个技巧',
'本文将向大家分享5个让你编写高效Python函数的技巧...', 'cc@example.com')
你将会收到下面这样的报错信息:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[6], line 10
7 print(f"CC: {cc}")
9 if __name__ == '__main__':
---> 10 send_email('jackzhang@example.com', '编写高效Python函数的5个技巧',
11 '本文将向大家分享5个让你编写高效Python函数的技巧...', 'cc@example.com')
TypeError: send_email() takes 3 positional arguments but 4 were given
4. 你的函数应该只使用必要参数
在定义函数时,严格限制参数数量,将其限制为函数操作所必要的参数至关重要。不必要的参数会常常会导致混淆,使函数调用臃肿,并且会让维护变得更复杂。以下是你应该只使用必要参数的几个原因:
- 提高可读性:更少的参数使函数签名更简单、更易于理解。
- 增强可维护性:参数较少的函数更易于重构和测试。
- 减少错误:当函数只接受必要的参数时,可以降低传递不正确或冗余数据的可能性。
以下是一个冗余参数的函数示例:
# example function for processing an order including unnecessary arguments
def process_order(order_id: int, customer_id: int, customer_name: str,
amount: float, discount: float = 0.0):
print(f"Processing order {order_id} for customer {customer_id} - {customer_name}")
total_amount = amount * (1 - discount)
print(f"Total amount after discount: {total_amount}")
if __name__ == '__main__':
process_order(666, 888, 'Jack Zhang', 1000.0, 0.05)
Processing order 666 for customer 888 - Jack Zhang
Total amount after discount: 950.0
在这个例子中,函数 process_order 同时接受 customer_id 和 customer_name 参数,如果所有必需的信息都可以从 order_id 中获得,这两个参数可能是不必要的。
现在,让我们只使用必要的参数,重构该函数:
# example function for processing an order with only necessary arguments
def process_order(order_id: int, amount: float, discount: float = 0.0):
print(f"Processing order {order_id}")
total_amount = amount * (1 - discount)
print(f"Total amount after discount: {total_amount}")
if __name__ == '__main__':
process_order(666, 1000.0, 0.05)
5. 使用生成器返回列表
生成器(Generator)是Python中一种特殊的可迭代对象,它允许你逐个迭代序列值,而不需要一次性将整个序列存储在内存中。生成器通过函数和 yield 关键字进行定义,该关键字允许函数返回一个值并暂停其状态,在下次请求值时恢复。这使得生成器成为处理大型数据集或数据流的高效方式。
以下是你应该使用生成器的几个原因:
- 内存效率:生成器一次只生成一个数据项,并且是按需生成,这意味着它们不需要将整个序列存储在内存中。这对于大型数据集特别有用。
- 性能:由于生成器按需生成数据项,因此它们可以让程序性能得到提升,因为避免了创建和存储大型数据结构的开销。
- 惰性求值:生成器按需计算值,这可以得到更高效和响应性更强的程序。
- 更简单的代码:生成器可以简化创建迭代器所需的代码,使其更易于阅读和维护。
以下是一个不使用生成器返回列表的函数示例:
from typing import List
def get_squares(n: int) -> List[int]:
"""Return a list of squares from 0 to n-1."""
squares = []
for i in range(n):
squares.append(i * i)
return squares
if __name__ == '__main__':
squares_list = get_squares(10)
print(squares_list)
# Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
在这个例子中,函数 get_squares 在返回之前会生成并存储所有数值(0~n-1)的平方数。现在,我们改用生成器实现相同的功能:
from typing import List
def get_squares(n: int) -> List[int]:
"""Yield squares from 0 to n-1."""
for i in range(n):
yield i * i
if __name__ == '__main__':
squares_gen = get_squares(10)
print(list(squares_gen))
在Python中使用生成器返回列表可以在内存效率、性能和惰性求值方面带来显著优势。通过按需生成值,你可以更高效地处理大型数据集,编写更简洁、更易维护的代码。
两种方法的比较清楚地显示了使用生成器的好处,特别是对于大型或计算密集型序列的优势尤其显著。