目录
- 0. 前言
- 1. 最基础的自定义函数写法
- 2. 参数传递
- 3. 函数返回值
- 4. 作用域
- 5. 匿名函数
- 6. 装饰器
- 7. 闭包
- 8. 生成器
- 9. 为自定义函数写注解
0. 前言
今天来深入学习 Python 中的自定义函数,为的是日后能够写出时间和内存更好的优化,以及形成良好的编程习惯。
操作系统:Windows10 专业版
开发环境:Pycahrm Comunity 2022.3
Python解释器版本:Python3.8
1. 最基础的自定义函数写法
以下这种自定义函数写法应该是很多人所用的方式了,包括我本人也喜欢这么做:
test.py
# 函数定义
def greet(name):
print(f"Hello, {name}!")
# 函数调用
greet("Alice")
# 输出:Hello, Alice!
2. 参数传递
这里我举例了三种参数传递的方式,你可以通过 关键字、位置以及默认参数来为函数传递参数,这些你已经在Python基础中有所了解:
# 位置参数
def add(x, y):
return x + y
result = add(3, 4)
print(result)
# 输出:7
# 关键字参数
def person_info(name, age):
print(f"Name: {name}")
print(f"Age: {age}")
person_info(age=25, name="Alice")
# 输出:
# Name: Alice
# Age: 25
# 默认参数
def greet(name, message="Hello"):
print(f"{message}, {name}!")
greet("Alice")
# 输出:Hello, Alice!
greet("Bob", "Hi")
# 输出:Hi, Bob!
3. 函数返回值
函数返回值就是返回你需要的运算结果(可以是各种数值,字符,类等对象),你只需要在函数结束的位置添加上 return num 就能够获取到你想要的返回值:
def add_and_subtract(x, y):
add_result = x + y
sub_result = x - y
return add_result, sub_result
result1, result2 = add_and_subtract(5, 3)
print(result1) # 输出:8
print(result2) # 输出:2
4. 作用域
变量的作用域,顾名思义就是变量的生效范围,包括全局作用域和局部作用域,以及如何在不同的作用域中声明和使用变量。
# 全局变量
count = 0
def increment():
global count # 使用 global 关键字声明对全局变量的引用
count += 1
increment()
print(count) # 输出:1
# 局部变量
def multiply(x, y):
result = x * y # result 是一个局部变量,只在 multiply 函数内可见
return result
print(multiply(3, 4)) # 输出:12
print(result) # 会引发报错(由于result仅在multiply()生效导致)
5. 匿名函数
学习使用 lambda 关键字创建匿名函数。这些函数是一种简化定义和使用的特殊函数,可以在需要函数的地方使用。
我自认为这种函数完完全全就是为了增加他人的阅读难度,所以不推荐使用,但我还是在下方举例:
# 使用匿名函数计算两个数的和
add = lambda x, y: x + y
result = add(3, 4)
print(result) # 输出:7
6. 装饰器
在装饰器模式中,wrapper 是一个内部函数(也称为闭包),它由装饰器函数返回。wrapper 函数用于将额外的功能添加到原始函数上。
在装饰器中,wrapper 函数通常用于在调用原始函数之前或之后执行一些操作,或者在其执行过程中进行修改。这样,通过调用 wrapper 函数,可以实现装饰器所需的额外功能。
在装饰器的实现中,wrapper 函数的参数列表通常使用 *args 和 **kwargs,以便能够接收任意数量和类型的位置参数和关键字参数,并将它们传递给原始函数。通过调用 func(*args, **kwargs),可以调用原始函数并获取其返回值。
在装饰器的返回语句中,通常会返回 wrapper 函数本身。在使用 @装饰器 语法将装饰器应用到函数上时,实际上是将原始函数作为参数传递给装饰器函数,并将装饰器函数的返回值即 wrapper 函数作为新的函数定义。
wrapper 函数是装饰器的核心部分,它代表了一个新的函数,通过在原始函数的前后注入额外的代码或修改其行为,实现了装饰器所需的功能。
例如,我们接下来为 print() 函数添加时间戳
import datetime
# 定义装饰器
def timestamp_decorator(func):
def wrapper(*args, **kwargs):
# 获取当前时间
timestamp = datetime.datetime.now()
# 格式化时间戳
formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
# 调用原函数并打印时间戳
print("[{}]".format(formatted_timestamp), end=" ")
return func(*args, **kwargs)
return wrapper
@timestamp_decorator
def print_with_timestamp(*args):
print(*args)
# 使用
print_with_timestamp("Hello, World!")
# 打印以下内容
# [2023-07-16 02:04:59] Hello, World!
这段代码先定义了一个装饰器,然后往下使用了“@”调用了装饰器函数为print_with_timestamp函数装饰。
这样使用print_with_timestamp打印的函数会带上时间戳,这在服务器中是很常见的,因为服务器需要记录某一时刻发生的事件。
其实使用装饰器与下面这段代码效果是一样的:
def print_with_time(*args):
# 获取当前时间
timestamp = datetime.datetime.now()
# 格式化时间戳
formatted_timestamp = timestamp.strftime("%Y-%m-%d %H:%M:%S")
# 调用原函数并打印时间戳
print("[{}] {}".format(formatted_timestamp,*args))
print_with_time("Hello, World!")
但是如果有很多个函数需要你记录时间戳呢?为所有函数添上相同重复的语句显然不太合适,主函数中多次调用打印时间戳的函数也会影响美观,所以使用装饰器是最佳的选择。
7. 闭包
闭包是一种在函数内部定义的函数,并且它可以访问外部函数的变量。
换句话说,闭包是一个函数对象,它能够记住并访问创建它时所在的环境中的变量,即使创建它的环境不存在了。
当一个内部函数引用了外部函数的变量,并且在外部函数执行完毕之后仍然存在,这个内部函数就被认为是一个闭包。
闭包的主要优点是:
- 它可以保护隐藏在外部函数作用域中的数据,使其不能被意外修改。
- 它可以为外部函数提供一种延续性,即使外部函数已经执行完毕,闭包仍然可以访问和使用外部函数的变量。
- 它可以允许我们在多个函数调用之间共享数据。
这些内容很绕,如果你还是不明白,你可以来运行一下以下示例:
def color_print(color):
def print_with_color(text):
print(f"\033[{color}m{text}\033[0m")
return print_with_color
# 创建不同的颜色打印函数
red_print = color_print("31") # 红色
green_print = color_print("32") # 绿色
yellow_print = color_print("33") # 黄色
# 使用颜色打印函数
red_print("Hello, World!")
green_print("Hello, World!")
yellow_print("Hello, World!")
它的运行效果如下:
我们定义了一个color_print闭包函数,它接受一个color参数。
color_print函数内部定义了一个print_with_color函数,它会打印带有指定颜色的文本。color_print函数返回这个print_with_color函数,形成了闭包。
我们可以通过调用color_print函数并指定颜色参数来创建不同颜色的打印函数。我们可以通过调用这些打印函数来打印相应颜色的文本。
8. 生成器
当我们使用生成器函数时,我们使用yield关键字来定义一个迭代器。生成器函数每次调用都会生成一个值,并返回给迭代器,而不是一次性返回所有的值。这样,在迭代器中只会保留当前生成的值,而不会占用额外的内存来存储整个数据序列。
以下是生成器的优点:
-
节省内存:生成器函数可以生成大数据序列,而不会在内存中一次性存储整个序列。这对于处理非常大的数据集或无限序列特别有用。
-
惰性计算:生成器函数可以进行惰性计算,即只在需要时才生成值。这在处理复杂计算或需要大量计算的情况下很有用,因为它只会在需要时进行计算,并不会浪费时间和资源。
-
无限序列:生成器函数可以用于生成无限序列,例如斐波那契数列或素数序列。通过生成器函数,我们可以在需要时生成无限序列的下一个值,而不必事先计算完整的序列。
-
管道处理:生成器函数可以用作数据处理的管道,通过一系列生成器对数据进行连续处理。每个生成器在接收到上一个生成器的输出后,生成并返回下一个值,从而实现一系列的数据处理操作。
-
状态保留:生成器函数可以保留其状态,使得函数在多次调用之间可以“记住”之前的状态。这对于需要在多个步骤中共享信息或保存上下文很有用。
生成器提供了一种更灵活、高效、节省内存的迭代数据序列的方式,对于处理大数据集、惰性计算、管道处理和无限序列等情况非常有用。它们可以极大地提升代码的可读性、效率和性能。
下面我将使用一些示例带你感受它的魅力:
# 1. 节省内存,使用生成器函数生成大数据序列
def large_data_generator(n):
for i in range(n):
yield i
# 使用示例
data = large_data_generator(1000000) # 生成一个大数据序列
for item in data:
print(item)
生成器函数每次只生成一个值,并在需要时生成下一个值,这样就不会一次性生成整个序列,从而减少了内存的使用。
# 2. 惰性计算,根据需要生成值
def lazy_computation():
x = 1
yield x
x += 1
yield x
x *= 2
yield x
computation = lazy_computation() # 进行惰性计算
print(next(computation))
print(next(computation))
print(next(computation))
函数lazy_computation()定义了一个生成器函数,该函数在每次调用时生成一个值并通过yield语句返回。
在每次生成一个值之后,函数会暂停执行,等待下一次调用。这样可以逐步计算值,而不是一次性计算所有值。
# 3. 生成无限序列,例如斐波那契数列
def fibonacci_sequence():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fibonacci = fibonacci_sequence() # 生成无限斐波那契数列
for _ in range(10):
print(next(fibonacci))
函数fibonacci_sequence()定义了一个生成器函数,使用while循环生成斐波那契数列。在每次循环迭代中,通过yield语句返回当前的斐波那契数。同时,通过同时更新变量a和b来计算下一个斐波那契数。
我们创建了一个生成器对象fibonacci并通过循环调用next()函数来获取生成器返回的值。由于生成器函数是用while True语句定义的,所以它会无限地生成新的斐波那契数。
# 4. 管道处理,使用连续的生成器函数进行数据处理
def pipeline_processing(data):
def stage_a(data):
for item in data:
yield item * 2
def stage_b(data):
for item in data:
yield item + 5
for result in stage_b(stage_a(data)):
yield result
numbers = [1, 2, 3, 4, 5] # 管道处理
processed_data = pipeline_processing(numbers)
for item in processed_data:
print(item)
函数pipeline_processing()定义了一个主要的生成器函数,它接收一个数据输入,并经过一系列的阶段处理后生成处理后的数据输出。每个阶段都是一个内部的生成器函数。
函数stage_a()和stage_b()是这两个处理阶段的生成器函数。它们分别对输入数据进行不同的处理,通过yield语句返回处理后的结果。
在pipeline_processing()函数中,先调用stage_a()生成器函数对输入数据进行处理,然后将处理后的结果作为参数传递给stage_b()生成器函数进行下一阶段的处理。在最后一阶段的循环中,通过yield语句返回最终处理结果。
对于输入数据 [1, 2, 3, 4, 5],经过处理后的输出如下: 7 9 11 13 15
# 5. 状态保留,生成器函数可以保留上一个状态
def stateful_generator():
state = 0
while True:
yield state
state += 1
state = stateful_generator() # 状态保留的生成器函数
print(next(state))
print(next(state))
print(next(state))
函数stateful_generator()定义了一个生成器函数,使用变量state来保存状态。每次循环迭代时,通过yield语句返回当前的状态,并在下一次迭代之前更新状态。
我们创建了一个生成器对象state,并通过调用next()函数来获取生成器返回的值。每次调用next()函数时,生成器会从上一次yield语句的位置恢复执行,并返回生成器函数的当前状态。
将输出: 0 1 2
这是因为每次调用next(state),都会获取生成器函数的下一个状态并打印出来。
9. 为自定义函数写注解
相信大家在写代码时候肯定遇到过这种代码,会遇到读不懂的情况:
from typing import List
def fun(questions: List[int]) -> int:
"""
这是一个示例函数,接受一个整数列表作为输入,并返回一个整数值。
参数:
- questions: 整数列表,包含待处理的问题
返回:
- 整数值,表示问题的答案
"""
# 函数体
return 1
其实questions是我们传进去的参数,“:List[int]” 表面了传入的questions类型是整数列表
最后的 " -> int" 表明函数的返回值是int类型。
需要注意的是这些东西都只起到提示作用,并不会改变传入值返回值的类型和值。
这段代码的优点如下:
-
类型注解:通过在函数定义的参数和返回值上进行类型注解,明确指定了函数的输入和输出类型,这样其他开发人员在使用和阅读代码时更容易理解函数的预期行为和使用方式。
-
错误预防:类型注解可以帮助开发人员在编写代码时捕获潜在的类型错误。如果无意中将其他类型的数据传递给函数,或者函数返回了错误的类型,类型检查器可以在开发阶段或静态分析阶段发现问题,避免了在运行时出现潜在错误。
-
文档说明:类型注解可以作为代码文档的一部分,帮助其他开发人员了解函数的预期输入和输出。它可以增加代码的可读性,让其他人更容易理解函数的目的和返回值的含义。