Skip to content

内置函数及文件

1. 匿名函数Lambda

匿名函数是一种通过单个语句生成函数的方式,其结果是返回值。匿名函数使用lambda关键字定义,该关键字仅表达“我们声明一个匿名函数”的意思。

lambda 函数可以接收任意多个参数 (包括可选参数) 并且返回单个表达式的值。

语法格式:lambda arg1,arg2,arg3,…: expression

  • arg1,arg2,arg3,…:这是函数的参数,可以有一个或多个。
  • expression:这是函数体,lambda函数只能有一个表达式,没有冒号和return语句。
# 一个简单的lambda函数,计算两个数的和
add = lambda x, y: x + y
print(add(3, 4))
# 7

# 使用lambda函数作为映射函数的参数
numbers = [1, 2, 5, 10]
squared = list(map(lambda x: x**2, numbers))
print(squared)
# [1, 4, 25, 100]

# 使用lambda函数作为排序函数的参数
numbers = [1, 2, 5, 10]
sorted_numbers = sorted(numbers, key=lambda x: x % 2)
print(sorted_numbers)
# [2, 10, 1, 5]

# 使用lambda函数进行乘法运算
multiply = lambda x, y: x * y
print(multiply(2, 3))
# 6

# 使用lambda函数列表
functions = [lambda a: a * 2, lambda b: b * 3]
print(functions[0](5))
# 10
print(functions[1](5))
# 15

对于sorted(numbers, key=lambda x: x % 2)中排序过程做个解释:

key=lambda x: x % 2 用于将数字[1, 2, 5, 10]分为偶数和奇数两组:

  • 对于数字1,1 % 2 等于1。(奇数组)
  • 对于数字2,2 % 2 等于0。(偶数组)
  • 对于数字5,5 % 2 等于1。(奇数组)
  • 对于数字10,10 % 2 等于0。(偶数组)

sorted() 函数对列表进行排序时,它首先根据 key 函数的结果对元素进行分组。对于偶数组和奇数组内的元素,Python 会根据它们的原始值进行排序。因此:

  • 偶数组内的排序:在偶数组内,数字2小于10,所以2会排在10前面。
  • 奇数组内的排序:在奇数组内,数字1小于5,所以1会排在5前面。

所以sorted_numbers)的输出结果是 [2, 10, 1, 5]

示例:Lambda函数与等价函数表达式

def short_func1(x):
    return x * 2

short_func2 = lambda x: x * 2

print(short_func1(5))  
# 10
print(short_func2(5))  
# 10

示例:Lambda函数作为参数传递给函数

# 定义一个函数 apply_to_list,接受一个列表和一个函数作为参数
def apply_to_list(some_list, f):
    # 返回一个新的列表,其中每个元素都是通过对原列表中的每个元素应用函数 f 得到的
    return [f(x) for x in some_list]

ints = [4, 0, 1, 5, 6]

# 调用 apply_to_list 函数,将每个整数乘以 2
result5 = apply_to_list(ints, lambda x: x * 2)

print(result5)  
# [8, 0, 2, 10, 12]
def apply(func, arg):
    return func(arg)

result = apply(lambda x: x * 2, 5)
print(result)  # 输出: 10

示例lambda: None 函数没有输入参数,输出是None

print(lambda: None)
# <function <lambda> at 0x7fa5c4097670>

示例lambda **kwargs: 1 输入是任意键值对参数,输出是1

print(lambda **kwargs: 1)  # 输出: <function <lambda> at 0x729037beb9a0>

result = lambda **kwargs: 1
print(result)  # 输出: <function <lambda> at 0x729037f0fd90>

# 调用不带参数的 lambda 函数
result = (lambda **kwargs: 1)()
print(result)  # 输出: 1

# 调用带有一些关键字参数的 lambda 函数
result = (lambda **kwargs: 1)(a=10, b=20)
print(result)  # 输出: 1

result = (lambda **kwargs: kwargs)(a=10, b=20)
print(result)  # 输出: {'a': 10, 'b': 20}

# 将 lambda 函数赋值给变量并调用它
my_lambda = lambda **kwargs: kwargs
result = my_lambda(x=5, y=15)
print(result)  # 输出: {'x': 5, 'y': 15}
print(result.values())  # 输出: dict_values([5, 15])
print(result.keys())  # 输出: dict_keys(['x', 'y'])

# 使用 **kwargs 计算总和
sum_lambda = lambda **kwargs: sum(kwargs.values())
result = sum_lambda(a=1, b=2, c=3)
print(result)  # 输出: 6

# 使用 **kwargs 进行字符串格式化
format_lambda = lambda **kwargs: "Name: {name}, Age: {age}".format(**kwargs)
result = format_lambda(name="Alice", age=30)
print(result)  # 输出: Name: Alice, Age: 30

示例:Lambda函数与filter()结合使用,用以过滤数据。

filter() 函数是一个内置函数,用于从一个序列中筛选出满足特定条件的元素。它接受两个参数:一个函数和一个可迭代对象。这个函数用于定义筛选条件,对序列中的每个元素进行测试,如果函数返回 True,则该元素会被包含在结果中;如果返回 False,则该元素会被排除。

filter() 函数的基本语法如下:

filter(function, iterable)
  • function:一个函数,用于定义筛选条件。该函数接受一个参数,并返回一个布尔值(TrueFalse)。
  • iterable:一个可迭代对象,如列表、元组、字符串等。
numbers = [1, 2, 3, 4, 5, 6]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)
# [2, 4, 6]

示例:Lambda函数作为字典的值,用于动态计算值。

dict_with_lambda = {'double': lambda x: x * 2}
print(dict_with_lambda['double'](5))
# 10

示例:Lambda函数与reduce()结合使用,用于在序列上执行累积操作。

reduce() 函数是 functools 模块中的一个函数,用于将一个二元函数应用于序列的元素,从而将序列缩减为一个单一的累积结果。reduce() 函数通常用于执行累积操作,如求和、乘积等。

reduce() 函数的基本语法如下:

from functools import reduce
reduce(function, sequence[, initial])

function:一个二元函数,接受两个参数并返回一个结果。 sequence:一个可迭代对象,如列表、元组等。 initial(可选):一个初始值,用于在累积操作开始之前设置初始累积结果。

reduce() 函数在处理累积操作时非常有用,例如:

  • 求和:如示例所示,计算序列中所有元素的和。
  • 乘积:计算序列中所有元素的乘积。
  • 连接字符串:将序列中的字符串连接成一个长字符串。
  • 最大值/最小值:通过适当的 lambda 函数,可以找到序列中的最大值或最小值。
from functools import reduce

# 计算列表中所有元素的累加
numbers = [1, 2, 3, 4, 5]
result = reduce(lambda x, y: x + y, numbers)
print(result)
# 15

# 计算列表中所有元素的乘积
numbers = [1, 2, 3, 4, 5]
product = reduce(lambda x, y: x * y, numbers)
print(product)
# 120

# 连接列表中的字符串
strings = ["Hello", " ", "World", "!"]
concatenated_string = reduce(lambda x, y: x + y, strings)
print(concatenated_string)
# "Hello World!"

# 找到列表中的最大值
numbers = [1, 5, 2, 8, 3]
max_value = reduce(lambda x, y: x if x > y else y, numbers)
print(max_value)
# 8

# 找到列表中的最小值
numbers = [1, 5, 2, 8, 3]
min_value = reduce(lambda x, y: x if x < y else y, numbers)
print(min_value)
# 1

示例lambda函数使用多个参数,实现多个输入。

multiply = lambda x, y, z: x * y * z
print(multiply(2, 3, 4))
# 24

示例lambda函数作为对象方法,用于在对象内部定义行为。

class Calculator:
    def __init__(self):
        self.operations = {}  # 初始化操作字典

    def add_operation(self, name, operation):
        self.operations[name] = lambda x, y: operation(x, y)  # 添加操作

    def calculate(self, name, x, y):
        return self.operations[name](x, y)  # 执行操作并返回结果


calc = Calculator()
calc.add_operation("add", lambda x, y: x + y)  # 添加加法操作
print(calc.calculate("add", 3, 4))  # 计算 3 + 4 并打印结果
# 7

2. 内置序列函数

2.1 通用序列操作函数

这些函数适用于大多数序列类型(如列表、元组、字符串等)。

函数名 说明
len(s) 返回序列 s 的长度(元素个数)。
min(s) 返回序列 s 中的最小元素。
max(s) 返回序列 s 中的最大元素。
sum(s) 返回序列 s 中所有元素的和(仅适用于数值类型的序列)。
all(s) 如果序列 s 中所有元素都为真值,则返回 True,否则返回 False
any(s) 如果序列 s 中至少有一个元素为真值,则返回 True,否则返回 False
sorted(s) 返回一个排序后的新列表(原序列不变)。
reversed(s) 返回一个反向迭代器(原序列不变)。
s = [3, 1, 4, 1, 5, 9]
print(len(s))        # 输出: 6
print(min(s))        # 输出: 1
print(max(s))        # 输出: 9
print(sum(s))        # 输出: 23
print(all(s))        # 输出: True
print(any(s))        # 输出: True
print(sorted(s))     # 输出: [1, 1, 3, 4, 5, 9]
print(list(reversed(s)))  # 输出: [9, 5, 1, 4, 1, 3]

2.2 序列类型转换函数

这些函数用于将序列转换为其他类型。

函数名 说明
list(s) 将序列 s 转换为列表。
tuple(s) 将序列 s 转换为元组。
str(s) 将序列 s 转换为字符串(适用于字符序列)。
set(s) 将序列 s 转换为集合(去重)。
frozenset(s) 将序列 s 转换为不可变集合(去重)。
t = (1, 2, 3)
print(list(t))       # 输出: [1, 2, 3]
print(set(t))        # 输出: {1, 2, 3}

2.3 序列查找和操作函数

这些函数用于查找、过滤和操作序列中的元素。

函数名 说明
enumerate(s) 返回一个枚举对象,生成 (index, value) 对。
filter(func, s) 过滤序列 s,返回满足 func 条件的元素组成的迭代器。
map(func, s) 对序列 s 中的每个元素应用 func,返回结果组成的迭代器。
zip(*s) 将多个序列按位置组合成元组,返回一个迭代器。
s = [3, 1, 4, 1, 5, 9]
t = (1, 2, 3)

print(list(enumerate(s)))
# [(0, 3), (1, 1), (2, 4), (3, 1), (4, 5), (5, 9)]
print(list(filter(lambda x: x > 2, s)))
# [3, 4, 5, 9]
print(list(map(lambda x: x * 2, s)))
# [6, 2, 8, 2, 10, 18]
print(list(zip(s, t)))
# [(3, 1), (1, 2), (4, 3)]

enumerate()

