Python 函数 - Functions in Python
函数的定义与调用
定义函数:使用 def 关键字来定义函数,语法为:
deffunction_name(parameters):
# 函数体
return value # 可选返回值
函数调用:函数通过函数名和参数来调用:
result = function_name(arguments)
返回值:函数使用 return 关键字返回值。一个函数只能返回一个值,但 Python 可以通过元组来模拟多个返回值。
defadd(x, y):
return x + y
defget_values():
return1, 2, 3# 实际上是返回了一个元组 (1, 2, 3)
# 通过解构赋值接收多个返回值
a, b, c = get_values()
yield:yield 用于生成器函数,它返回一个迭代器,而不是一次性返回所有结果。调用生成器函数时并不会立即执行函数内部的代码,而是返回一个生成器对象。生成器会在 yield 处暂停执行,并记住当前的状态(如变量值)。下次迭代时,它会从yield停止的地方继续执行,而不是从头开始。这意味着生成器不会一次性返回所有结果,而是按需生成。这使得生成器特别适合处理大量数据或者需要延时计算的场景。
defcount_up_to(limit):
count = 1
while count <= limit:
yield count
count += 1
for number in count_up_to(5):
print(number)
为什么使用 yield 而不是 return?
生成器也可配合 next() 内建函数使用:
defcount_up_to(limit):
count = 1
while count <= limit:
yield count
count += 1
# 创建生成器对象
counter = count_up_to(5)
# 使用 next() 显式获取下一个生成值
print(next(counter)) # 输出 1
print(next(counter)) # 输出 2
print(next(counter)) # 输出 3
print(next(counter)) # 输出 4
print(next(counter)) # 输出 5
- 延迟计算:
yield 允许按需生成结果,而不是一次性返回所有的值。对于大规模数据处理,生成器非常高效,因为它只在需要时生成下一个值,避免了内存占用。 - 状态保持:生成器保存了函数的状态。每次
yield 都会保存当前的执行状态(变量值等),使得下次从停止的位置继续执行,而不是从头开始。 - 惰性求值:生成器是“惰性求值”的,也就是说,只有在请求下一个值时,它才会计算并返回,这使得它能够在内存有限的情况下处理大量数据。
参数系统
参数传递(对象引用)
对象引用传递:Python 中的参数传递机制是 "对象引用传递",意味着函数接受的是对象的引用,而不是副本。
defmodify_list(lst):
lst.append(1)
my_list = [1, 2, 3, 4]
modify_list(my_list)
print(my_list) # 输出 [1, 2, 3, 4, 1]
- 对于可变对象(如列表、字典),在函数内部对其修改会影响原对象。
- 对于不可变对象(如整数、字符串、元组),函数内的修改不会影响原对象。
位置参数(Positional Arguments)
位置参数:按位置顺序传递给函数。
defgreet(name, age):
print(f"Hello {name}, you are {age} years old.")
greet("Alice", 30)
关键字参数(Keyword Arguments)
关键字参数:通过 key=value 方式显式传递,避免位置错误。
defgreet(name, age):
print(f"Hello {name}, you are {age} years old.")
greet(name="Alice", age=30)
默认参数(Default Parameters)
默认值:如果未传递某个参数,则使用默认值。
defgreet(name, age=30):
print(f"Hello {name}, you are {age} years old.")
greet("Alice") # age 使用默认值 30
可变默认参数陷阱:避免使用可变对象作为默认参数,因为它们在函数调用中会被共享。
defappend_to_list(value, lst=[]):
lst.append(value)
return lst
# 会发生意外的共享效果
result1 = append_to_list(1)
result2 = append_to_list(2)
print(result1) # 输出 [1, 2]
print(result2) # 输出 [1, 2],应避免此类问题
解决方法:使用 None 作为默认值并在函数内创建新的对象:
defappend_to_list(value, lst=None):
if lst isNone:
lst = []
lst.append(value)
return lst
*args 可变位置参数
可变位置参数:使用 *args 来接收任意数量的位置参数,返回一个元组。
defsum_numbers(*args):
print(type(args)) # 输出 <class 'tuple'>
return sum(args)
print(sum_numbers(1, 2, 3)) # 输出 6
**kwargs 可变关键字参数
可变关键字参数:使用 **kwargs 来接收任意数量的关键字参数,返回一个字典。
defprint_details(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
print_details(name="Alice", age=30)
- parameters 通常指定义函数时声明的形式参数(形参)。
- arguments 通常指函数调用传入的实际参数(实参)。
仅位置参数 /< 和仅关键字参数 *
仅位置参数 /<
通过 /< 来指定某些参数只能通过位置传递,不能通过关键字传递。
deffoo(a, b, /, c, d):# / 前面的是仅位置参数,后面的可通过关键字传递
print(a, b, c, d)
foo(1, 2, 3, 4) # 正确
foo(a=1, b=2, c=3, d=4) # 错误,a 和 b 不能作为关键字参数
foo(1, 2, c=3, d=4) # 正确,a、b 仅位置参数,c、d可作为关键字参数
仅关键字参数 *
通过 * 来指定某些参数只能通过关键字传递,也就是说,这些参数不能通过位置传递,只能显式地指定其名称。
deffoo(a, b, *, c, d):# * 后面的是仅关键字参数,前面的可通过关键字传递
print(a, b, c, d)
foo(1, 2, c=3, d=4) # a、b 通过位置传递,c、d 通过关键字传递
foo(1, 2, 3, 4) # 错误,c,d 只能通过关键字传递
foo(a=1, b=2, c=3, d=4) # 正确,a,b 也可以通过关键字参数传递
混合使用:
deffoo(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
在这个函数中:
c 和 d 是普通的位置参数,可以通过位置或关键字传递。
高级
函数作为对象
函数是第一类对象:函数可以作为参数传递、返回值、赋值给变量等。
defgreet(name):
returnf"Hello, {name}!"
say_hello = greet
print(say_hello("Alice"))
高阶函数
高阶函数(Higher-Order Function)是指接受一个或多个函数作为参数,或者返回一个函数的函数。换句话说,高阶函数的特点是它能够操作函数,使得函数的处理更加灵活和抽象。
defgreet(name):
returnf"Hello, {name}!"
defprocess_greeting(func, name):
# 接受一个函数和一个名字作为参数,并执行该函数
return func(name)
# 传递 greet 函数作为参数
result = process_greeting(greet, "Alice")
print(result) # 输出 "Hello, Alice!"
defmultiply_by(n):
defmultiplier(x):
return x * n
return multiplier
# 创建一个函数,该函数将数字乘以 2
double = multiply_by(2)
# 调用返回的函数
print(double(5)) # 输出 10
defouter(strParam):
definner(func):
print("Before function be called.")
func(strParam)
print("After function be called.")
return inner
defgreet(name):
print(f"Hello, {name}")
outer("Alice")(greet) # 高阶函数的调用,两次函数调用的链式组合
闭包 Closure
闭包是指:一个内嵌函数(即函数内部定义的函数)引用了其外部函数的变量,形成了对这些变量的"闭合"引用。换句话说,闭包允许内嵌继续访问外部函数的局部变量,即使外部函数已经执行完毕并返回。
用途:
- 延迟计算:闭包允许在外部函数执行时保存某些信息,并在稍后的时间里通过内嵌函数访问和操作这些信息。
- 状态封装:它可以封装一些状态信息,避免这些状态暴露给外部,达到更好的数据封装和保护。
闭包由两个部分组成:
- 外部函数:定义外部环境变量,通常是用于某些计算或状态。
- 内部函数:使用了外部函数的变量,并返回这个内部函数。
defouter(x):# 外部函数
definner(y):# 内部函数
return x + y # 使用外部函数的变量 x
return inner # 返回内部函数
# 创建一个闭包函数 add_five
add_five = outer(5)
# 使用闭包函数,传入参数 10
print(add_five(10)) # 输出 15
defcounter(start=0):# 外部函数,定义初始状态
defincrement():# 内部函数,改变并返回状态
nonlocal start # 使用外部函数的变量
start += 1
return start
return increment # 返回内部函数,形成闭包
# 创建一个计数器闭包
counter1 = counter(5)
print(counter1()) # 输出 6
print(counter1()) # 输出 7
print(counter1()) # 输出 8
装饰器是 Python 中非常常见的用法,它本质上就是通过闭包的方式对一个函数进行"包装",从而在不改变函数本身的代码的情况下,添加一些额外的功能。
defdecorator(func):# 外部函数
defwrapper():# 内部函数
print("Before function call")
func() # 调用原始函数
print("After function call")
return wrapper # 返回内部函数
@decorator
defgreet():
print("Hello!")
greet() # 调用装饰过的函数
解释:
decorator(func) 是外部函数,它接受一个函数 func。wrapper() 是内部函数,它在调用 func() 前后增加了一些行为。@decorator 语法等价于 greet = decorator(greet),它将 greet 函数作为参数传递给 decorator,返回的 wrapper 函数替代了原始的 greet 函数。- 每次调用
greet() 时,都会先打印“Before function call”,然后执行原始的 greet(),最后打印“After function call”。
lambda 表达式(匿名函数)
lambda 用于创建匿名函数的方法。通常,lambda 用于需要一个简单函数的场景,尤其是在一些短小的回调函数或者函数式编程的应用中。
lambda arguments: expression
arguments:函数的参数,可以有多个,用逗号分隔。也可没有参数。lambda: expressionexpression:函数体的表达式,lambda 会返回该表达式的计算结果。
使用场景:
在 map() 函数中使用 lambda:map() 函数会将指定函数应用到给定可迭代对象的每一个元素上,返回一个结果的迭代器。通常可以与 lambda 一起使用,以实现简单的转换。
numbers = [1, 2, 3, 4]
squared = map(lambda x: x ** 2, numbers)
print(list(squared)) # 输出 [1, 4, 9, 16]
在 filter() 函数中使用 lambda:filter() 函数用于筛选出符合条件的元素。lambda 可以简化条件判断的书写。
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = filter(lambda x: x % 2 == 0, numbers)
print(list(even_numbers)) # 输出 [2, 4, 6]
在 sorted() 函数中使用 lambda:sorted() 可以通过 key 参数指定排序的标准,而 lambda 函数通常用于定义复杂的排序规则。
pairs = [(1, 'one'), (3, 'three'), (2, 'two')]
sorted_pairs = sorted(pairs, key=lambda pair: pair[0])
print(sorted_pairs) # 输出 [(1, 'one'), (2, 'two'), (3, 'three')]
以上的 map()、filter()、sorted() 都是高阶函数。
递归 Recursion
递归:函数直接或间接调用自身,通常用于分治算法。需要有终止条件。
deffactorial(n):# 递归求 n 的阶乘
if n == 1:
return1
return n * factorial(n - 1)
装饰器 Decorators
装饰器:返回函数的函数,用于扩展原函数的功能,常用于日志、缓存、权限验证等。
缓存计算结果的装饰器应用:
defcache(func):
cached_results = {} # 存储缓存的结果
defwrapper(*args):
if args in cached_results:
print("Returning cached result for", args)
return cached_results[args] # 如果参数已经缓存,直接返回缓存结果
result = func(*args) # 否则计算结果
cached_results[args] = result # 缓存结果
return result
return wrapper
# 示例函数:计算斐波那契数列
@cache
deffibonacci(n):# 递归计算斐波那契数列
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 第一次计算,缓存中没有结果
print(fibonacci(10)) # 第二次计算,返回缓存的结果
使用装饰器包裹递归计算斐波那契数列的函数,并加上缓存功能。带有缓存的递归函数,相当于递归有了记忆,每次碰到相同的 n 就不用重复计算。正如那句名言:那些忘记过去的人,注定要重蹈覆辙。
装饰器用于权限验证:
defrequires_permission(permission):
defdecorator(func):
defwrapper(user, *args, **kwargs):
if permission notin user['permissions']:
raise PermissionError(f"User does not have '{permission}' permission")
return func(user, *args, **kwargs)
return wrapper
return decorator
# 假设有一个用户对象
user = {
'name': 'Alice',
'permissions': ['read', 'write']
}
# 示例函数:删除用户数据
@requires_permission('delete')
defdelete_user(user):
print(f"User {user['name']} deleted.")
# 测试
try:
delete_user(user) # 用户没有 delete 权限,会抛出异常
except PermissionError as e:
print(e)
# 为用户添加 delete 权限后再试
user['permissions'].append('delete')
delete_user(user) # 正常执行
带有参数的装饰器需要两层的函数调用,外层用于接收参数,内层用于接收被装饰的函数作为参数。
@requires_permission('delete')
defdelete_user(user):
print(f"User {user['name']} deleted.")
相当于:
defdelete_user(user):
print(f"User {user['name']} deleted.")
requires_permission('delete')(delete_user)
函数签名 Function Signature 与函数返回类型注解
Python 提供了 inspect 模块来帮助我们查看函数的签名,即函数参数列表定义部分。
import inspect
defexample(a, b=10, *args, **kwargs):
pass
# 查看函数签名
print(inspect.signature(example))
# 输出:(a, b=10, *args, **kwargs)
Python 3 引入了函数注解(Function Annotations)允许在函数签名中注明参数类型和返回值类型。这些注解并不影响函数的运行,但它们提供了额外的文档和类型信息,有助于静态分析工具或开发者理解函数的预期行为。
import inspect
defadd(a: int, b: int) -> int:
return a + b
defexample(a: int, b: int =10, *args, **kwargs) -> str:
return"success"
print(inspect.signature(example))
可调用对象 Callable Objects
可调用对象:类实例通过实现 __call__ 方法使得对象可以像函数一样被调用。
classMyCallable:
def__call__(self, name):
returnf"Hello, {name}"
obj = MyCallable()
print(obj("Alice")) # 输出 "Hello, Alice!"
动态函数(动态创建函数)
在 Python 中,动态函数指的是在程序运行时动态创建和执行的函数。与静态定义的函数不同,动态函数是通过代码运行时的字符串输入来创建和执行的。Python 提供了 exec() 和 eval() 两个内置函数来支持动态创建和执行函数
exec()
exec() 是 Python 中一个非常强大的内置函数,它可以执行存储在字符串中的 Python 代码。
用法:
exec() 可以执行多行 Python 代码,并且可以定义函数、类等。exec() 执行的代码字符串必须是有效的 Python 代码。
# 定义一个字符串,该字符串包含要执行的 Python 代码
code = """
def dynamic_function(x):
return x ** 2
"""
# 使用 exec() 执行字符串中的代码,动态定义函数
exec(code)
# 调用动态创建的函数
print(dynamic_function(4)) # 输出 16
eval()
eval() 与 exec() 类似,但它的功能有所不同。eval() 用于执行单个表达式,并返回表达式的值。它通常用于执行简单的表达式计算。
用法:
eval() 只接受一个单独的表达式,并返回计算结果。eval() 返回表达式的结果,而不像 exec() 只是执行代码。
# 动态计算表达式
result = eval("3 + 5 * 2")
print(result) # 输出 13
和 result = 3 + 5 * 2 的区别:
这两种情况下的最终结果都是 13 ,但它们在实现上有所不同。主要区别在于 动态计算 和 静态计算 的方式。
静态计算:在 result = 3 + 5 * 2 中,Python 直接计算表达式,结果是 13。这个表达式在代码编写时就已经确定下来,Python 解释器直接对其进行求值并赋值给 result。
动态计算:在 result = eval("3 + 5 * 2") 中,Python 运行时会首先将字符串 "3 + 5 * 2" 作为表达式传递给 eval(),然后 eval() 将其解析并计算。这里的表达式本身是在运行时(而不是编写代码时)确定的。
eval() 可以执行动态的字符串表达式,因此它适用于那些需要在程序运行时从外部输入或构建的代码进行计算的场景。eval() 会将传入的字符串当作 Python 表达式求值,这使得它非常灵活。例如,它允许用户输入动态的数学表达式,或者根据程序的需求构造出不同的表达式进行计算。
exec() 和 evel() 允许程序运行时让用户输入代码进行执行,但要慎用,防止用户输入有害系统的代码语句。
尾递归优化 Tail Call Optimization, TCO
尾递归优化:递归的最后一步直接返回结果,Python 不支持尾递归优化,但可以理解其原理。
递归的基本原理是,函数通过不断地调用自身,在内存中的调用栈上压入当前函数的执行状态,包括参数和局部变量等。递归过程会持续进行,直到满足终止条件,函数逐层返回,并将调用栈中的函数状态逐一弹出,返回给调用方。虽然在代码层面,递归函数通常非常简洁,但在内存管理层面,它较为复杂,特别是在递归深度较大或递归条件复杂时,会显著增加内存开销,并带来栈溢出的风险。
尾递归优化正是为了解决这一问题而提出的优化技术。它通过在编译器或解释器层面,识别递归函数中的尾调用并对其进行优化。具体来说,尾递归优化可以消除递归调用过程中对栈帧的累积,从而避免递归调用栈的持续增长,防止栈溢出。在尾递归优化的帮助下,递归调用的栈空间不会随着递归深度的增加而消耗更多内存,提高了程序的效率和稳定性。
普通递归函数求阶乘:
deffactorial(n):
if n == 1:
return1
else:
return n * factorial(n - 1) # 递归调用发生在乘法运算之前
尾递归优化求阶乘:
deffactorial_tail_recursive(n, accumulator=1):
if n == 1:
return accumulator
else:
return factorial_tail_recursive(n - 1, accumulator * n) # 递归调用发生在函数的最后一步
# 局部变量 accumulator 在递归调用的过程中保存了阶乘的值,在递归终止条件满足时,直接返回 accumulator 数值
在尾递归中,递归调用不需要保留当前的栈帧。也就是说,递归调用时,当前函数调用的栈帧可以被丢弃,新的函数调用栈帧会覆盖在当前栈帧上,从而节省内存。