Python CookBook —— Chapter 7 (个人笔记)

Chap 7 函数

7.1 可接受任意数量参数的函数 — *args & **kwargs

如何构造一个可接受任意数量参数的函数?

# 1. 为接受任意数量的“位置参数”, 可使用一个 * 参数:

def avg(first, *rest):
    # 这里 rest 是由所有其他位置参数组成的“元组”
    return (first + sum(rest)) / (1 + len(rest))

# Examples
print(avg(1, 3))    # 2.0
print(avg(1, 3, 5, 7))    # 4.0


# 2. 为接受任意数量的“关键字参数”, 可使用一个 ** 参数:
import html

def make_element(name, value, **attrs):
    # 此处 attrs 是一个字典, 包含所有被传入の关键字参数
    keyvals = [' {0}="{1}"'.format(key, value) for key, value in attrs.items()]    # 列表解析
    attr_str = ''.join(keyvals)    # 拼接上述列表(构造属性字符串)
    element = '<{name}{attrs}>{value}</{name}>'.format(
        name=name,
        attrs=attr_str,
        value=html.escape(value)
    )    # 构造 html 标签元素
    return element

# Example 1: Creates '<item size="large" quantity="6">Albatross</item>'
elem = make_element('item', 'Albatross', size='large', quantity=6)
print(elem)

# Example 2: Creates '<p>&lt;spam&gt;</p>'
elem = make_element('p', '<spam>')
print(elem, html.unescape(elem), sep='\t和\t')


# 3. 若希望函数能同时接受任意数量の“位置参数 & 关键字参数”, 则可同时使用 * & **:

def anyargs(*args, **kwargs):
    # 所有位置参数会被放到元组 args 中, 所有关键字参数会被放到字典 kwargs 中
    print(args)     # tuple
    print(kwargs)   # dict

# Examples
anyargs('hello', 'python', 'world', date='20200202', time='23:59:59')
anyargs('goodbye', name=None)

Remark一个 * 参数只能出现在函数定义中最后一个位置参数后面
而 ** 参数只能是最后一个参数
出现在 *args 参数之后的任何形式参数都是 仅限关键字参数,也就是说它们只能作为关键字参数而不能是位置参数。

7.2 只接受关键字参数的函数 — 位于 * or *args 后の强制关键字参数

函数的某些参数可以强制使用关键字参数传递

# 1. 将强制关键字参数放到 *args 后面即可:

def recv(maxsize, *, block):
    """ Receives a message.
    block 是一个强制关键字参数, 它只能以关键字参数的方式被传递 """
    print(f"block = {block}")

# recv(1024)    # TypeError: recv() missing 1 required keyword-only argument: 'block'
# recv(1024, True)    # TypeError: recv() takes 1 positional argument but 2 were given
recv(1024, block=True)    # 必须以关键字参数形式传递


# 2. 在 “接受任意多位置参数的函数” 中指定关键字参数:

def minimum(*values, clip=None):
    """ 显然 clip 是一个带默认值的 强制关键字参数, 因此调用函数时可以不传 clip """
    m = min(values)
    if clip is not None:    # 对于 None 必须使用 is 判断
        m = clip if clip > m else m
    return m

print(minimum(1, 5, 2, -5, 10))    # -5
print(minimum(1, 5, 2, -5, 10, clip=0))    # 0 (clip 必须以关键字参数传递, 否则会被当做位置参数)

Remark:1. 使用强制关键字参数会比使用位置参数表意更清晰,程序更具可读性

					2. 使用强制关键字参数也会比使用 **kwargs 参数更好,因为在<font color=Coral>使用 help 函数时</font>输出也会更容易理解。
print(help(recv))

7.3 给函数参数增加元信息 — 函数参数注解 & .annotations 属性

为函数的参数增加一些额外信息,让使用者能清楚的知道这个函数如何使用。

# 1. 使用 “函数参数注解” 是个好办法, 它能提示程序员应该怎样正确使用函数:

def add(x: int, y: int) -> int:
    # python 解释器不会对这些注解添加任何的语义
    return x + y

print(help(add))
""" Remark: 尽管可使用任意类型的对象给函数添加注解 (如数字, 字符串, 对象实例等), 但通常来讲使用 “类 or 字符串” 最好。"""


# 2. 函数注解只存储在函数的 __annotations__ 属性中:
print(add.__annotations__)

7.4 返回多个值的函数 — return x, y, z