enumerate() 函数用于将一个可迭代对象组合为一个索引序列,同时列出数据和数据下标。它返回一个枚举对象,其中每个元素是一个元组,包含一个计数(从零开始)和值。 所以,当需要对数据建立索引时,一种有效的模式就是使用enumerate构造一个字典,将序列值(假设是唯一的)映射到索引位置上。

seasons = ['Spring', 'Summer', 'Fall', 'Winter']

print(list(enumerate(seasons)))  
# [(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')]

对比下面2个循环:

a_list = ['foo', 'bar', 'baz']
mapping = {}

for i, v in enumerate(a_list):  # enumerate生成索引值i和序列值v
    mapping[v] = i

print(mapping)  
# {'foo': 0, 'bar': 1, 'baz': 2}

i = 0
mapping = {}
for v in a_list:
    print(i, a_list[i])
    mapping[v] = i
    i += 1

print(mapping)  
# {'foo': 0, 'bar': 1, 'baz': 2}

利用 enumerate() 批量修改列表内的元素。

a_list = ["01", "02", "03"]
unit_element = "1"  # 必须是字符串

for i, element in enumerate(a_list):
    a_list[i] = unit_element + element

print(a_list)
# ['101', '102', '103']

sorted()

sorted()函数返回一个根据任意序列中的元素新建的已排序列表。sorted()函数接受的参数与列表的sort方法一致。

y = sorted([7, 1, 2, 6, 0, 3, 2])
print(y)  # [0, 1, 2, 2, 3, 6, 7] 结果已排序

z = sorted('Hello World')
print(z)  
# [' ', 'H', 'W', 'd', 'e', 'l', 'l', 'l', 'o', 'o', 'r']

sorted() 函数和 .sort() 方法都用于对序列进行排序,但它们之间有一些关键的区别:

sorted() 函数:

  • 返回值:sorted() 是一个内置函数,它返回一个新的已排序列表,而不会修改原始列表。
  • 适用范围:可以用于任何可迭代对象,包括列表、元组、字符串等。
numbers = [3, 1, 4, 1, 5, 9, 2]
sorted_numbers = sorted(numbers)

print(sorted_numbers)
# 输出: [1, 1, 2, 3, 4, 5, 9]
print(numbers)
# 输出: [3, 1, 4, 1, 5, 9, 2](原始列表未修改)

.sort() 方法:

  • 返回值:.sort() 是列表对象的一个方法,它直接在原始列表上进行排序,返回 None
  • 适用范围:只能用于列表。
numbers = [3, 1, 4, 1, 5, 9, 2]
numbers.sort()
print(numbers)
# 输出: [1, 1, 2, 3, 4, 5, 9](原始列表被修改)

共同点:

  • 参数:两者都可以接受 keyreverse 参数来定制排序行为。key 参数用于指定一个函数,该函数会在每个元素上应用,以确定排序依据。reverse 参数是一个布尔值,用于指定是否按降序排序(默认为 False,即升序)。
  • 稳定性:两者都是稳定的排序算法,即当两个元素相等时,它们在排序后的相对顺序与原始顺序相同。

建议:

  • 如果需要保留原始列表不变,并希望得到一个新的已排序列表,使用 sorted() 函数。
  • 如果不需要保留原始列表,并且只关心排序后的结果,使用 .sort() 方法可以更高效,因为它不需要创建新的列表。

zip()

zip()将列表、元组或其他序列的元素配对,新建一个元组构成的列表。

seq1 = ['foo', 'bar', 'baz']
seq2 = ['one', 'two', 'three']
seq3 = [False, True]

zipped = zip(seq1, seq2)
print(list(zipped))  
# [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]

zipped = zip(seq1, seq2, seq3)
print(list(zipped))  
# [('foo', 'one', False), ('bar', 'two', True)]

for i, (a, b) in enumerate(zip(seq1, seq2)):
    print('{0}: {1}, {2}'.format(i, a, b))  # 方法1 {0}列表元素的索引, {1}元组中第一个值, {2}元组中第二个值
    print(f'{i}: {a}, {b}') # 方法2
    # 0: foo, one
    # 1: bar, two
    # 2: baz, three

给定一个已“配对”的序列时,zip()函数可以去“拆分”序列。这种方式的另一种思路就是将行的列表转换为列的列表。参考Python的Unpacking

pitchers = [('Jack', 'Ma'), ('Tom', 'Li'), ('Jimmy', 'Zhang')]
first_names, last_names = zip(*pitchers)

print(first_names)  
# ('Jack', 'Tom', 'Jimmy')
print(last_names)  
# ('Ma', 'Li', 'Zhang')

2.4 字符串专用序列函数

字符串是一种特殊的序列类型,Python 提供了许多专门用于字符串的函数。

函数名 说明
str.join(s) 将序列 s 中的元素用字符串 str 连接起来。
str.split() 将字符串按分隔符拆分为列表。
str.strip() 去除字符串两端的空白字符。
str.find(sub) 返回子字符串 sub 在字符串中的索引,未找到则返回 -1
str.replace(old, new) 将字符串中的 old 替换为 new
text = "hello,world"
print(text.split(','))  # 输出: ['hello', 'world']
print('-'.join(['a', 'b', 'c']))  # 输出: a-b-c
print(text.find('world'))  # 输出: 6

2.5 其他常用序列函数

函数名 说明
range(start, stop, step) 生成一个整数序列,常用于循环。
slice(start, stop, step) 创建一个切片对象,用于序列切片操作。
iter(s) 返回序列 s 的迭代器。
next(iter) 从迭代器中获取下一个元素。
r = range(1, 10, 2)
print(list(r))  # 输出: [1, 3, 5, 7, 9]
sl = slice(1, 5, 2)
print(s[sl])    # 输出: [1, 4]

3. 列表、集合和字典的推导式

推导式comprehensions(又称解析式),是Python的一种特性。使用推导式可以快速生成列表、元组、集合、字典类型的数据。

推导式又分为列表推导式、元组推导式、集合推导式、字典推导式。

3.1 列表推导式

列表推导式(list comprehension)允许你过滤一个容器的元素,用一种简明的表达式转换传递给过滤器的元素,从而生成一个新的列表。

列表推导式的基本形式为:[expression for item in iterable if condition]

  • expression:生成列表元素的表达式。
  • item:迭代变量,从 iterable 中取值。
  • iterable:可迭代对象(如列表、元组、字符串等)。
  • condition(可选):过滤条件,只有满足条件的元素才会被包含在结果中。

基本用法示例:

# 生成 1 到 10 的平方列表
squares = [x**2 for x in range(1, 11)]
print(squares)
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# 过滤偶数
evens = [x for x in range(1, 11) if x % 2 == 0]
print(evens)
# [2, 4, 6, 8, 10]

# 嵌套循环
pairs = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y]
print(pairs)
# [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

列表推导式与下面的for循环是等价的:

result = []
for val in collection:
    if condition:
        result.append(expr)

下面是列表推导式和等价for循环的示例:

data = []

for i in range(-5, 5):
    if i >= -1:
        data.append(i**2)

print(data)
# [1, 0, 1, 4, 9, 16]

data = [i**2 for i in range(-5, 5) if i >= -1]
print(data)
# [1, 0, 1, 4, 9, 16]

下面的例子是用列表推导式将小写p开头单词的所有字母转换成大写。

data = []
fruit = [
    "apricot",
    "Watermelon",
    "Papaya",
    "pear",
]

data = [x.upper() if x.startswith("p") else x.title() for x in fruit]

print(data)
# ['Apricot', 'Watermelon', 'Papaya', 'PEAR']

嵌套列表推导式

下面的例子是用嵌套列表推导式代替2层for循环。

# 用for循环实现2层嵌套
data = []
list_i = []
list_j = []

for i in range(-3, 3):
    list_i.append(i)
    if i > 0:
        for j in range(1, 3):
            list_j.append(j)
            data.append((i, j))

print(list_i)
# [-3, -2, -1, 0, 1, 2]
print(list_j)
# [1, 2, 1, 2]
print(data)
# [(1, 1), (1, 2), (2, 1), (2, 2)]


# 用嵌套列表推导式实现
data = [(i, j) for i in range(-3, 3) if i > 0 for j in range(1, 3)]
print(data)
# [(1, 1), (1, 2), (2, 1), (2, 2)]

再举一个嵌套列表推导式的例子。

all_data = [
    ['John', 'Emily', 'Michael', 'Lee', 'Steven'],
    ['Maria', 'Juan', 'Javier', 'Natalia', 'Pilar'],
]
names_of_interest = []

for names in all_data:
    enough_es = [name for name in names if name.count('e') >=2]
    names_of_interest.extend(enough_es)

print(names_of_interest)  
# ['Lee', 'Steven']

result = [name for names in all_data for name in names if name.count('e') >= 2]
print(result)  
# ['Lee', 'Steven']

用嵌套列表推导式将矩阵扁平化。

考虑下面这个3x4的矩阵,它由3个长度为4的列表组成。下面例子对比了用传统for循环将矩阵扁平化,和用嵌套列表推导式将矩阵扁平化。并且通过列表推导式中的列表推导式将扁平矩阵还原为3x4矩阵。

matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12],
]

flattened = []

# 传统for循环嵌套
for m in matrix:
    for x in m:
        flattened.append(x)

print(flattened)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

# 嵌套列表推导式
flattened = [x for m in matrix for x in m]
print(flattened)
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

# 列表推导式中的列表推导式(上述嵌套列表推导式的改写)
z = [[x for x in m] for m in matrix]
print(z)
# [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]

3.2 集合推导式

集合推导式(Set Comprehension)用于快速生成集合(元素唯一)。

语法:{expression for item in iterable if condition},语法与列表推导式类似,但结果是一个集合(去重)。

# 生成 1 到 10 的平方集合
squares_set = {x**2 for x in range(1, 11)}
print(squares_set)
# {64, 1, 4, 36, 100, 9, 16, 49, 81, 25}
print(type(squares_set))
# <class 'set'>

# 过滤偶数并去重
evens_set = {x for x in [1, 2, 2, 3, 4, 4, 5] if x % 2 == 0}
print(evens_set)
# {2, 4}

3.3 字典推导式

字典推导式(Dictionary Comprehension)用于快速生成字典。

语法:{key_expression: value_expression for item in iterable if condition}

  • key_expression:生成字典键的表达式。
  • value_expression:生成字典值的表达式。
  • item:迭代变量,从 iterable 中取值。
  • iterable:可迭代对象。
  • condition(可选):过滤条件。
# 生成数字到其平方的字典
squares_dict = {x: x**2 for x in range(1, 6)}
print(squares_dict)
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# 过滤偶数并生成字典
evens_dict = {x: x**2 for x in range(1, 11) if x % 2 == 0}
print(evens_dict)
# {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}

# 交换键值对
original_dict = {'a': 1, 'b': 2, 'c': 3}
swapped_dict = {v: k for k, v in original_dict.items()}
print(swapped_dict)
# {1: 'a', 2: 'b', 3: 'c'}

