第七章 函数
7.1 可接受任意数量参数的函数
# 接受任意数量位置参数的函数
# 只要使用*开头的参数即可
def avg(first, *rest):
return (first + sum(rest)) / (1 + len(rest))
# avg(1, 2, 3, 4)
# rest会被包装成一个元组 包含其余的位置参数
# 接受任意数量关键字参数
import html
# attrs是一个字典 包含所有关键字参数(可以为空)
def make_element(name, value, **attrs):
keyvals = [' %s="%s"' % item for item in attrs.items()]
# 使用列表推导式将 attrs 中的所有键值对转换为类似 key=value 的字符串
# attrs = {'id': 'my-id', 'class': 'highlight'}
attr_str = ''.join(keyvals)
# 'id="my-id" class="highlight"'
element = '<{name}{attrs}>{value}</{name}>'.format(
name=name,
attrs=attr_str,
value=html.escape(value))
return element
# make_element('item', 'Albatross', size='large', quantity=6)
# '<item size="large" quantity="6">Albatross</item>'
# 同时接受位置参数与关键字参数
def anyargs(*args, **kwargs):
print(args) # A tuple
print(kwargs) # A dict
在函数定义中,以*开头的参数只能作为最后一个位置参数出现,而以**开头的参数只 能作为最后一个参数出现。
*args
是打包所有多余的位置参数 **kwargs
是打包所有关键字参数
7.2 只接受关键字参数的函数
# 设计函数只通过关键字的形式接受特定的参数
# 将关键字参数放置在以*开头的参数或者是一个单独的*之后
def recv(maxsize, *, block):
'Receives a message'
pass
recv(1024, True) # TypeError
recv(1024, block=True) *# Ok
# * 开始之后的所有参数都必须以关键字方式传入
这项技术也可以用来为那些可接受任意数量的位置参数的函数来指定关键字参数。示 例如下:
# 为可接受任意数量的位置参数的函数来指定关键字参数
# 实现一个带下限的最小值功能
def minimum(*values, clip=None):
m = min(values)
if clip is not None:
m = clip if clip > m else m
return m
minimum(1, 5, 2, -5, 10) # -5 这个输入标识没有默认的最小值
minimum(1, 5, 2, -5, 10, clip=0) # 0 clip一定要使用关键字输入
# keyword-only可以提高代码的可读性 例如此时用户可以知道是否要实现block功能
msg = recv(1024, block=False)
7.3 将元数据信息附加到函数参数上
# 为参数添加额外信息
# 使用参数注解 标明参数和返回值的类型
def add(x: int, y: int) -> int:
return x + y
# python解释器不会附加任何语法意义到参数注解上 你输入什么都可以
def bar(a: 42, b: Myfunction(), c: [1, 2, 3]) -> "要有一个返回值":
pass
# 使用help()指令
>>> help(add)
Help on function add in module __main__:
add(x: int, y: int) -> int
# 函数注解只会保存在函数的__annotations__属性中
>>> add.__annotations__
{'y': <class 'int'>, 'return': <class 'int'>, 'x': <class 'int'>}
7.4 从函数中返回多个值
# 让函数返回多个值
def myfun():
# 函数会返回一个元组
return 1, 2, 3
# 利用解包进行赋值
a, b, c = myfun() # a=1,b=2,c=3
x = myfun() # x=(1,2,3)
# 补充点 可以使用两种方式创建元组
a = (1, 2)
b = 1, 2
7.5 定义带有默认参数的函数
# 定义默认参数
# 在定义中为参数赋 值,并确保默认参数出现在最后
def spam(a, b=42):
print(a, b)
spam(1) # a=1, b=42
spam(1, 2) # a=1, b=2
# 如果默认值是可变容器,应该把 None 作为默认值
def spam(a, b=None):
if b is None:
b = []
# 错误的定义如:def bad_func(a, b=[]): 所有对于b的修改会共享同一个列表 这样是不符合期望的
spam(1, None) # b = None_no_value = object()
# 判断某个参数是否被显式传入,即使它传的是 None
def spam(a, b=_no_value):
if b is _no_value:
print('未输入b')
spam(1) # 未输入b
spam(1, 2) # b = 2
spam(1, None) # b = None
# 未传值和穿None是不一样的
# 对默认参数的赋值只会在函数定义的时候绑定一次
x = 42
def spam(a, b=x):
print(a, b)
spam(1) # 1 42
x = 23 # 无效操作
spam(1) # 1 42
# 给默认参数赋值的应该总是不可变的对象,比如 None、True、False、数字或者字符串
# 如果默认值在函数体之外被修改了,那么这种修改将在之后的函数调用中对参数的默认值产生持续的影响
# 错误案例
def spam(a, b=[]):
print(b)
return b
x = spam(1) # 输出: []
x.append(2)
x = spam(1) # 输出: [2]
# 检测默认参数时应该使用is 而不是not
# 错误示范
def spam(a, b=None):
if not b:
b = []
spam(1) # 暂时正常
x = []
spam(1, x) # x是一个空列表 not x成立
spam(1, 0) # 对0和空字符串同理
spam(1, '')
# 正确写法 def spam(a, b=None):
7.6 定义匿名或内联函数
# 使用lambda 表达式
add = lambda x, y: x + y
add(2,3) # 5
add('hello', 'world') # 'helloworld'
# 和函数功能是相同的
def add(x, y):
return x + y
# 在排序或者对数据进行整理时很好用
names = ['David Beazley', 'Brian Jones', 'Raymond Hettinger', 'Ned Batchelder']
# 根据姓氏 name.split()[-1]进行排序且忽略大小写
sorted(names, key=lambda name: name.split()[-1].lower())
['Ned Batchelder', 'David Beazley', 'Raymond Hettinger', 'Brian Jones']
7.7 在匿名函数中绑定变量的值
# 在定义匿名函数时实现特定变量的绑定
x = 10
a = lambda y: x + y
x = 20
b = lambda y: x + y
# lambda 表达式中的x是一个自由变量,在运行时才进行绑定
a(10) # 30
b(10) # 30
# 通过默认参数实现匿名函数在定义时绑定变量
x = 10
a = lambda y, x=x: x + y
x = 20
b = lambda y, x=x: x + y
print(a(10)) # 输出: 20
print(b(10)) # 输出: 30
# 一些错误案例
funcs = [lambda x: x + n for n in range(5)]
# 所有的lambda函数都引用了同一个变量 n ,而这个 n 是在循环结束后才被调用的
# 当循环结束时,n 的值是 4
for f in funcs:
print(f(0))
# 会循环的输出4
funcs = [lambda x, n=n: x + n for n in range(5)]
# 将每次循环中的 n 值作为默认参数保存下来 使得表达式可以捕捉定义时的n
for f in funcs:
print(f(0))
# 输出0到4
7.8 减少调用函数所需要的参数
# 减少函数的参数数量,应该使用 functools.partial()
# partial()允许我们给一个或多个参数指定固定的值,以此减少需要提供给之后调用的参数数量
from functools import partial
def spam(a, b, c, d):
print(a, b, c, d)
s1 = partial(spam, 1) # 固定 a=1
s1(2, 3, 4) # 输出: 1 2 3 4
s1(4, 5, 6) # 输出: 1 4 5 6
s2 = partial(spam, d=42) # 固定 d=42
s2(1, 2, 3) # 输出: 1 2 3 42
s2(4, 5, 5) # 输出: 4 5 5 42
s3 = partial(spam, 1, 2, d=42) # 固定 a=1, b=2, d=42
s3(3) # 输出: 1 2 3 42
s3(4) # 输出: 1 2 4 42
s3(5) # 输出: 1 2 5 42
# partial()对特定的参数赋了固定值并返回了一个全新的可调用对象,新的可调用对象将传递给 partial()的固定参数结合起来,统一将所有的参数传递给原始的函数。
# 一个使用案例
import math
from functools import partial
# 计算两点之间的欧几里得距离
def distance(p1, p2):
x1, y1 = p1
x2, y2 = p2
return math.hypot(x2 - x1, y2 - y1)
points = [(1, 2), (3, 4), (5, 6), (7, 8)]
pt = (4, 3)
# 使用 partial 把 distance 的第一个参数固定为 pt
# 根据与pt间的距离进行排序
points.sort(key=partial(distance, pt))
print(points)
# 输出: [(3, 4), (1, 2), (5, 6), (7, 8)]
# 使用案例2
import logging
from multiprocessing import Pool
from functools import partial
# 用于输出结果
# 如果提供了log 输出debug级别的参数
def output_result(result, log=None):
if log is not None:
log.debug('Got: %r', result)
# 示例函数
def add(x, y):
return x + y
if __name__ == '__main__':
# 配置日志系统
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('test')
# 创建进程池
with Pool() as p:
# 使用 partial 固定 log 参数
# 在子进程中异步调用 add(3, 4)
# 当add执行完后会自动调用callback指定的方法
# multiprocessing.Pool.apply_async(),在调用回调函数时只传递任务结果这一个参数
# 在回调中同时使用 log 参数,就需要使用 partial() 来“预绑定”这个参数。
p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
# 等待异步任务完成
p.close()
p.join()
7.9 用函数替代只有单个方法的类
# 通常只有单个方法的类可以通过闭包(closure)将其转换成函数
from urllib.request import urlopen
class UrlTemplate:
def __init__(self, template):
# 接受一个url模板字符串 http://example.com/{param}
self.template = template
def open(self, **kwargs):
# 使用 format_map 将参数代入模板生成完整 URL
url = self.template.format_map(kwargs)
return urlopen(url)
if __name__ == '__main__':
# 创建模板对象,用于从 Yahoo 获取股票数据
yahoo = UrlTemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
# 发起请求并读取结果
with yahoo.open(names='IBM,AAPL,FB', fields='sl1c1v') as response:
for line in response:
print(line.decode('utf-8'))
# 构建一个函数替代上述代码
from urllib.request import urlopen
# 闭包:一个函数内部定义的函数,并且这个内部函数引用了外部函数作用域中的变量
def urltemplate(template):
def opener(**kwargs):
return urlopen(template.format_map(kwargs))
return opener
# 创建模板函数
yahoo = urltemplate('http://finance.yahoo.com/d/quotes.csv?s={names}&f={fields}')
# 使用方式:像函数一样调用 yahoo,并传入参数
with yahoo(names='IBM,AAPL,FB', fields='sl1c1v') as response:
for line in response:
print(line.decode('utf-8'))
闭包就是一个函数,但是它还保存着额外的变量环境,使得这些变量可以在函数中使 用。闭包的核心特性就是它可以记住定义闭包时的环境
7.10 在回调函数中携带额外的状态
# 不传入额外状态的示例
def apply_async(func, args, *, callback):
# 执行传入的函数,获取结果
result = func(*args)
# 使用回调函数处理结果
callback(result)
# 回调函数:用于处理任务结果
# 这个回调函数只接受单独的结果参数 并没有传入额外信息到函数中
def print_result(result):
print('Got:', result)
# 示例任务函数:加法操作
def add(x, y):
return x + y
# 示例 1:计算两个数字相加
apply_async(add, (2, 3), callback=print_result)
# 输出: Got: 5
# 示例 2:拼接两个字符串
apply_async(add, ('hello', 'world'), callback=print_result)
# 输出: Got: helloworld
# 使用类的绑定方法作为回调函数 ,并在这个过程中携带和维护状态调用次数
class ResultHandler:
def __init__(self):
self.sequence = 0 # 初始化计数器
def handler(self, result):
self.sequence += 1 # 每次调用时递增
print('[{}] Got: {}'.format(self.sequence, result))
def apply_async(func, args, *, callback):
result = func(*args) # 执行任务
callback(result) # 调用回调函数处理结果
def add(x, y):
return x + y
# 测试部分
if __name__ == '__main__':
r = ResultHandler() # 创建一个带状态的回调处理器
apply_async(add, (2, 3), callback=r.handler)
# 输出: [1] Got: 5
apply_async(add, ('hello', 'world'), callback=r.handler)
# 输出: [2] Got: helloworld
# 使用闭包实现上述类的功能
def make_handler():
sequence = 0 # 状态变量
def handler(result):
nonlocal sequence
sequence += 1
print('[{}] Got: {}'.format(sequence, result))
return handler
def apply_async(func, args, *, callback):
result = func(*args)
callback(result)
# 示例任务函数
def add(x, y):
return x + y
# 模拟异步执行器
def apply_async(func, args, *, callback):
result = func(*args)
callback(result)
# 使用示例
if __name__ == '__main__':
handler = make_handler() # 创建带状态的回调函数
apply_async(add, (2, 3), callback=handler)
# 输出: [1] Got: 5
apply_async(add, ('hello', 'world'), callback=handler)
# 输出: [2] Got: helloworld
# 使用基于生成器的协程完成该项目
# 协程是一种可以“暂停执行、保留状态、并在之后恢复执行”的函数
def make_handler():
sequence = 0
while True:
result = yield # 等待外部通过 send() 发送数据
sequence += 1
print('[{}] Got: {}'.format(sequence, result))
# 示例任务函数
def add(x, y):
return x + y
# 模拟异步执行器
def apply_async(func, args, *, callback):
result = func(*args)
callback(result)
# 使用示例
if __name__ == '__main__':
handler = make_handler() # 创建生成器对象
next(handler) # 启动生成器,运行到第一个 yield
# 使用handler.send作为回调函数 会把callback中的参数result传递给生成器中的result
apply_async(add, (2, 3), callback=handler.send)
# 输出: [1] Got: 5
apply_async(add, ('hello', 'world'), callback=handler.send)
# 输出: [2] Got: helloworld
# 使用partial
from functools import partial
# 定义一个带状态的类,用于记录调用次数
class SequenceNo:
def __init__(self):
self.sequence = 0
# 回调函数,接收 result 和一个状态对象 seq
def handler(result, seq):
seq.sequence += 1
print('[{}] Got: {}'.format(seq.sequence, result))
# 示例任务函数
def add(x, y):
return x + y
# 模拟异步执行器
def apply_async(func, args, *, callback):
result = func(*args)
callback(result)
# 使用示例
if __name__ == '__main__':
# 创建一个状态对象
seq = SequenceNo()
# 使用 partial 绑定额外参数 seq 到 handler 函数
callback_func = partial(handler, seq=seq)
# 模拟两次异步调用
apply_async(add, (2, 3), callback=callback_func)
# 输出: [1] Got: 5
apply_async(add, ('hello', 'world'), callback=callback_func)
# 输出: [2] Got: helloworld
#使用lambda表达式进行包装
apply_async(add, (2, 3), callback=lambda r: handler(r, seq))
7.11 内联回调函数
from queue import Queue
from functools import wraps
def add(x, y):
return x + y
# 用于封装一个要执行的异步任务 将函数引用和参数打包
class Async:
def __init__(self, func, args):
self.func = func
self.args = args
# 异步执行器 执行传入的函数,将结果通过回调函数返回
# 使用*, callback实现强制关键字参数
def apply_async(func, args, *, callback):
result = func(*args)
callback(result)
# 协程调度装饰器
# inlined_async 是一个接受函数作为参数的函数 它返回一个新函数(wrapper)
# 当前inlined_async 接收的参数是 test 函数对象
def inlined_async(func):
# @wrap是标准库提供的装饰器 保持原始函数元信息
@wraps(func)
def wrapper(*args):
# func是原始函数(即test),f是生成器对象 是一个协程
# 协程可以暂停和恢复执行,在执行过程中主动让出控制权,然后在合适的时候继续执行
f = func(*args)
result_queue = Queue() # 创建用于通信的队列
result_queue.put(None) # 放入启动信号
while True:
result = result_queue.get() # 获取异步执行结果
try:
# 将结果发送给协程,继续执行到下一个 yield
# 在生成器还没有启动时 send(None) 等价于 next()
# 第一次调用时返回 Async(add, (2, 3))
a = f.send(result)
# 异步执行协程返回的任务,完成后将结果放入队列
apply_async(a.func, a.args, callback=result_queue.put)
except StopIteration:
# 协程执行完毕,退出循环
break
return wrapper
# 应用示例
# inlined_async 本质上是一个函数,因为使用了@语法,所以它充当了装饰器的角色
@inlined_async
def test():
# r会接受到 send()发送的消息
r = yield Async(add, (2, 3))
print(r) # 输出: 5
r = yield Async(add, ('hello', 'world'))
print(r) # 输出: helloworld
for n in range(10):
r = yield Async(add, (n, n))
print(r) # 输出: 0, 2, 4, ..., 18
if __name__ == "__main__":
test()
阶段1:程序启动 - 装饰器应用
Python 解释器加载代码
执行装饰器:test = inlined_async(test)
test 变量现在指向 wrapper 函数(调度器)
原始 test 函数被保存在 wrapper 的闭包中
装饰完成,等待用户调用
阶段2:用户调用 - 调度器启动
用户调用 test()
实际执行的是 wrapper() 函数(调度器)
进入异步调度环境
阶段3:调度器初始化
调用 f = func(*args) 创建协程(生成器对象)
创建通信队列 result_queue = Queue()
放入启动信号 result_queue.put(None)
进入调度循环 while True:
阶段4:第一次调度循环 - 协程启动
从队列获取启动信号:result = result_queue.get() → result = None
启动协程:a = f.send(None)
协程开始执行:进入原始 test 函数体
创建任务对象:Async(add, (2, 3))
执行 yield Async(add, (2, 3)):
协程在此处暂停
Async 对象返回给调度器
变量 r 还未赋值
阶段5:任务调度
调度器接收到 Async 对象:a = Async(add, (2, 3))
提取任务信息:a.func = add, a.args = (2, 3)
调度异步任务:apply_async(a.func, a.args, callback=result_queue.put)
等价于:apply_async(add, (2, 3), callback=result_queue.put)
阶段6:异步任务执行
apply_async 函数开始执行
执行具体任务:result = func(*args) → result = add(2, 3) → result = 5
调用回调函数:callback(result) → result_queue.put(5)
结果 5 被放入通信队列
阶段7:第二次调度循环 - 结果传递
调度器继续循环
从队列获取结果:result = result_queue.get() → result = 5
恢复协程并传递结果:a = f.send(5)
协程从 yield 处恢复执行
阶段8:协程恢复执行
yield 表达式求值:yield Async(add, (2, 3)) 的值变成 5
完成赋值操作:r = 5
执行后续代码:print(r) → 输出 5
函数执行完毕,没有更多 yield
抛出 StopIteration 异常
阶段9:第一个异步操作完成
协程执行 print(r) → 输出 5
协程继续执行到下一行:r = yield Async(add, ('hello', 'world'))
创建第二个任务对象:Async(add, ('hello', 'world'))
执行第二个 yield
协程再次在此处暂停
第二个Async对象返回给调度器
变量 r 等待被重新赋值
阶段10:第二次任务调度
调度器通过 f.send(5) 收到第二个 Async 对象
a = Async(add, ('hello', 'world'))
提取第二个任务信息:
a.func = add 函数
a.args = ('hello', 'world') 参数
调度第二个异步任务:apply_async(add, ('hello', 'world'), callback=result_queue.put)
调度器继续 while 循环,等待第二个任务完成
阶段11:第二个异步任务执行
apply_async 内部执行第二个任务
调用 add('hello', 'world')
执行字符串拼接:result = 'hello' + 'world' → result = 'helloworld'
调用回调函数:callback('helloworld')
执行 result_queue.put('helloworld')
......
第二个异步任务结果 'helloworld' 进入队列
# 关于send的使用基础
def basic_communication():
def worker():
# yield表达式会"等待"外部发送值
task = yield "我需要一个任务"
print(f"收到任务: {task}")
# # no.2 输出 收到任务: 清理数据库
# 处理任务并等待下一个指令
instruction = yield "任务完成,等待指令"
print(f"收到指令: {instruction}")
# no.4 输出 收到指令: 关闭连接
gen = worker()
# 启动协程
request1 = gen.send(None) # None用于启动
print(f"协程请求: {request1}") # no.1 输出 协程请求: 我需要一个任务
# 发送任务给协程
request2 = gen.send("清理数据库")
print(f"协程请求: {request2}")
# no.3 协程请求: 任务完成,等待指令
try:
gen.send("关闭连接")
except StopIteration:
print("协程完成")
# no.5 输出 协程完成
basic_communication()
7.12 访问定义在闭包内的变量
# 通过函数拓展闭包 使得闭包内定义的变量可以被访问和修改
# 通过编写 存取函数(即getter/setter方法)并将它们作为函数属性附加到闭包上来提供对内层变量的访问支持
def sample():
n = 0 # 外层函数的局部变量
# 内层函数,形成闭包
def func():
print('n=', n) # 访问外层函数的变量n
# 访问器函数们也是闭包
def get_n():
return n # 读取外层变量n
def set_n(value):
nonlocal n # 声明要修改外层变量n
n = value # 修改外层变量n
# 将访问器函数作为属性附加到主函数上
func.get_n = get_n
func.set_n = set_n
return func # 返回带有附加属性的函数
f = sample()
# 1. 调用 sample() 函数
# 2. sample() 内部创建了 func 函数
# 3. sample() 返回 func 函数对象
# 4. f 变量现在引用 func 函数
f() # 输出 n= 0 等价于调用 func()
f.set_n(10)
n= 10
print(f.get_n()) # 输出 10
# 在Python中,内层函数默认只能读取外层函数的变量,但不能修改它们
# nonlocal声明(set_n方法)告诉Python:这个变量不是局部变量,而是要修改外层作用域的变量
# 在Python中,函数也是对象,可以给函数添加属性
# 结合以上两个特点实现了闭包中变量的访问
使用闭包提升访问速度
import sys
# 每当Python调用一个函数时,都会创建一个帧对象,包含:函数的局部变量、参数、执行位置信息等
class ClosureInstance:
def __init__(self, locals=None):
if locals is None:
# 获取帧1的局部变量字典
locals = sys._getframe(1).f_locals
# f_locals是Python帧对象(frame object)的一个属性,它是一个字典,包含了该帧中所有的局部变量。
# 对可调用对象进行筛选
# callable(value) 检查值是否可调用
self.__dict__.update((key,value) for key, value in locals.items()
if callable(value) )
def __len__(self):
return self.__dict__['__len__']()
# 这是一个叫做stack的函数
def Stack():
# 创建私有数据
items = []
# 由于闭包 push可以访问到外层的items变量
def push(item):
items.append(item)
def pop():
return items.pop()
def __len__():
return len(items)
# 创建并返回一个ClosureInstance()对象 其会自动捕获Stack 函数中的所有函数
return ClosureInstance()
# 测试代码
if __name__ == "__main__":
s = Stack()
print(s)
s.push(10)
s.push(20)
s.push('Hello')
print(len(s))
print(s.pop())
print(s.pop())
print(s.pop())
Python中sys._getframe(n)的编号规则:
n=0:当前正在执行的函数
n=1:调用当前函数的函数
n=2:调用n=1函数的函数
s = Stack() -> -> ClosureInstance.__init__
此时 帧0应该是
┌─────────────────────────────────┐
│ ClosureInstance.__init__ │
│ - self = <ClosureInstance> │
│ - locals = None │
└─────────────────────────────────┘
帧1应该是
┌─────────────────────────────────┐
│ Stack() │
│ - items = [] │
│ - push = <function> │
│ - pop = <function> │
│ - __len__ = <function> │
└─────────────────────────────────┘
帧2是
┌─────────────────────────────────┐
│ <module> (主程序) │
│ - s = <待赋值> │
└─────────────────────────────────┘
使用闭包会比使用普通的类快一些
# 创建一个普通的类方法
class Stack2:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
return self.items.pop()
def __len__(self):
return len(self.items)
# 进行使用时间的比较
from timeit import timeit
s = Stack()
timeit('s.push(1);s.pop()', 'from __main__ import s')
# 0.9874754269840196
s = Stack2()
timeit('s.push(1);s.pop()', 'from __main__ import s')
# 1.0707052160287276