如何构造返回多个值 的函数

# 为了能返回多个值, 函数直接 return 一个元组即可:

def func():
    return 'hello', 'world', 23333

a, b, c = func()    # 这里对返回的 tuple 做了解压赋值
print(a, b, c, sep='\t')

""" Remark: 尽管 func() 看上去返回了多个值, 实际上是先创建了一个元组后返回的; func() 中使用了逗号来生成一个元组, 而不是用括号。"""

u = (1, 2, 3)    # With parentheses
v = 1, 2, 3    # Without parentheses
print(u, v, sep='\n')

7.5 定义有默认参数的函数 — 带默认值的参数, 使用 None 作为默认值, is 操作符

如何定义一个函数,使其参数是可选的并且有一个默认值

# 1. 在函数定义中给参数指定一个默认值, 并放到 “参数列表最后” 即可:

def spam1(a, b=42):
    print(a, b)

spam1(1)    # 1 42
spam1(1, 2)    # 1 2


# 2. 若默认参数是 “可修改の容器” (如列表, 集合 or 字典), 则应使用 None 作为默认值:

def spam2(a, b=None):
    if b is None:
        b = []    # 使用 None 作为默认值, 然后经过判断再给参数赋值列表, 集合 or 字典

Remark:① 参数的默认值仅在 函数定义时 被赋值;② 参数的默认值应是 不可变对象,如 None,True,False,数字 or 字符串;③ 在测试 None 值时应该使用 is 操作符

# ① -----------------------------------
x = 42

def sp(a, b=x):
    """ 参数 b 的默认值在函数定义时就确定了, 后续改变 x 值不会影响函数中 b 的默认值 """
    print(a, b)

# Examples
sp(2)    # 2 42
x = 21
sp(2)    # 2 42, 可见上面改变 x 的值并不影响 sp(2) 的结果


# ② -----------------------------------
def bad_func(a, b=[]):
    """ 不要这样指定参数 b 的默认值!!! """
    return b

# Examples
x = bad_func(1)    
print(x)

x.append(99)
x.append('Gozen Sanji')
print(x)    # [99, 'Gozen Sanji']

y = bad_func(1)
print(y)    # [99, 'Gozen Sanji'], 显然这样的调用结果不合理

""" Remark: 为避免上述情况发生, 最好是将默认值设为 None, 然后在函数中检查它 (see spam2) """


# ③ -----------------------------------
def wrong_func_2(a, b=None):
    # 此写法の问题在于 “除了 None 以外还有其他对象” 被当作 False (如长度为零の字符串, 列表, 元组, 字典等)
    if not b:
        b = []
    print(b)

# Examples
wrong_func_2(1)    # [], 预期的效果
wrong_func_2(2, [])    # [], 非预期的效果
wrong_func_2(1, 0)    # [], 非预期的效果
wrong_func_2(1, '')    # [], 非预期的效果

Page 220 & Page 221 中有不懂的例子

7.6 定义匿名或内联函数 — lambda Expr

假设你想为 sort() 操作创建一个很短的回调函数,但又不想用 def 去写一个单行函数,而是希望通过某个快捷方式以内联方式来创建这个函数

# 1. 使用 lambda 表达式代替简单函数定义:
add = lambda x, y: x + y    # 事实上将 lambda 表达式赋值给一个变量不是一种好的写法
print(add(2, 3))    # 5
print(add('hello ', 'world'))    # hello world

# 这里的 lambda 表达式跟下面定义函数の效果是一样的:
def add_(x, y):
    return x + y

print(add_(2, 3))    # 5
print(add_('hello ', 'world'))    # hello world


# 2. lambda 表达式 “典型的使用场景” 是排序 or 数据 reduce 等:
names = ['David Beazley', 'Brian Jones', 'Raymond Hettinger', 'Ned Batchelder']
sorted_names = sorted(names,
                      key=lambda name: name.split(' ')[-1].lower())
"""
Comments on sorted():
    ※ Return a new list containing all items from the iterable in ascending order.
    ※ A custom key function can be supplied to customize the sort order, and the
    reverse flag can be set to request the result in descending order.
"""
print(sorted_names)

Remark:尽管 lambda 表达式允许你定义简单函数,但其使用是有限制的。你只能指定单个表达式,其值就是最后的返回值,也就是说 不能包含其他的语言特性,如:多个语句、条件表达式、迭代以及异常处理等