# 将字串列表改成字典
strings = ['a', 'as', 'bat', 'car', 'dove', 'python']
loc_mapping = {index: val for index, val in enumerate(strings)}
print(loc_mapping)  
# {0: 'a', 1: 'as', 2: 'bat', 3: 'car', 4: 'dove', 5: 'python'}

3.4 元组推导式

实际上,Python 中并没有元组推导式(tuple comprehension),因为元组是不可变的,即一旦创建,就不能更改其内容。

下面的例子生成一个包含数字1~5的元组。从结果可以看到,元组推导式生成的结果并不是一个元组,而是一个生成器对象。所以,这里所说的元组推导式,与其他推导式不一样,其实就是使用tuple()函数将生成器对象转换成元组。

data = (x for x in range(5))

print(data)
# <generator object <genexpr> at 0x7f87217a8e40>

print(type(data))
# <class 'generator'>

在上面的例子中,data = (x for x in range(5)) 创建了一个生成器表达式(generator expression),它是一种惰性求值的迭代器。生成器表达式不会立即计算所有值,而是按需生成值。要输出生成器表达式中的值,则需要迭代它。

以下是几种方法来输出 data 的值:

# 方法1: 使用 `for` 循环
data = (x for x in range(5))
for value in data:
    print(value)
# 0
# 1
# 2
# 3
# 4


# 方法2: 使用list()函数将生成器表达式转换为列表,然后输出整个列表
data = (x for x in range(5))
data_list = list(data)
print(data_list)
# [0, 1, 2, 3, 4]


# 方法3: 使用next()函数逐个获取生成器的下一个值,直到没有更多值可生成时抛出 `StopIteration` 异常
data = (x for x in range(5))
try:
    while True:
        print(next(data))
except StopIteration:
    pass
# 0
# 1
# 2
# 3
# 4


# 方法4: 使用tuple()函数将生成器表达式转换为元组,然后输出整个元组
data = (x for x in range(5))
data_tuple = tuple(data)
print(data_tuple)
# (0, 1, 2, 3, 4)


# 方法5: 使用 set()函数将生成器表达式转换为集合,然后输出整个集合(注意:这会去除重复元素)
data = (x for x in range(5))
data_set = set(data)
print(data_set)
# {0, 1, 2, 3, 4}

4. 函数声明

函数声明是定义函数的语法结构,它告诉Python解释器如何创建和调用一个函数。函数声明包括函数名、参数列表和函数体。函数通过 def 关键字声明,可以接受参数并返回值。

4.1 基本函数声明

最简单的函数声明形式如下:

def function_name(parameters):
    """函数文档字符串(可选)"""
    # 函数体
    return value  # 返回值(可选)
  • def:关键字,用于声明函数。
  • function_name:函数名,遵循变量命名规则。
  • parameters:函数的参数列表,可以有零个或多个参数。
  • """文档字符串""":描述函数功能的字符串(可选)。
  • return:关键字,用于返回值(可选)。如果省略,函数默认返回 None
def greet():
    """打印问候语"""
    print("Hello, World!")

# 调用函数
greet()
# Hello, World!

4.2 带参数的函数

函数可以接受参数,参数可以是任意数据类型。

def greet(name):
    """向指定名字的人打招呼"""
    print(f"Hello, {name}!")

# 调用函数
greet("Alice")
# Hello, Alice!
greet("Bob")
# Hello, Bob!

4.3 带返回值的函数

函数可以通过 return 语句返回值。

def add(a, b):
    """返回两个数的和"""
    return a + b

# 调用函数
result = add(3, 5)
print(result)
# 8

4.4 默认参数

可以为函数的参数指定默认值。如果调用函数时未提供该参数,则使用默认值。

def greet(name="World"):
    """向指定名字的人打招呼,默认是 World"""
    print(f"Hello, {name}!")

# 调用函数
greet()
# Hello, World!
greet("Alice")
# Hello, Alice!

4.5 关键字参数和位置参数

位置参数(Positional Arguments)是根据参数在函数定义中的位置来传递的。调用函数时,参数的顺序必须与函数定义中的顺序一致。不能省略任何参数,除非函数定义中为参数提供了默认值。不能使用参数名来传递。

下例中,ab都属于位置参数

def add(a, b):
    return a + b

# 调用函数时,参数按照定义的顺序传递
result = add(3, 5)  # 正确的调用方式
print(result)
# 8
result = add(3)
# TypeError: add() missing 1 required positional argument: 'b'

关键字参数(Keyword Arguments)是通过参数名来传递的。调用函数时,参数的顺序可以与函数定义中的顺序不同。可以省略某些参数,只要这些参数在函数定义中有默认值。可以与位置参数混合使用,但关键字参数必须位于位置参数之后。

下例中,greeting属于位置参数,name属于关键字参数,name位于greeting之后。

def greet(greeting, name="John"):
    print(f"{greeting}, {name}!")

# 调用函数
greet(name="Alice", greeting="Hi")
# Hi, Alice!
greet(greeting="John")
# Hi, John!
greet()
# TypeError: greet() missing 1 required positional argument: 'greeting'

4.6 可变参数

函数可以接受任意数量的参数。

*args

*args接受任意数量的位置参数,将参数打包成一个元组。

def add(*numbers):
    """返回所有数字的和"""
    total = 0
    for num in numbers:
        total += num
    return total

# 调用函数
print(add(1, 2, 3))
# 6
print(add(1, 2, 3, 4, 5))
# 15

**kwargs

