程序员文章、书籍推荐和程序员创业信息与资源分享平台

网站首页 > 技术文章 正文

5个简单而有效的Python技巧,用于编写更好、更可维护的函数

hfteth 2024-12-27 17:35:49 技术文章 37 ℃

Python 中的函数可让编写模块化代码。当需要在多个地方执行一项任务时,可以将任务的逻辑包装到 Python 函数中。并且可以在每次需要执行该特定任务时调用该函数。虽然开始使用 Python 函数似乎很简单,但编写可维护且性能良好的函数并不那么简单。

一个函数只做一件事

在用 Python 编写函数时,人们常常会将所有相关任务放入一个函数中。虽然这可以帮助您快速编写代码,但在不久的将来,这只会使您的代码难以维护。这不仅会使理解函数的功能变得更加困难,还会导致其他问题,例如参数过多。

作为一种良好做法,应该始终让您数只做一件事(一项任务),并做好这件事。但有时,对于一项任务,可能需要完成一系列子任务。那么,如何决定是否以及如何重构该函数?

根据函数要执行的操作以及任务的复杂程度,可以确定子任务之间的关注点分离。然后确定一个合适的级别,可以在该级别将函数重构为多个函数 - 每个函数专注于特定的子任务。



这是一个例子。看一下这个函数analyze_and_report_sales

# fn. to analyze sales data, calculate sales metrics, and write it to a file
def analyze_and_report_sales(data, report_filename):
	total_sales = sum(item['price'] * item['quantity'] for item in data)
	average_sales = total_sales / len(data)
    
	with open(report_filename, 'w') as report_file:
    	    report_file.write(f"Total Sales: {total_sales}\n")
    	    report_file.write(f"Average Sales: {average_sales}\n")
    
	return total_sales, average_sales

很容易看出它可以重构为两个函数:一个计算销售指标,另一个将销售指标写入文件,如下所示:

# refactored into two funcs: one to calculate metrics and another to write sales report
def calculate_sales_metrics(data):
	total_sales = sum(item['price'] * item['quantity'] for item in data)
	average_sales = total_sales / len(data)
	return total_sales, average_sales

def write_sales_report(report_filename, total_sales, average_sales):
	with open(report_filename, 'w') as report_file:
    	    report_file.write(f"Total Sales: {total_sales}\n")
    	    report_file.write(f"Average Sales: {average_sales}\n")

现在,可以更轻松地分别调试与销售指标计算和文件操作相关的任何问题。以下是示例函数调用:

data = [{'price': 100, 'quantity': 2}, {'price': 200, 'quantity': 1}]
total_sales, average_sales = calculate_sales_metrics(data)
write_sales_report('sales_report.txt', total_sales, average_sales)

应该能够在工作目录中看到包含销售指标的“sales_report.txt”文件。这是一个简单的入门示例,但它非常有用,尤其是在您处理更复杂的函数时。

2. 添加类型提示以提高可维护性

Python 是一种动态类型语言。因此,无需为创建的变量声明类型。但您可以添加类型提示来指定变量的预期数据类型。定义函数时,可以为参数和返回值添加预期数据类型。

由于 Python 不会在运行时强制类型,因此添加类型提示在运行时不会产生任何效果。但使用类型提示仍然有好处,尤其是在可维护性方面:

  • 向 Python 函数添加类型提示可作为内联文档,并可以更好地了解函数的作用以及它使用和返回的值。
  • 当向函数添加类型提示时,可以配置 IDE 以利用这些类型提示。因此,如果您尝试在一个或多个函数调用中传递无效类型的参数、实现返回值与预期类型不匹配的函数等,将收到有用的警告。因此,您可以提前将错误降到最低。
  • 可以选择使用静态类型检查器(例如mypy)来尽早捕获错误,而不是让类型不匹配引入难以调试的细微错误。

以下是处理订单详情的函数:

# fn. to process orders
def process_orders(orders):
	total_quantity = sum(order['quantity'] for order in orders)
	total_value = sum(order['quantity'] * order['price'] for order in orders)
	return {
    	'total_quantity': total_quantity,
    	'total_value': total_value
	}

现在向函数添加类型提示,如下所示:

# modified with type hints
from typing import List, Dict

def process_orders(orders: List[Dict[str, float | int]]) -> Dict[str, float | int]:
	total_quantity = sum(order['quantity'] for order in orders)
	total_value = sum(order['quantity'] * order['price'] for order in orders)
	return {
    	'total_quantity': total_quantity,
    	'total_value': total_value
	}

通过修改后的版本,可以知道该函数接受一个字典列表。字典的键应全部为字符串,值可以是整数或浮点值。该函数还返回一个字典。现在进行一个示例函数调用:

# Sample data
orders = [
	{'price': 100.0, 'quantity': 2},
	{'price': 50.0, 'quantity': 5},
	{'price': 150.0, 'quantity': 1}
]

# Sample function call
result = process_orders(orders)
print(result)

输出如下:

{'total_quantity': 8, 'total_value': 600.0}

在此示例中,类型提示可帮助我们更好地了解函数的工作原理。