7.7 匿名函数捕获变量值 — lambda x, y=y: x+y 以捕获参数 y 的值

如何用 lambda 表达式定义一个匿名函数,并在定义时捕获到某些变量的值

# 1. lambda 表达式不指定捕获变量值的情况:
x = 10
a = lambda y: x + y    # 其中 x 是“自由变量”, 其值在运行时才绑定

x = 20
b = lambda y: x + y

print(a(10), b(10))    # 30 30
"""
    这其中的奥妙在于 lambda 表达式中的 x 是一个【自由变量】, 在运行时绑定值, 而非定义时绑定,
        这与函数参数的默认值的情况不同; 因此, 在调用这个 lambda 表达式时 x 的值是执行时的值。
"""
x = 15
print(a(10))    # 25
x = 0
print(a(10))    # 10


# 2. 接下来让匿名函数 “在定义时就捕获到参数值”:
x = 10
p = lambda y, x=x: x + y    # 将此处的 x 定义成默认参数形式, 即可使匿名函数在定义时捕获到变量 x 的值
x = 20
q = lambda y, x=x: x + y
print(p(10), q(10))    # 20 30

# Question: 为什么不干脆像下面这样写呢? # 3 中的例子回答了这个问题。
r = lambda y, x=665: x + y
print(r(1), r(1, 2))    # 666 3


# 3. 使用 lambda 表达式不恰当的例子:
# 通过在一个循环 or 列表解析中创建一个 lambda 表达式列表, 并期望函数能在定义时就记住每次的迭代值:
functions = [lambda t: t + n for n in range(5)]

for func in functions:
    print(func(1), end='')    # 55555, 而不是(错误的)预期的 12345, 可见实际运行时 n 的值为迭代的最后一个值(n=4)

# 现修改如下:
funcs = [lambda t, n=n: t + n for n in range(5)]    # 将 n 写为带默认值の参数的形式

for f in funcs:
    print(f(1), end='')    # 12345 √

7.8 减少可调用对象的参数个数 — functools.partial() 固定参数

有一个被其他 python 代码使用的 callable 对象,可能是一个回调函数 or 处理器,但其参数太多,导致调用时出错

# 使用 functools.partial() 可减少某个函数的参数个数;
# partial() 函数允许给参数设置固定值, 以减少调用时的参数个数:
from functools import partial

def test1(a, b, c, d):
    print(a, b, c, d)

# 接下来使用 partial() 函数来固定某些参数值:
s1 = partial(test1, 1)    # 固定参数 a = 1
print(s1(2, 3, 4))    # 1 2 3 4
print(s1(4, 5, 6))    # 1 4 5 6

s2 = partial(test1, d=42)    # 固定参数 d = 42
print(s2(1, 2, 3))    # 1 2 3 42
print(s2(4, 5, 6))    # 4 5 6 42

s3 = partial(test1, 1, 2, d=42)    # 固定参数 a = 1, b = 2, d = 42
print(s3(3))    # 1 2 3 42
print(s3(4))    # 1 2 4 42
print(s3(5))    # 1 2 5 42

从上面的例子容易看出:partial() 固定某些参数并返回一个新的 callable 对象,这个新的 callable 接受 未赋值的 参数,然后跟之前已赋值过的参数合并起来,最后将所有参数传递给原始函数。

# Example 1: 假设有一个点的列表来表示 (x,y) 坐标元组。可使用下面的函数来计算两点之间的距离:
# ---------------------------------------------------------------------------------->
import math
points = [(1, 2), (3, 4), (5, 6), (7, 8)]

def norm_L2(p1, p2):
    """ L2 范数 """
    (x1, y1), (x2, y2) = p1, p2    # 不加括号则无法正确解压赋值哦~
    # math.hypot() returns the Euclidean distance, sqrt(x*x + y*y).
    return math.hypot(x2 - x1, y2 - y1)    

# 现设想以某个点为 “基点”, 根据点和基点之间的距离来排序所有这些点, 使用 partial() 来实现:
pt = (4, 3)    # 基点
# 通过 partial() 固定 norm_L2 的第一位置参数为 pt (其实这里固定那个参数都可以)
points.sort(key=partial(norm_L2, pt))    
print(points)    # [(3, 4), (1, 2), (5, 6), (7, 8)]

下面还有两个例子 on Page 226 看不懂

7.9 将单方法的类转换为函数 — 还需深入