**kwargs `接受任意数量的关键字参数将参数打包成一个字典。

def print_info(**info):
    """打印所有关键字参数"""
    for key, value in info.items():
        print(f"{key}: {value}")

# 调用函数
print_info(name="Alice", age=25, city="New York")
# name: Alice
# age: 25
# city: New York

参数解包

def add(a, b, c, d=5):
    return a + b +c + d


args = (3, 5, 7)
kwargs = {"a": 3, "b": 5, "c": 7, "d": 9}

print(add(*args))
# 20
print(add(**kwargs))
# 24

4.7 Lambda函数

Lambda函数是一种匿名函数,通常用于简单的操作。

语法:lambda parameters: expression

# 定义一个 Lambda 函数
add = lambda a, b: a + b

# 调用 Lambda 函数
print(add(3, 5))
# 8

4.8 函数注解

在Python中,类型注解(type annotations)是一种可选的语法,用于为函数的参数和返回值指定预期的类型。

注解不会做任何处理,只是存储在函数的__annotations__属性(一个字典)中。换句话说,注解对Python解释器没有任何意义。 注解只是元数据 ,可以供IDE、框架和装饰器等工具使用。

参数类型注解:在函数定义中,参数类型注解紧跟在参数名后面,使用冒号:分隔,用以说明每个参数的预期类型。

返回值类型注解:在函数定义的末尾,使用箭头->后跟类型注解来指定返回值的类型,用于说明函数预期返回的类型。

下例中,

  • name: str 表示 name 参数预期是一个字符串类型。
  • return保存的是返回值注解,即函数声明中->标记的部分,下例中-> str 表示 greet 函数预期返回一个字符串类型的结果。
def greet(name: str) -> str:
    """返回问候语"""
    return f"Hello, {name}!"

# 调用函数
print(greet("Alice"))
# Hello, Alice!

补充信息:

Eli Bendersky在其文章 "Python objects, types, classes, and instances - a glossary" 中提到了关于类(class)和类型(type)的观点。这句话的英文原文是:

"The terms 'class' and 'type' are an example of two names referring to the same concept. To avoid this confusion, I will always try to say 'type' when I mean a type, and 'user-defined class' (or 'user-defined type') when referring to a new type created using the class construct. Note that when we create new types using the C API of CPython, there's no 'class' mentioned - we create a new 'type', not a new 'class'." 这段话的意思是,术语“类”和“类型”指的是同一个概念,但为了避免混淆,作者倾向于在提到类型时使用“类型”一词,而在提到使用class关键字创建的新类型时使用“用户定义的类”或“用户定义的类型”。作者还指出,在使用CPython的C API创建新类型时,并没有提到“类”,而是创建了一个新的“类型”,而不是一个新的“类”。

Python的内部运算符会检查操作数是否满足运算符的要求。然而,在函数定义里,我们一般不包括任何运行时类型检查,但我们可以通过 类型提示 的注解,以达到通过工具(如:mypy)检查来确保参数符合类型提示要求,并方便代码阅读。 我们可以在变量名后加上一个冒号和变量类型(比如:age:int)来指定变量的类型。我们可以在函数(和方法)的参数中这么做,也可以在赋值语句中这么做。另外,我们还可以用->语法说明函数(或一个类方法)的返回值(比如:def odd(n: int) -> bool)。

  • main()函数没有返回值类型;
  • 如果函数确实没有返回值,mypy建议使用->None显式声明这一点;

4.9 嵌套函数

函数可以嵌套定义,内部函数可以访问外部函数的变量。

def outer():
    """外部函数"""
    message = "Hello"

    def inner():
        """内部函数"""
        print(message)

    # 调用内部函数
    inner()

# 调用外部函数
outer()
# Hello

4.10 闭包

维基百科中的解释:闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。 这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

闭包是指内部函数引用了外部函数的变量,即使外部函数已经执行完毕。闭包延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

def outer():
    """外部函数"""
    message = "Hello"

    def inner():
        """内部函数"""
        print(message)

    return inner  # 返回内部函数

# 调用外部函数,返回内部函数
closure = outer()
closure()
# Hello

例1:计算移动平均值

方法1:传统类实现方式

在传统类实现方式中,Avg 类的实例是一个可调用对象。每次调用实例时,会将新值添加到列表中,并计算当前的平均值。

class Avg:
    def __init__(self):
        self.mylist = []  # 用于存储历史值

    def __call__(self, newValue):
        self.mylist.append(newValue)  # 添加新值
        total = sum(self.mylist)  # 计算总和
        avg = total / len(self.mylist)  # 计算平均值
        print(f"List: {self.mylist}, Rolling Average: {avg}")
        return avg


# 使用示例
avg = Avg()

avg(10)  # List: [10], Rolling Average: 10.0
avg(20)  # List: [10, 20], Rolling Average: 15.0
avg(30)  # List: [10, 20, 30], Rolling Average: 20.0

特点:

  • Avg 类的实例 avg 是一个可调用对象。
  • 历史值存储在实例属性 self.mylist 中。

方法2:高阶函数实现方式

在高阶函数实现方式中,调用 make_avg 会返回一个内部函数 avg。每次调用 avg 时,会将新值添加到列表中,并计算当前的平均值。

  • 历史值存储在自由变量my_list中。
  • 返回的avg函数是一个闭包,闭包中的自由变量可以通过 __closure__ 属性访问。
def make_avg():
    my_list = []  # 自由变量,用于存储历史值

    def avg(newValue):
        my_list.append(newValue)  # 添加新值
        total = sum(my_list)  # 计算总和
        avg = total / len(my_list)  # 计算平均值
        print(f"List: {my_list}, Rolling Average: {avg}")
        return avg

    return avg  # 返回内部函数


# 使用示例
my_avg = make_avg()

my_avg(10)  # List: [10], Rolling Average: 10.0
my_avg(20)  # List: [10, 20], Rolling Average: 15.0
my_avg(30)  # List: [10, 20, 30], Rolling Average: 20.0

# 查看函数的局部变量和自由变量
print(my_avg.__code__.co_varnames)  # ('newValue', 'total')
print(my_avg.__code__.co_freevars)  # ('my_list',)

# 查看闭包中的值
print(my_avg.__closure__[0].cell_contents)  # [10, 20, 30]

特点:

  • make_avg 返回的内部函数 avg 是一个闭包。
  • 历史值存储在自由变量 my_list 中。
  • 闭包中的自由变量可以通过 __closure__ 属性访问。

(3)使用 nonlocal 改进高阶函数实现

在改进的高阶函数实现中,使用 nonlocal 声明自由变量counttotal,避免使用可变对象(如列表),而是通过不可变对象(如整数)来存储状态。

def make_avg():
    count = 0  # 自由变量,用于存储值的数量
    total = 0  # 自由变量,用于存储值的总和

    def avg(newValue):
        nonlocal count, total  # 声明自由变量
        count += 1  # 更新数量
        total += newValue  # 更新总和
        return total / count  # 计算平均值

    return avg  # 返回内部函数


# 使用示例
my_avg = make_avg()

print(my_avg(10))  # 10.0
print(my_avg(20))  # 15.0
print(my_avg(30))  # 20.0

# 查看函数的局部变量和自由变量
print(my_avg.__code__.co_varnames)  # ('newValue',)
print(my_avg.__code__.co_freevars)  # ('count', 'total')

# 查看闭包中的值
print(my_avg.__closure__[0].cell_contents)  # 3 (count 的最终值)
print(my_avg.__closure__[1].cell_contents)  # 60 (total 的最终值)

特点:

  • 使用 nonlocal 声明自由变量 counttotal
  • 避免了使用可变对象(如列表),而是通过不可变对象(如整数)来存储状态。
  • 闭包中的自由变量可以通过 __closure__ 属性访问。

(1)和(2)两种实现方式的共通点:

  • 无论是类实现还是高阶函数实现,调用 Avg()make_avg() 都会返回一个可调用对象 avg
  • 每次调用 avg(n) 时,都会将 n 添加到历史值中,并重新计算当前的平均值。

闭包的深入理解:

  • 自由变量:指未在本地作用域中绑定的变量(如 my_listcounttotal)。
  • 闭包:闭包是一个函数及其相关的引用环境(即自由变量的绑定)。闭包允许函数访问其定义时的作用域中的变量,即使函数在其定义作用域之外执行。
  • __closure__ 属性:返回的闭包对象是一个元组,每个元素是一个 cell 对象,包含自由变量的值。

总结:

  • 类实现:适合需要维护复杂状态的场景。
  • 高阶函数实现:适合需要轻量级闭包的场景。
  • nonlocal 的使用:适合需要修改自由变量的场景,避免了使用可变对象。

例2:计数器

在下面的例子中,money 是一个局部变量,定义在函数 get_money 中。通常情况下,当外围函数 get_money 执行完毕后,其局部变量 money 应该不再存在。然而,由于嵌套函数 work 引用了 money 这个自由变量,局部变量 money 被封闭在了嵌套函数 work 中,从而形成了一个闭包。因此,即使 get_money 函数执行完毕,money 变量仍然可以通过闭包被访问和修改。

通过调用 closure = get_money(),我们获得了一个闭包对象 closure,它实际上就是嵌套函数 work。当我们执行 closure() 时,实际上是调用了 work() 函数,它会打印出当前 money 的值,并将 money 的值增加100。

def get_money():
    money = 0

    def work():
        nonlocal money
        money += 100
        print(money)

    return work


closure = get_money()

closure()
# 100
closure()
# 200
closure()
# 300

在这个例子中,nonlocal 关键字用于声明 money 变量,表明它是一个自由变量,即在内嵌函数 work() 的作用域之外定义的变量。通过使用 nonlocal,我们可以在 work() 函数中修改 money 的值,而这个修改会反映在闭包中保存的 money 变量上。

需要注意的是,如果要修改全局变量,可以使用 global 关键字进行声明。但对于内嵌函数中引用的自由变量,必须使用 nonlocal 进行声明,以便正确地修改其值。

4.11. 递归函数

函数可以调用自身,称为递归。

def factorial(n):
    """计算阶乘"""
    if n == 1:
        return 1
    else:
        return n * factorial(n - 1)

# 调用函数
print(factorial(5))
# 120

4.12 生成器

生成器(Generators) 是一个用于创建迭代器的简单而强大的工具。

生成器函数

生成器的写法类似于标准的函数,即生成器函数,是一种特殊的函数,它使用yield语句来返回值,而不是使用return语句。生成器函数的主要特点是它们可以暂停执行并在以后恢复执行(它会记住上次执行语句时的所有数据值),这使得它们非常适合用于生成一系列值,而不需要一次性将所有值存储在内存中。

生成器函数特点:

  • 惰性求值:生成器函数不会立即计算所有值,而是按需生成值。这意味着它们可以用于处理大量数据或无限序列,而不会消耗过多内存。
  • 状态保持:生成器函数在每次调用next()方法时,会从上次暂停的地方恢复执行,并记住之前的状态。这使得它们可以维护内部状态,而不需要使用外部变量。
  • 可迭代:生成器函数返回的对象是一个迭代器,可以用于for循环或其他迭代操作。

生成器函数的基本语法如下:

def generator_function():
    # 生成器函数体
    yield value
    # 可以有多个yield语句

yield语句用于返回一个值,并暂停函数的执行。当生成器的next()方法被调用时,函数会从上次yield语句之后的位置继续执行。

下面是一个生成器函数的示例,生成斐波那契数列:

def fibonacci(n):
    """生成斐波那契数列的生成器函数"""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# 使用生成器函数
fib = fibonacci(10)
for num in fib:
    print(num)
# 0
# 1
# 1
# 2
# 3
# 5
# 8
# 13
# 21
# 34

在上面示例中:

  • fibonacci函数是一个生成器函数,它使用yield语句来返回斐波那契数列中的每个数。
  • 当调用fibonacci(10)时,它返回一个生成器对象fib
  • 我们可以使用for循环来迭代生成器对象,每次迭代都会调用生成器的next()方法,从而生成下一个斐波那契数。

下面是一个生成器函数的示例,生成从1n的数字:

def count_up_to(n):
    """生成从 1 到 n 的数字"""
    i = 1
    while i <= n:
        yield i
        i += 1

# 调用生成器函数
for num in count_up_to(5):
    print(num)
# 1
# 2
# 3
# 4
# 5

生成器表达式

用生成器表达式来创建生成器更为简单。生成器表达式与列表、字典、集合的推导式很类似,创建一个生成器表达式,只需要将列表推导式的中括号替换为小括号即可。

gen1 = (x ** 2 for x in range(100))

print(gen1)  
# <generator object <genexpr> at 0x7fd3f30c9580>

上面的代码与下面的生成器是等价的

def _make_gen():
    for x in range(100):
        yield x ** 2

gen2 = _make_gen()
print(gen2)  
# <generator object _make_gen at 0x7fceb69ed580>

生成器表达式可以作为函数参数用于替代列表推导式。对比下面2个例子。

示例1:

result1 = sum(x**2 for x in range(100))
print(result1)
# 328350

gen1 = (x**2 for x in range(100))
result1 = sum(gen1)
print(result1)
# 328350

示例2:

result2 = dict((i, i**2) for i in range(5))
print(result2)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

gen2 = ((i, i**2) for i in range(5))
result2 = dict(gen2)
print(result2)
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

生成器itertools模块

标准库中的itertools模块是适用于大多数数据算法的生成器集合。

import itertools

first_letter = lambda x: x[0]
names = ['Alan', 'Adam', 'Wes', 'Will', 'Albert', 'Steven']

for letter, names in itertools.groupby(names, first_letter):
    print(letter)
    print(first_letter)
    print(letter, list(names))  # names is generator

# A
# <function <lambda> at 0x7fa598a7a0d0>
# A ['Alan', 'Adam']
# W
# <function <lambda> at 0x7fa598a7a0d0>
# W ['Wes', 'Will']
# A
# <function <lambda> at 0x7fa598a7a0d0>
# A ['Albert']
# S
# <function <lambda> at 0x7fa598a7a0d0>
# S ['Steven']

调用生成器函数的几种方式

方式1: 使用 for 循环

这是最常见和最简单的方式来调用生成器函数。直接在 for 循环中使用生成器函数,循环会自动处理生成器的迭代过程。

def simple_generator():
    yield 1
    yield 2
    yield 3

# 使用 for 循环调用生成器函数
for value in simple_generator():
    print(value)
# 输出:
# 1
# 2
# 3

代码执行流程:

  • 生成器创建:当调用 simple_generator() 时,Python创建了一个生成器对象,但不会立即执行函数体中的代码。
  • 迭代开始:for 循环开始迭代生成器对象。在每次迭代中,循环会调用生成器的 __next__() 方法。
  • 第一次迭代:执行到 yield 1,生成器返回值 1,并暂停执行。for 循环打印出 1
  • 第二次迭代:__next__() 方法再次被调用,生成器从上次暂停的地方恢复执行,执行到 yield 2,返回值 2,并暂停执行。for 循环打印出 2
  • 第三次迭代:__next__() 方法再次被调用,生成器继续执行到 yield 3,返回值 3,并暂停执行。for 循环打印出 3
  • 迭代结束:当生成器函数执行完毕,没有更多的 yield 语句时,它会抛出 StopIteration 异常,for 循环随之结束。

方式2: 使用 next() 函数

手动创建生成器对象,并使用 next() 函数来获取生成器的下一个值。每次调用 next() 函数,生成器函数会从上次 yield 语句之后的位置继续执行,直到遇到下一个 yield 语句。

def simple_generator():
    yield 1
    yield 2
    yield 3

# 创建生成器对象
gen = simple_generator()

# 使用 next() 函数获取值
print(next(gen))  # 1
print(next(gen))  # 2
print(next(gen))  # 3

方式3: 使用 list() 或其他迭代器转换函数

将生成器对象转换为列表或其他可迭代对象,这会立即消耗生成器中的所有值。

def simple_generator():
    yield 1
    yield 2
    yield 3

# 将生成器转换为列表
gen_list = list(simple_generator())
print(gen_list)
# [1, 2, 3]

方式4: 使用 iter() 函数

虽然生成器对象本身就是一个迭代器,但可以使用 iter() 函数来获取生成器的迭代器。

def simple_generator():
    yield 1
    yield 2
    yield 3

# 创建生成器对象
gen = simple_generator()

# 使用 iter() 函数获取迭代器
gen_iter = iter(gen)

# 使用 next() 函数获取值
print(next(gen_iter))  # 1
print(next(gen_iter))  # 2
print(next(gen_iter))  # 3
  • 当生成器对象耗尽所有值后,再次调用 next() 函数会引发 StopIteration 异常。通常,for 循环会自动处理这个异常,但在手动调用 next() 函数时,需要捕获这个异常或使用其他方式来处理生成器的耗尽。
  • 生成器函数非常适合用于处理大量数据或无限序列,因为它们按需生成值,不需要一次性将所有值存储在内存中。

5. 命名空间、作用域和本地函数

5.1. 命名空间

命名空间(Namespace)是一个从名称到对象的映射。Python 中的每个模块、函数和类都有自己的命名空间,用于存储变量、函数和类的名称。

命名空间的类型:

  1. 内置命名空间(Built-in Namespace):

    • 包含 Python 内置函数和异常(如 printlenTypeError 等)。
    • 在 Python 解释器启动时创建,程序结束时销毁。
  2. 全局命名空间(Global Namespace):

    • 包含模块级别的变量、函数和类。
    • 在模块被导入时创建,程序结束时销毁。
  3. 局部命名空间(Local Namespace):

    • 包含函数或方法内部的变量、函数和类。
    • 在函数或方法调用时创建,函数或方法返回时销毁。
# 全局命名空间
x = 10  # 全局变量

def foo():
    # 局部命名空间
    y = 20  # 局部变量
    print(x, y)  # 访问全局变量和局部变量

foo()
# 10 20

5.2 作用域

作用域(Scope)是命名空间在程序中的可见范围。Python 中的作用域遵循 LEGB 规则

  • L(Local):局部作用域,包含函数内部的变量。
  • E(Enclosing):嵌套函数的外层函数作用域。
  • G(Global):全局作用域,包含模块级别的变量。
  • B(Built-in):内置作用域,包含 Python 内置名称。

当访问一个变量时,Python 会按照 LEGB 顺序查找:

  1. 先在局部作用域(Local)中查找。
  2. 如果未找到,则在外层函数作用域(Enclosing)中查找。
  3. 如果仍未找到,则在全局作用域(Global)中查找。
  4. 如果仍未找到,则在内置作用域(Built-in)中查找。
  5. 如果仍未找到,则抛出 NameError 异常。
x = 10  # 全局作用域

def outer():
    y = 20  # 外层函数作用域

    def inner():
        z = 30  # 局部作用域
        print(x, y, z)  # 访问全局、外层和局部变量

    inner()

outer()
# 10 20 30

5.3 本地函数

本地函数(Local Function)是在另一个函数内部定义的函数。本地函数可以访问外层函数的变量(即 闭包)。

def outer():
    x = 10  # 外层函数变量

    def inner():
        print(x)  # 访问外层函数变量

    inner()  # 调用本地函数

outer()
# 10

5.4 闭包

如果本地函数引用了外层函数的变量,并且外层函数返回了本地函数,则形成了闭包。闭包可以记住外层函数的状态。

def outer():
    x = 10  # 外层函数变量

    def inner():
        print(x)  # 访问外层函数变量

    return inner  # 返回本地函数


closure = outer()
closure()
# 10

5.5 globalnonlocal 关键字

  • global:用于在函数内部修改全局变量。
  • nonlocal:用于在嵌套函数中修改外层函数的变量。
x = 10  # 全局变量

def outer():
    y = 20  # 外层函数变量

    def inner():
        nonlocal y  # 修改外层函数变量
        global x    # 修改全局变量
        y += 1
        x += 1
        print(x, y)

    inner()

outer()  # 输出: 11 21

5.6 命名空间与作用域的关系

  • 命名空间是名称到对象的映射。
  • 作用域是命名空间在程序中的可见范围。
  • 每个作用域对应一个命名空间。
x = 10  # 全局命名空间

def foo():
    y = 20  # 局部命名空间
    print(x, y)  # 访问全局和局部命名空间

foo()
# 10 20

数据清洗示例

states = [
    "   Alabama",
    "Georgia!",
    "georgia",
    "Georgia",
    "FlOrIda",
    "south    carolina##",
    "West virginia? ",
]

# 方法1
import re


# 定义一个函数,用于清理字符串列表
def clean_string1(strings):
    result2 = []
    for value in strings:
        value = value.strip()  # 去除字符串两端的空格
        value = re.sub("[! #? ]", "", value)  # 去除字符串中的特定字符
        value = value.title()  # 将字符串转换为标题格式
        result2.append(value)  # 将清理后的字符串添加到结果列表中
    return result2


# 打印清理后的字符串列表
print(clean_string1((states)))
# ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'Southcarolina', 'Westvirginia']


# 方法2
# 定义一个函数,用于去除字符串中的特定字符
def remove_punctuaion(value):
    return re.sub("[! #? ]", "", value)


# 定义一个包含多个清理操作的列表
clean_ops = [str.strip, remove_punctuaion, str.title]


# 定义一个函数,用于按顺序应用多个清理操作
def clean_string2(strings, ops):
    result3 = []
    for value in strings:
        for function in ops:
            value = function(value)  # 依次应用每个清理操作
        result3.append(value)  # 将清理后的字符串添加到结果列表中
    return result3


# 获取并打印清理后的字符串列表
result4 = clean_string2(states, clean_ops)

print(result4)
# ['Alabama', 'Georgia', 'Georgia', 'Georgia', 'Florida', 'Southcarolina', 'Westvirginia']


# 可以将函数作为一个参数传给其他的函数。
# 使用 map 函数将 remove_punctuaion 应用于 states 列表中的每个元素
for x in map(remove_punctuaion, states):
    print(x)

# Alabama
# Georgia
# georgia
# Georgia
# FlOrIda
# southcarolina
# Westvirginia

6. 柯里化:部分参数应用

柯里化是计算机科学术语(以数学家Haskell Curry命名),它表示通过部分参数应用的方式从已有的函数中衍生出新的函数。柯里化是一种将多参数函数转化为单参数高阶函数的技术,如果你固定某些参数,你将得到接受余下参数的一个函数。

  • 定义一: 柯里化:一个函数中有个多个参数,想固定其中某个或者几个参数的值,而只接受另外几个还未固定的参数,这样函数演变成新的函数。

  • 定义二: 函数柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

  • 定义三: 一些函数式语言的工作原理是将多参数函数语法转化为单参数函数集合,这一过程称为柯里化,它是以逻辑学家Haskell Curry的名字命名的。Haskell Curry从早期概念中发展出了该理论。其形式相当于将z=f(x, y)转换成z=f(x)(y)的形式,原函数由两个参数,现在变为两个接受单参数的函数,

6.1 手动实现柯里化

在这个例子中,curry_add 函数接受一个参数 a,并返回一个接受参数 b 的函数 add_badd_b 又返回一个接受参数 c 的函数 add_c。最终,add_c 返回三个参数的和。

# 普通写法
def add(x, y, z):
    return x + y + z

print(add(1, 2, 3))  
# 6

# 柯里化写法
def curry_add(a):
    def add_b(b):
        def add_c(c):
            return a + b + c
        return add_c
    return add_b

# 使用柯里化函数
curried_add = curry_add(1)(2)(3)
print(curried_add)
# 6

# 通过固定其中的第二个参数不变来实现柯里化
def curry_add(a, b):
    def add_c(a, b, c):
        return a + b + c

    return add_c(a, 100, b)


result = curry_add(12, 13)
print(result)
# 125

result = curry_add(12, 555, 13)
# TypeError: curry_add() takes 2 positional arguments but 3 were given

6.2 使用functools.partial实现柯里化

functools.partial可以固定函数的部分参数,生成一个新的函数。

from functools import partial


def add(x, y, z):
    return x + y + z


# 固定第一个参数,add_1现在是一个接受两个参数y和z的函数,当调用时,它会将1作为第一个参数传递给add函数。
add_1 = partial(add, 1)
# 固定第二个参数,add_1_2现在是一个接受一个参数z的函数,当调用时,它会将1和10作为前两个参数传递给add函数。
add_1_2 = partial(add_1, 10)
# 调用新函数,这个调用实际上是调用add(1, 10, 100)
result = add_1_2(100)
print(result)
# 111

6.3 通过lambda表达式来实现柯里化

def add1(a, b, c):
    return a + b + c

add2 = lambda x, y: add1(x, 100, y)

result = add2(12, 13)

print(result)  
# 1251

6.4 通过装饰器来实现柯里化

def add1(a, b, c):
    return a + b + c

def currying_add(func):
    def wrapper(a, c, b=100):
        return func(a, b, c)
    return wrapper

result = currying_add(add1)(12, 13)

print(result)  
# 125

6.5 通过装饰器符号@来实现柯里化

def currying_add(func):
    def wrapper(a, c, b=100):
        return func(a, b, c)
    return wrapper

@currying_add
def add(a, b, c):
    return a + b + c

result = add(12, 13)

print(result)
# 125

7. 错误和异常处理

在Python中,异常处理是一种机制,用于在程序运行时捕获和处理错误或异常情况。异常对象(exception object)是表示异常的实例,它们提供了关于错误的详细信息。遇到错误后,会引发异常。如果异常对象并未被处理或捕捉,程序就会用所谓的回溯(traceback, 一种错误信息)终止执行。

异常和语法错误是有区别的。

  • 错误:是指代码不符合解释器或者编译器语法。
  • 异常:是指不完整、不合法输入,或者计算出现错误。

7.1 异常处理

Python使用try-except语句来实现异常处理。基本语法如下:

try:
    # 尝试执行的代码
    pass
except ExceptionType as e:
    # 处理异常的代码
    pass
  • try块:包含可能引发异常的代码。如果在try块中发生异常,Python会立即停止执行try块中的剩余代码,并跳转到相应的except块。
  • except块:用于捕获和处理特定类型的异常。可以指定一个或多个异常类型,并为每个异常类型提供一个处理代码块。as e部分是可选的,用于将异常对象赋值给变量e,以便在处理异常时访问异常的详细信息。
  • 多个except块:为不同的异常类型提供多个except块,以便对不同类型的异常进行不同的处理。
  • else块:可选的else块用于当try块中的代码没有引发异常时执行。它必须位于所有except块之后。
  • finally块:可选的finally块用于执行无论是否发生异常都需要执行的代码。它通常用于清理资源,如关闭文件或释放网络连接。

7.2 异常对象

异常对象是表示异常的实例,它们提供了关于错误的详细信息。在Python中,所有异常都是BaseException类的子类,通常是Exception类的子类。

有两个关键的内置异常类,SystemExitKeyboardInterrupt,它们直接继承自BaseException类,而不是Exception类。

  • SystemExit异常在程序自然退出时被抛出,通常是因为我们在代码的某处调用了sys.exit()函数,设计这个异常的目的是,在程序最终退出之前完成清理工作。
  • KeyboardInterrupt异常常见于命令行程序。

以下是一些常用的异常类型:

  • ValueError:当传入的值不符合预期时引发。
  • TypeError:当操作或函数应用于不适当类型的对象时引发。
  • IndexError:当索引超出序列的范围时引发。
  • KeyError:当字典中找不到指定的键时引发。
  • IOError:当输入/输出操作失败时引发(在Python 3中,IOError已被OSError取代)。
  • RuntimeError:当检测到不适当的或意外的运行时条件时引发。

异常对象通常包含以下属性和方法:

  • args:一个包含异常参数的元组。
  • message:异常的描述信息(在Python 3中,建议使用str(e)来获取异常的描述)。
  • __str__():返回异常的字符串表示。
  • __repr__():返回异常的官方字符串表示。

在编写代码时,尽量使用具体的异常类型进行捕获,而不是使用通用的Exception类,以便更准确地处理不同类型的错误。

以下是一个简单的异常处理示例:

try:
    # 尝试除以零
    result = 10 / 0
except ZeroDivisionError as e:
    # 处理除以零的异常
    print(f"Error: {e}")
else:
    # 如果没有异常发生,执行这里的代码
    print("Division successful.")
finally:
    # 无论是否发生异常,都执行这里的代码
    print("Cleanup code.")

在这个示例中,try块中的代码尝试执行除以零的操作,这会引发ZeroDivisionError异常。except块捕获该异常,并打印错误信息。无论是否发生异常,finally块都会执行,用于执行清理代码。

补充:

Python程序员倾向于这样一个原则:请求宽恕比请求许可更容易(It's Easier to Ask Forgiveness than Permission),简称为EAFP。也就是说,他们先执行代码,然后解决错误。另一种“三思而后行”(Look Before You Leap)的原则则是反其道而行之,简称LBYL,没有那么流行。

data = {"a": 1, "b": 2}

# EAFP: 先尝试访问键,如果出错则捕获异常
try:
    value = data["c"]
except KeyError:
    value = "default_value"

print(value)  # 输出: default_value

# LBYL: 先检查键是否存在,如果存在则访问
if "c" in data:
    value = data["c"]
else:
    value = "default_value"
print(value)  # 输出: default_value

8. 文件与操作系统

Python 提供了内置模块(如 osshutilpathlib 等)来处理文件和目录,以及与操作系统进行交互。

  • 文件操作:使用 open() 函数进行文件的读取、写入和追加。
  • 目录操作:使用 osshutil 模块创建、删除和遍历目录。
  • 路径操作:使用 os.pathpathlib 处理文件路径。
  • 操作系统交互:使用 ossubprocess 模块执行系统命令和管理环境变量。

8.1 文件操作

Python 提供了内置的 open() 函数来操作文件。文件操作通常包括 读取写入追加 数据。

打开文件

使用 open() 函数打开文件,并指定模式(如 r 表示读取,w 表示写入,a 表示追加)。

# 打开文件(读取模式)
file = open("example.txt", "r")
content = file.read()  # 读取文件内容
file.close()  # 关闭文件

# 使用 with 语句自动关闭文件
with open("example.txt", "r") as file:
    content = file.read()
  • f=open(path, 'w'),一个新的文件会在path指定的路径被创建,并在同一路径下覆盖同名文件。(请小心!)
  • f=open(path, 'x'),一个新的文件会在path指定的路径被创建,如果给定路径下已经存在同名文件就会创建失败。
import os

# 查看当前路径
os.getcwd()
# '/opt/myMemo'

# 更改文件读取默认路径
os.chdir('~/datasets/examples')

# 指定文件名
path = 'file01.txt'

# 打开文件
f = open(path)

# 读取文件每一行,文件每一行作为列表一个元素
lines = [x.rstrip() for x in open(path)]

# 输出列表
print(lines)

# 关闭文件会将资源释放回操作系统
f.close()  

另一种更简单的关闭文件的方式

import os

# 查看当前路径
current_path = os.getcwd()

# 更改文件读取默认路径
os.chdir(current_path + "/docs/python/datasets/examples")

# 指定文件名
path = "file01.txt"

# 打开文件
f = open(path)

# 读取文件每一行,文件每一行作为列表一个元素
lines = [x.rstrip() for x in open(path)]

# 输出列表
print(lines)

# 关闭文件会将资源释放回操作系统
f.close()

在打开文件时使用seek读取文件内容要当心。如果文件的句柄位置恰好在一个Unicode符号的字节中间时,后续的读取会导致错误。

import os

# 查看当前路径
current_path = os.getcwd()

# 更改文件读取默认路径
os.chdir(current_path + "/docs/python/datasets/examples")

# 指定文件名
path = 'file01.txt'

# 打开文件
f = open(path)

# 读取文件。
print(f.read(5))  # 输出前5个字符。 read方法通过读取的字节数来推进文件句柄的位置。
# I Thi 
print(f.tell())  # tell方法可以给出句柄当前的位置
# 5  
print(f.seek(6))  # seek方法可以将句柄位置改变到文件中特定的字节
# 6  
print(f.read(1))  # 从第7个字节开始,输出1个字节
# k 

# 关闭文件会将资源释放回操作系统
f.close()

如果使用二进制方式打开文件,则:

import os

# 查看当前路径
current_path = os.getcwd()

# 更改文件读取默认路径
os.chdir(current_path + "/docs/python/datasets/examples")

# 指定文件名
path = 'file01.txt'

# 打开文件
f2 = open(path, 'rb')  # 二进制模式

# 读取文件
print(f2.read(5))  # 第一个b代表二进制格式
# b'I Thi'  
print(f2.tell())  
# 5
print(f2.seek(6))  
# 6
print(f2.read(2))  # 从第7个字节开始,输出2个字节
# b'k '  

# 关闭文件会将资源释放回操作系统
f2.close()

读取文件

  • read():读取整个文件内容。
  • readline():读取一行内容。
  • readlines():读取所有行,返回一个列表。
with open("file01.txt", "r") as file:
    print(file.read())  # 读取整个文件
    print(file.readline())  # 读取一行
    print(file.readlines())  # 读取所有行

写入文件

  • write():写入字符串。
  • writelines():写入字符串列表。
with open("file01.txt", "w") as file:
    file.write("Hello, World!\n")  # 写入一行
    file.writelines(["Line 1\n", "Line 2\n"])  # 写入多行

将本文写入文件,可以使用文件对象的writewirtelines方法。

import os

# 查看当前路径
current_path = os.getcwd()

# 更改文件读取默认路径
os.chdir(current_path + "/docs/python/datasets/examples")

# 指定文件名
path1 = 'file01.txt'
path2 = 'file02.txt'  # file02.txt是一个空文件

with open(path2, 'r+', encoding='utf-8') as f:
    f.writelines(x for x in open(path1, 'r', encoding='utf-8') if len(x) > 1)  # 把file01.txt的内容写入file02.txt
    lines = f.readlines()
    print(lines)

追加数据

使用 a 模式打开文件,可以在文件末尾追加数据。

with open("file01.txt", "a") as file:
    file.write("This is a new line.\n")

8.2 目录操作

Python 提供了 ospathlib 模块来操作目录。

创建目录

使用 os.mkdir()os.makedirs() 创建目录。

import os

# 创建单层目录
os.mkdir("new_dir")

# 创建多层目录
os.makedirs("parent_dir/child_dir")

删除目录

使用 os.rmdir()shutil.rmtree() 删除目录。

import os
import shutil

# 删除空目录
os.rmdir("new_dir")

# 删除非空目录
shutil.rmtree("parent_dir")

遍历目录

使用 os.listdir()os.walk() 遍历目录。

import os

# 列出目录内容
print(os.listdir("."))  # 当前目录

# 递归遍历目录
for root, dirs, files in os.walk("."):
    print(root, dirs, files)

8.3 路径操作

Python 提供了 os.pathpathlib 模块来处理文件路径。

使用 os.path

os.path 提供了许多函数来处理路径。

在使用os.path.join()的时候,需要留意下面的特点:

  • 绝对路径:以 / 开头的路径被视为绝对路径,os.path.join() 会忽略之前的路径组件。
  • 相对路径:不以 / 开头的路径被视为相对路径,os.path.join() 会将其与之前的路径组件合并。
  • 正确使用:确保在拼接路径时,相对路径不以 / 开头,以避免绝对路径的覆盖问题。
import os

# 查看当前路径
current_path = os.getcwd()

# 拼接路径
path = os.path.join(current_path, "docs/python/datasets/examples/file01.txt")

# 获取绝对路径
abs_path = os.path.abspath("file01.txt")

# 检查路径是否存在
print(os.path.exists(path))
# True

# 获取文件名和目录名
print(os.path.basename(path))
# file01.txt
print(os.path.dirname(path))
# /opt/mySite/docs/python/datasets/examples

使用 pathlib

pathlib 提供了面向对象的路径操作方式。

import os
from pathlib import Path

# 查看当前路径
current_path = os.getcwd()

# 创建 Path 对象
path = Path(current_path + "/docs/python/datasets/examples/file01.txt")

# 获取绝对路径
abs_path = path.absolute()

# 检查路径是否存在
print(path.exists())
# True

# 读取文件内容
print(path.read_text())

8.4 与操作系统交互

Python 提供了 ossubprocess 模块来与操作系统交互。

执行系统命令

使用 os.system()subprocess.run() 执行系统命令。

import os
import subprocess

# 使用 os.system
cmd = "ls -l"
os.system(cmd)

# 使用 subprocess.run
result = subprocess.run(["ls", "-l"], capture_output=True, text=True)
print(result.stdout)

获取环境变量

使用 os.environ 获取或设置环境变量。

import os

# 获取环境变量
var_home = os.environ.get("HOME")
print(var_home)

# 设置环境变量
os.environ["MY_VAR"] = "value"
var_myvar = os.environ.get("MY_VAR")
print(var_myvar)

8.5 文件与目录的高级操作

复制文件或目录

使用 shutil.copy()shutil.copytree() 复制文件或目录。

场景:假设有一个名为 example_project 的项目目录,其中包含一些源代码文件和资源文件。我们准备将这些文件和目录复制到另一个位置,以便进行备份或分发。

项目目录结构如下:

example_project/
├── source.txt
├── module1.py
├── module2.py
└── resources/
    ├── image1.png
    └── image2.png

在下面的代码中,我们使用 shutil.copy() 函数将 source.txt 文件从 example_project 目录复制到 backup 目录。os.makedirs() 函数用于确保目标目录存在,exist_ok=True 参数表示如果目录已存在,则不会引发异常。

import shutil

# 复制单个文件
source_file = "example_project/source.txt"
destination_file = "backup/source.txt"

# 确保目标目录存在
os.makedirs(os.path.dirname(destination_file), exist_ok=True)

# 复制文件
shutil.copy(source_file, destination_file)
print(f"File '{source_file}' copied to '{destination_file}'")

在下面的代码中,我们使用 shutil.copytree() 函数将 resources 目录从 example_project 复制到 backup 目录。shutil.rmtree() 函数用于删除已存在的目标目录,因为 copytree() 不会覆盖已存在的目录。

import shutil
import os

# 复制整个目录
source_dir = "example_project/resources"
destination_dir = "backup/resources"

# 确保目标目录不存在,因为 copytree() 不会覆盖已存在的目录
if os.path.exists(destination_dir):
    shutil.rmtree(destination_dir)

# 复制目录
shutil.copytree(source_dir, destination_dir)
print(f"Directory '{source_dir}' copied to '{destination_dir}'")

提示:

  • 文件覆盖:shutil.copy() 会覆盖目标文件,如果目标文件已存在。
  • 目录覆盖:shutil.copytree() 不会覆盖已存在的目标目录。如果需要复制到已存在的目录,可以先删除目标目录或使用其他方法来合并目录内容。
  • 权限问题:在复制文件和目录时,可能会遇到权限问题。确保我们有足够的权限来访问源文件/目录和目标位置。
  • 大文件复制:对于大文件的复制,可以考虑使用 shutil.copyfileobj() 函数,它允许以流的方式复制文件内容,从而减少内存占用。

移动文件或目录

以下是一个实际的例子,展示了如何使用 shutil.move() 函数来移动文件或目录:

示例场景:假设有下面的项目目录,其中有一些临时文件和日志文件,我们希望将这些文件移动到一个专门的备份目录中,以便进行归档和清理。

项目目录结构

project/
├── source.txt
├── log.txt
└── temp/
    ├── temp_file1.txt
    └── temp_file2.txt

在下面的代码中,我们使用 shutil.move() 函数将 source.txt 文件从 project 目录移动到 backup 目录。os.makedirs() 函数用于确保目标目录存在,exist_ok=True 参数表示如果目录已存在,则不会引发异常。

import shutil
import os

# 移动单个文件
source_file = "project/source.txt"
destination_file = "backup/source.txt"

# 确保目标目录存在
os.makedirs(os.path.dirname(destination_file), exist_ok=True)

# 移动文件
shutil.move(source_file, destination_file)
print(f"File '{source_file}' moved to '{destination_file}'")

在下面的代码中,我们使用 shutil.move() 函数将 temp 目录从 project 移动到 backup 目录。shutil.rmtree() 函数用于删除已存在的目标目录,因为 move() 会覆盖已存在的目录。

import shutil
import os

# 移动整个目录
source_dir = "project/temp"
destination_dir = "backup/temp"

# 确保目标目录不存在,因为 move() 会覆盖已存在的目录
if os.path.exists(destination_dir):
    shutil.rmtree(destination_dir)

# 移动目录
shutil.move(source_dir, destination_dir)
print(f"Directory '{source_dir}' moved to '{destination_dir}'")

提示:

  • 文件覆盖:shutil.move() 会覆盖目标文件或目录,如果目标已存在。
  • 跨文件系统移动:如果源文件和目标文件位于不同的文件系统,shutil.move() 实际上会先复制文件,然后删除源文件。
  • 权限问题:在移动文件和目录时,可能会遇到权限问题。确保你有足够的权限来访问源文件/目录和目标位置。
  • 错误处理:在实际应用中,建议添加适当的错误处理逻辑,例如捕获并处理 shutil.Error 异常,以便在移动过程中发生错误时能够进行相应的处理。

9.设计模式

9.1 工厂函数

在Python中,工厂函数是一种设计模式,用于创建对象,而不需要直接指定具体的类。工厂函数通常返回一个类的实例,或者返回一个函数,这个函数可以用来创建对象。使用工厂函数可以提高代码的灵活性和可维护性,因为它将对象的创建逻辑封装在一个单独的函数中,使得代码更加模块化。

(1)使用工厂函数创建类实例

以下是一个使用工厂函数创建类实例的例子:

class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

def get_pet(pet_type):
    """工厂函数,根据pet_type返回相应的宠物实例"""
    if pet_type == "dog":
        return Dog()
    elif pet_type == "cat":
        return Cat()
    else:
        raise ValueError("Unknown pet type")

# 使用工厂函数创建宠物实例
dog = get_pet("dog")
print(dog.speak())
# Woof!

cat = get_pet("cat")
print(cat.speak())
# Meow!

在这个例子中,get_pet 是一个工厂函数,它根据传入的 pet_type 参数返回一个 DogCat 类的实例。这样就可以在不直接指定具体类的情况下创建宠物对象。

(2)使用工厂函数返回函数

函数 maker 中定义了嵌套函数 actionaction 函数引用了 maker 函数作用域内的变量 kn。当 maker 函数执行完毕后,它将 action 函数作为返回值返回。

通过执行 f = maker(2),我们获得了返回对象 action 并将其赋值给变量 f。尽管此时 maker 函数已经结束执行,但对象 f 仍然记住了 maker 函数作用域内的变量 kn 的值。当调用 f(3) 时,f 将传入的参数 x=3 以及之前记住的 kn 的值一并传入 action() 函数,计算并返回 x + n + k 的结果。类似地,f(4)f(5) 也会根据传入的参数和记住的变量值进行计算并返回结果。

def maker(n):
    k = 8

    def action(x):
       return x + n + k

    return action


f = maker(2) 

print(f(3))
# 13
print(f(4))
# 14
print(f(5))
# 15

工厂函数和闭包的关系:

  • 工厂函数是一种设计模式,用于创建对象,而不需要直接指定具体的类。它通常返回一个类的实例,或者返回一个函数,这个函数可以用来创建对象。
  • 闭包是一个函数对象,通常由一个嵌套函数构成,这个嵌套函数引用了其外部函数中的自由变量,即使这些变量在函数外部的作用域中已经不再可用。
  • 工厂函数的主要目的是创建对象,而闭包的主要目的是保持和访问外部函数作用域中的变量。
  • 工厂函数可以返回闭包。在这种情况下,工厂函数的作用是创建并返回一个闭包函数,这个闭包函数可以访问和修改工厂函数作用域中的变量。
  • 闭包可以作为工厂函数的实现方式之一。通过闭包,工厂函数可以创建具有特定行为的函数对象,而无需使用类。

9.2 装饰器

装饰器是一种设计模式,用于在不修改原有函数代码的情况下,增强或改变函数的行为。它通过将一个函数作为参数传递给另一个函数来实现。

(1)通过闭包实现装饰器。

  • 装饰器函数:my_decorator 是一个装饰器函数,它接受一个函数 nestedFunc 作为参数。
  • 嵌套函数:myFuncmy_decorator 内部定义的嵌套函数,用于在调用 nestedFunc 之前和之后执行额外的代码。
  • 返回值:my_decorator 返回 myFunc 函数对象,从而实现对 nestedFunc 的装饰。
def my_decorator(nestedFunc):
    def myFunc():
        """This is a docstring in myFunc()."""
        print("Before executing nestedFunc()")
        nestedFunc()
        print("After executing nestedFunc()")

    return myFunc


def nestedFunc():
    """This is a docstring in nestedFunc()."""
    print("Decoration - executing nestedFunc()")


nestedFunc()  # 直接调用 nestedFunc()
# Decoration - executing nestedFunc()

nestedFunc = my_decorator(nestedFunc)  # 使用 my_decorator 装饰 nestedFunc
nestedFunc()
# Before executing nestedFunc()
# Decoration - executing nestedFunc()
# After executing nestedFunc()

(2)使用 @ 语法简化装饰器应用。

@ 语法:@my_decoratornestedFunc = my_decorator(nestedFunc) 的快捷方式,使得代码更加简洁。

函数名问题:使用装饰器后,原函数的名称和文档字符串可能会被装饰器函数的名称和文档字符串替代,如上例中print(nestedFunc.__name__)返回的结果是myFunc,不是我们预期的nestedFunc

def my_decorator(nestedFunc):
    def myFunc():
        """This is a docstring in myFunc()."""
        print("Before executing nestedFunc()")
        nestedFunc()
        print("After executing nestedFunc()")

    return myFunc


@my_decorator
def nestedFunc():
    """This is a docstring in nestedFunc()."""
    print("New added to decoration - executing nestedFunc()")

nestedFunc()
# Before executing nestedFunc()
# New added to decoration - executing nestedFunc()
# After executing nestedFunc()

print(nestedFunc.__name__)  
# myFunc
print(nestedFunc.__doc__)  
# This is a docstring in myFunc().

(3)使用 functools.wraps 保留原函数信息。

@wraps装饰器**:@wraps(nestedFunc) 用于保留原函数 nestedFunc 的名称和文档字符串,避免被装饰器函数 myFunc 替代。

from functools import wraps

def my_decorator(nestedFunc):
    @wraps(nestedFunc)
    def myFunc():
        """This is a docstring in myFunc()."""
        print("Before executing nestedFunc()")
        nestedFunc()
        print("After executing nestedFunc()")

    return myFunc


@my_decorator
def nestedFunc():
    """This is a docstring in nestedFunc()."""
    print("New added to decoration - executing nestedFunc()")

nestedFunc()
# Before executing nestedFunc()
# New added to decoration - executing nestedFunc()
# After executing nestedFunc()

print(nestedFunc.__name__)  
# nestedFunc
print(nestedFunc.__doc__)  
# This is a docstring in nestedFunc().

(4)装饰器的通用模板。

  • 通用模板:这是一个装饰器的通用模板,使用 *args**kwargs 来接受任意数量的位置参数和关键字参数。
  • 条件控制:在装饰器内部,可以根据条件(如 can_run)来决定是否执行原函数 f
from functools import wraps

def decorator_name(f):
    @wraps(f)
    def decorated(*args, **kwargs):
        if not can_run:
            return "Function will not run"
        return f(*args, **kwargs)
    return decorated

@decorator_name
def func():
    return "Function is running"

can_run = True
print(func())
# Function is running

can_run = False
print(func())
# Function will not run

下面还是一个装饰器的例子。

  • 装饰器 register 用于将函数注册到 registry 列表中。它在函数定义时自动执行,并将函数对象添加到注册表中。
# 创建一个空列表 registry,用于存储注册的函数。
registry = []


# 定义一个装饰器函数 register,用于注册函数。
def register(func):
    print(f"running register {func}")  # 当装饰器被应用时,打印一条消息,表明正在注册函数 func。
    registry.append(func)  # 将函数 func 添加到 registry 列表中。
    return func  # 返回原始函数 func,以便它可以被正常调用。


# 将装饰器 register 应用于函数 f1 和 f2。应用装饰器后,f1 和 f2 会被自动注册到 registry 列表中。
@register
def f1():
    print("running f1()")


@register
def f2():
    print("running f2()")


# 定义一个未注册的函数 f3。
def f3():
    print("running f3()")


def main():
    print("runnning main()")
    print(f"registry--> {registry}") # 打印注册表 registry 的内容,显示已注册的函数。
    f1()
    f2()
    f3()


if __name__ == "__main__":
    main()

执行上述代码段,得到下面的结果。

running register <function f1 at 0x7f70847bec80>
running register <function f2 at 0x7f70705aa9d8>
runnning main()
registry--> [<function f1 at 0x7f70847bec80>, <function f2 at 0x7f70705aa9d8>]
running f1()
running f2()
running f3()

执行顺序分析:

  • Python解释器开始执行这段代码时,首先加载整个代码段。
  • 在代码加载阶段,Python会按顺序执行模块中的顶级代码,包括函数定义和装饰器的调用。
  • 在模块加载阶段,遇到 @register 装饰器时,Python会立即执行 register 函数,并将被装饰的函数作为参数传递给 register

对于 @register 装饰的 f1f2 函数:

  • 首先,执行 register(f1),这会导致打印 running register <function f1 at 0x...>,并将 f1 添加到 registry 列表中。
  • 然后,执行 register(f2),这会导致打印 running register <function f2 at 0x...>,并将 f2 添加到 registry 列表中。

这两个打印语句在模块加载阶段执行,因此在主函数 main 被调用之前,这两个消息就已经被输出了。

总结

  • 装饰器的即时执行:装饰器在函数定义时立即执行,而被装饰的函数只在明确调用时运行,这就是Python的**导入时**和**运行时**之间的区别。因此,register(f1)register(f2) 在模块加载阶段就已经被调用,导致了两个 running register 消息的输出。
  • 模块加载与执行分离:模块加载阶段主要负责加载和初始化代码,而主函数的执行则是在模块加载完成后进行的。这种分离使得装饰器可以在函数定义时立即应用,而不需要等到函数被显式调用。
  • 上面例子中装饰器函数与被装饰的函数在同一个模块中定义。实际应用中,装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
  • 上面例子中register装饰器返回的函数与通过参数传入的相同。实际应用中,大多数装饰器会在内部定义一个函数,然后将其返回。

9.2.1 dataclass

dataclass 是一个装饰器,用于自动为类生成特殊方法,如 __init____repr____eq__ 等,不需要手动编写类重复实现。

使用 dataclass 只需要在类定义前加上 @dataclass 装饰器,并定义类的属性。dataclass 会自动为你生成 __init____repr____eq__ 等方法。

下面是在举例NamedTuple时使用的示例代码。

from typing import NamedTuple

class City(NamedTuple):
    name: str
    country: str
    population: float
    coordinates: tuple

beijing = City("Beijing", "CN", 22.596, (39.9042, 116.4074))

dataclass 改写为:

from dataclasses import dataclass

@dataclass
class City:
    name: str
    country: str
    population: float
    coordinates: tuple

beijing = City("Beijing", "CN", 22.596, (39.9042, 116.4074))

print(beijing)
# City(name='Beijing', country='CN', population=22.596, coordinates=(39.9042, 116.4074))

print(beijing.population)
# 22.596

print(beijing.coordinates)
# (39.9042, 116.4074)

上面通过装饰器@dataclass实现的City类,会自动生成下面的方法:

  • __init__ 方法:自动为你生成初始化方法,接受类属性作为参数,并将它们赋值给实例变量。
  • __repr__ 方法:自动为你生成一个易于阅读的字符串表示,方便调试和日志记录。
  • __eq__ 方法:自动为你生成比较方法,允许你比较两个类实例是否相等。
  • __hash__ 方法:如果类属性都是不可变的,dataclass 会自动为你生成 __hash__ 方法,使类实例可以作为字典的键。
  • __lt____le____gt____ge__ 方法:如果需要比较大小,可以通过 order=True 参数启用这些方法。

比如比较大小(给装饰器添加order=True参数,就会创建所有的比较判断方法):

from dataclasses import dataclass

@dataclass(order=True)
class City:
    name: str
    country: str
    population: float
    coordinates: tuple

beijing = City("Beijing", "CN", 22.596, (39.9042, 116.4074))
shanghai = City("Shanghai", "CN", 26.317, (31.2304, 121.4737))

print(beijing < shanghai)
# True

dataclass支持的比较规则和元组的类似:定义的顺序就是比较的顺序。

  • 逐属性比较:比较从第一个属性开始,如果第一个属性的值不同,则根据第一个属性的值决定大小;如果第一个属性的值相同,则比较第二个属性,依此类推。上例中,按照字典顺序,Beijing 小于 Shanghai,因此返回 True
  • 属性类型:比较时会使用每个属性的自然比较规则。例如,字符串会按照字典顺序比较,数字会按照数值大小比较,元组会逐元素比较。

dataclass 支持为类属性设置默认值。默认值必须放在没有默认值的属性之后。例如:

from dataclasses import dataclass

@dataclass
class City:
    name: str
    country: str = "CN"
    population: float = 0.0
    coordinates: tuple = (0.0, 0.0)

beijing = City("Beijing")
print(beijing)
# City(name='Beijing', country='CN', population=0.0, coordinates=(0.0, 0.0))

在使用dataclass时,通过设置 frozen=True 参数,可以将数据类定义为不可变的。这意味着一旦实例化,类的属性就不能被修改。创建一个类似于typing.Named-Tuple的类。

from dataclasses import dataclass

@dataclass(frozen=True, order=True)
class City:
    name: str
    country: str
    population: float
    coordinates: tuple

beijing = City("Beijing", "CN", 22.596, (39.9042, 116.4074))

try:
    beijing.population = 23.0
except Exception as e:
    print(e)
# cannot assign to field 'population'

对比 dataclassNamedTuple

  • NamedTuple:是一个不可变的数据结构,适合用于定义不可变的数据类型。它生成的类是元组的子类,因此支持元组的所有操作。
  • dataclass:是一个可变的数据结构,更适合用于定义可变的数据类型。它生成的类是普通类,因此支持类的所有操作。
  • 如果我们需要一个不可变的数据结构,并且需要支持默认值、字段元数据等高级功能,建议使用 @dataclass(frozen=True)
  • 如果我们需要一个简单的不可变数据结构,并且不需要额外的功能,typing.NamedTuple 是一个更轻量级的选择。
from typing import NamedTuple

class City(NamedTuple):
    name: str
    country: str
    population: float
    coordinates: tuple
    __defaults__ = ("CN", 0.0, (0.0, 0.0))

beijing = City("Beijing", "CN", 22.596, (39.9042, 116.4074))
shanghai = City("Shanghai", "CN", 26.317, (31.2304, 121.4737))

# 打印实例
print(beijing)
# City(name='Beijing', country='CN', population=22.596, coordinates=(39.9042, 116.4074))

# 比较大小
print(beijing < shanghai)
# True

# 尝试修改属性(会报错)
try:
    beijing.population = 23.0
except Exception as e:
    print(e)
# can't set attribute

dataclass 还支持字段元数据,可以通过 field 函数为每个字段添加额外的元数据。比如:

from dataclasses import dataclass, field

@dataclass
class City:
    name: str
    country: str = field(default="CN", metadata={"description": "Country code"})
    population: float = field(default=0.0, metadata={"description": "Population in millions"})
    coordinates: tuple = field(default=(0.0, 0.0), metadata={"description": "Latitude and Longitude"})

beijing = City("Beijing")
print(beijing)
# City(name='Beijing', country='CN', population=0.0, coordinates=(0.0, 0.0))

# 获取字段的元数据
for field_name, field_def in City.__dataclass_fields__.items():
    print(f"Field: {field_name}")
    print(f"  Default: {field_def.default}")
    print(f"  Metadata: {field_def.metadata}")
    print()
# Field: name
#   Default: <dataclasses._MISSING_TYPE object at 0x7f37b8138560>
#   Metadata: {}

# Field: country
#   Default: CN
#   Metadata: {'description': 'Country code'}

# Field: population
#   Default: 0.0
#   Metadata: {'description': 'Population in millions'}

# Field: coordinates
#   Default: (0.0, 0.0)
#   Metadata: {'description': 'Latitude and Longitude'}

9.3 迭代器

迭代器既不是函数,也不是设计模式,而是一种 编程概念协议。它提供了一种遍历集合(如列表、字典、集合等)中元素的方式,而无需暴露集合的内部实现细节。

  • 迭代器 是一种编程概念或协议,用于遍历集合中的元素。
  • 迭代器设计模式 是一种设计思想,迭代器是其具体实现。
  • 生成器函数 是一种实现迭代器的便捷方式。
  • 迭代器不是函数,但可以通过函数(如生成器函数)来实现。

(1)迭代器的定义

迭代器是一个实现了 迭代器协议 的对象。迭代器协议包括两个方法:

  • __iter__():返回迭代器对象本身。
  • __next__():返回集合中的下一个元素。如果没有更多元素,则抛出 StopIteration 异常。

(2)迭代器的特点

  • 惰性计算:迭代器按需生成元素,而不是一次性生成所有元素。
  • 节省内存:适用于处理大型数据集,因为它不需要将所有数据加载到内存中。
  • 单向遍历:迭代器只能向前移动,不能回退或重置。

(3)迭代器的实现

在 Python 中,可以通过以下两种方式实现迭代器:

(3.1)自定义迭代器类:通过实现 __iter__()__next__() 方法,创建一个自定义迭代器类。

class MyIterator:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.data):
            raise StopIteration
        value = self.data[self.index]
        self.index += 1
        return value


# 使用自定义迭代器
my_iter = MyIterator([1, 2, 3])
for item in my_iter:
    print(item)
# 输出:
# 1
# 2
# 3

(3.2)生成器函数:使用 yield 关键字定义生成器函数,生成器函数会自动实现迭代器协议。

def my_generator(data):
    for item in data:
        yield item


# 使用生成器函数
gen = my_generator([1, 2, 3])
for item in gen:
    print(item)
# 输出:
# 1
# 2
# 3

(4)迭代器与设计模式

迭代器设计模式是一种行为设计模式,用于提供一种统一的方式来遍历集合中的元素,而无需暴露集合的内部结构。

  • 迭代器设计模式 是一种设计思想。
  • 迭代器 是这种设计思想的具体实现。

(5)迭代器与函数

迭代器本身不是函数,但可以通过函数(如生成器函数)来实现迭代器。

  • 生成器函数 是一种特殊的函数,使用 yield 关键字生成值。
  • 生成器对象 是生成器函数返回的迭代器。

(6)Python 中的内置迭代器

Python 中的许多内置类型(如列表、字典、集合等)都实现了迭代器协议,可以直接用于 for 循环。

# 列表迭代器
my_list = [1, 2, 3]
for item in my_list:
    print(item)

# 字典迭代器
my_dict = {"a": 1, "b": 2}
for key in my_dict:
    print(key, my_dict[key])