3. 只接受你真正需要的参数

如果是初学者或刚刚开始从事第一个开发角色,那么在定义函数签名时考虑不同的参数非常重要。确保函数只接受实际需要的参数,可以让函数调用更简洁、更易于维护。另外,函数中的参数过多也会使维护变得困难。那么,如何定义具有适当数量参数的易于维护的函数呢?

如果发现编写的函数的参数数量越来越多,第一步是从函数中删除所有未使用的参数。如果即使执行此步骤后参数仍然太多,请返回提示 1:将任务分解为多个子任务,并将函数重构为多个较小的函数。这将有助于控制参数数量。



现在来看一个简单的例子。这里计算学生成绩的函数定义包含
instructor从未使用过的参数:

# takes in an arg that's never used!
def process_student_grades(student_id, grades, course_name, instructor'):
	average_grade = sum(grades) / len(grades)
	return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."

可以像这样重写不带参数的函数instructor

# better version!
def process_student_grades(student_id: int, grades: list, course_name: str) -> str:
	average_grade = sum(grades) / len(grades)
	return f"Student {student_id} achieved an average grade of {average_grade:.2f} in {course_name}."

# Usage
student_id = 12345
grades = [85, 90, 75, 88, 92]
course_name = "Mathematics"
result = process_student_grades(student_id, grades, course_name)
print(result)

这是函数调用的输出:

Student 12345 achieved an average grade of 86.00 in Mathematics.

4. 强制使用关键字参数以最大程度地减少错误

实际上,大多数 Python 函数都接受多个参数。您可以将参数作为位置参数、关键字参数或两者的混合传递给 Python 函数。有些参数天生就是位置参数。但有时函数调用只包含位置参数可能会造成混淆。当函数接受多个相同数据类型的参数时尤其如此,有些参数是必需的,有些是可选的。

使用位置参数时,参数会按照它们在函数调用中出现的顺序传递给函数签名中的参数。因此,参数顺序的变化可能会引入细微的错误和类型错误

可选参数设为仅关键字通常很有帮助。这也使得添加可选参数变得更加容易 — 而不会破坏现有调用。

这是一个例子。该process_payment函数接受一个可选description字符串:

# example fn. for processing transaction
def process_payment(transaction_id: int, amount: float, currency: str, description: str = None):
	print(f"Processing transaction {transaction_id}...")
	print(f"Amount: {amount} {currency}")
	if description:
    		print(f"Description: {description}")

假设想让可选参数成为description仅关键字参数。可以这样做:

# enforce keyword-only arguments to minimize errors
# make the optional `description` arg keyword-only
def process_payment(transaction_id: int, amount: float, currency: str, *, description: str = None):
	print(f"Processing transaction {transaction_id}:")
	print(f"Amount: {amount} {currency}")
	if description:
    		print(f"Description: {description}")

现在来看一个示例函数调用:

process_payment(1234, 100.0, 'USD', description='Payment for services')

输出如下:

Processing transaction 1234...
Amount: 100.0 USD
Description: Payment for services

现在尝试将所有参数作为位置传递:

# throws error as we try to pass in more positional args than allowed!
process_payment(5678, 150.0, 'EUR', 'Invoice payment') 

将收到如下所示的错误:

Traceback (most recent call last):
  File "/home/balapriya/better-fns/tip4.py", line 9, in 
	process_payment(1234, 150.0, 'EUR', 'Invoice payment')
TypeError: process_payment() takes 3 positional arguments but 4 were given

5. 不要从函数返回列表;改用生成器

编写生成序列(例如值列表)的 Python 函数很常见。但应尽可能避免从 Python 函数返回列表。相反,可以将它们重写为生成器函数。生成器使用惰性求值;因此它们会按需生成序列的元素,而不是提前计算所有值。举例来说,以下函数可生成达到一定上限的斐波那契数列:

# returns a list of Fibonacci numbers
def generate_fibonacci_numbers_list(limit):
	fibonacci_numbers = [0, 1]
	while fibonacci_numbers[-1] + fibonacci_numbers[-2] <= limit:
    		fibonacci_numbers.append(fibonacci_numbers[-1] + fibonacci_numbers[-2])
	return fibonacci_numbers

这是一个递归实现,计算量很大,填充列表并返回列表似乎比必要的更冗长。以下是使用生成器的函数的改进版本:

# use generators instead
from typing import Generator

def generate_fibonacci_numbers(limit: int) -> Generator[int, None, None]:
	a, b = 0, 1
	while a <= limit:
    		yield a
    	a, b = b, a + b

在这种情况下,该函数返回一个生成器对象,然后您可以循环遍历该对象以获取序列的元素:

limit = 100
fibonacci_numbers_generator = generate_fibonacci_numbers(limit)
for num in fibonacci_numbers_generator:
	print(num)

输出如下:

0
1
1
2
3
5
8
13
21
34
55
89

如您所见,使用生成器可以提高效率,尤其是对于较大的输入大小。此外,您可以将多个生成器链接在一起,这样您就可以使用生成器创建高效的数据处理管道。

Tags:

最近发表
标签列表