你有一个除 _ _ init _ _() 方法外只定义了一个方法的类,为简化代码,可将它转换成一个函数:

# 多数情况下, 可使用 “闭包” 来将单个方法的类转换成函数:

class PathTemplate:
    def __init__(self, template):
        self.template = template

    def read(self, **kwargs):
        with open(self.template.format_map(kwargs), 'r', encoding='utf-8') as f:
            contents = f.readlines()[:10]
        return contents

# Example use:
new_file = PathTemplate(r"D:\Contract_Specs\{filename}")
for line in new_file.read(filename='Contracts_20200923.txt'):
    print(line, end='')

print('👽'*56)
# 上面的类可被一个更简单的函数代替:

def path_template(template):
    def read(**kwargs):
        with open(template.format_map(kwargs), 'r', encoding='utf-8') as f:
            contents = f.readlines()[:10]
        return contents
    return read

# Example use:
new_file = path_template(r"D:\Contract_Specs\{filename}")
for line in new_file(filename='Contracts_20200923.txt'):
    print(line, end='')

大部分情况下,你拥有一个单方法类的原因是需要存储某些额外的状态来给方法使用。使用一个内部函数 or 闭包的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。
闭包关键特点是:它会记住自己被定义时的环境。因此在上面的例子中,read() 函数记住了 template 参数的值,并在接下来的调用中使用他。

任何时候,只要碰到需要给某个函数增加额外状态信息的问题,都可以考虑使用闭包。相比将函数转换成一个类而言, 闭包通常是一种更加简洁和优雅的方案。

更详细的关于闭包的内容,参考《流畅的Python》第七章

7.10 带额外状态信息的回调函数 — 还需深入

你的代码中需要依赖到回调函数的使用 (如事件处理器、等待后台任务完成后的回调等),并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。

# 本节主要讨论那些出现在很多函数库 & 框架中的 “回调函数的使用” ———— 特别是跟 “异步处理” 有关的内容:

def apply_async(func, args, *, callback):
    """ 参数 args 以 tuple 形式传递
        callback 为强制关键字参数 """
    # 1. Compute the result
    result = func(*args)

    # 2. Invoke the callback with the result
    callback(result)

# Example Use 1:
def print_result(result):
    print(f'Got: {result}')

def add(x, y):
    """ 测试函数 """
    return x + y

apply_async(add, (2, 3), callback=print_result)    # Got: 5
apply_async(add, ('hello', 'world'), callback=print_result)    # Got: helloworld


# Case 1: 为让回调函数访问外部信息, 可使用一个 “绑定方法” 来代替一个简单函数:
class ResultHandler:
    def __init__(self):
        self.sequence = 0    # 内部序列号

    def handler(self, result):
        self.sequence += 1
        print('[{}] Got: {}'.format(self.sequence, result))

# 使用该类时, 先创建一个类的实例, 然后用它的 handler() 绑定方法作为回调函数:
r = ResultHandler()
apply_async(add, (3, 2), callback=r.handler)    # [1] Got: 5
apply_async(add, ('Goodbye', 'World'), callback=r.handler)    # [2] Got: GoodbyeWorld


# Case 2: 也可以使用一个 “闭包” 来捕获状态值:
def make_handler():
    sequence = 0

    def handler(result):
        # nonlocal 关键字其实就是为了使用 make_handler() 中的 sequence, 详见下面 Remark
        nonlocal sequence    
        sequence += 1
        print('[{}] Got: {}'.format(sequence, result))
    return handler

# 接下来使用闭包:
handler = make_handler()    # 调用 make_handler() 返回一个函数对象
apply_async(add, (2, 3), callback=handler)    # [1] Got: 5
apply_async(add, ('hello', 'world'), callback=handler)    # [2] Got: helloworld

Remark:非局部声明变量指代的已有标识符是最近外面函数的已声明变量,但不包括全局变量。
这个是很重要的,因为绑定的默认行为是首先搜索本地命名空间。nonlocal 声明的变量只对局部起作用,离开封装函数,则该变量无效。
非局部声明不像全局声明,必须在封装函数前面事先声明该变量。
非局部声明不能与局部范围的声明冲突。

协程的例子 on Page 230 & Page 231 中的讨论看不懂

7.11 内联回调函数 — ???

看不懂0.0


def


xyz

7.12 访问闭包中定义的变量 — ???

看不懂0.0


def


xyz

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值