Python 进阶

高级 Python 教程。 它涵盖了集合,装饰器,生成器,多线程,日志记录等主题。

原视频地址:[Advanced Python](https://www.python-engineer.com/courses/advancedpython/)

01. 列表

列表(List)是一种有序且可变的容器数据类型。 与集合(Set)不同,列表允许重复的元素。 它方便保存数据序列并对其进行进一步迭代。 列表用方括号创建。

my_list = ["banana", "cherry", "apple"]

Python中基本的内置容器数据类型的比较:

  • 列表(List)是一个有序且可变的数据类型。 允许重复的成员。

  • 元组(Tuple)是有序且不可变的数据类型。 允许重复的成员。

  • 集合(Set)是无序和未索引的数据类型。 不允许重复的成员。

  • 字典(Dict)是无序,可变和可索引的数据类型。 没有重复的成员。

  • 字符串是Unicode代码的不可变序列。

创建列表

列表使用方括号创建,或者内置的 list 函数。

list_1 = ["banana", "cherry", "apple"]
print(list_1)

# 或者使用 list 函数创建空列表
list_2 = list()
print(list_2)

# 列表允许不同的数据类
list_3 = [5, True, "apple"]
print(list_3)

# 列表允许重复元素
list_4 = [0, 0, 1, 1]
print(list_4)
    ['banana', 'cherry', 'apple']
    []
    [5, True, 'apple']
    [0, 0, 1, 1]

访问元素

可以通过索引号访问列表项。 请注意,索引从0开始。

item = list_1[0]
print(item)

# 你也可以使用负索引,比如 -1 表示最后一个元素,
# -2 表示倒数第二个元素,以此类推
item = list_1[-1]
print(item)
		banana
    apple

修改元素

只需访问索引并分配一个新值即可。

# 列表创建之后可以被修改
list_1[2] = "lemon"
print(list_1)
		['banana', 'cherry', 'lemon']

有用的方法

查看Python文档以查看所有列表方法:https://docs.python.org/3/tutorial/datastructures.html

my_list = ["banana", "cherry", "apple"]

# len() : 获取列表的元素个数
print("Length:", len(my_list))

# append() : 添加一个元素到列表末尾
my_list.append("orange")

# insert() : 添加元素到特定位置
my_list.insert(1, "blueberry")
print(my_list)

# pop() : 移除并返回特定位置的元素,默认为最后一个
item = my_list.pop()
print("Popped item: ", item)

# remove() : 移除列表中的元素
my_list.remove("cherry") # 如果元素没有在列表中,则触发 Value error
print(my_list)

# clear() : 移除列表所有元素
my_list.clear()
print(my_list)

# reverse() : 翻转列表
my_list = ["banana", "cherry", "apple"]
my_list.reverse()
print('Reversed: ', my_list)

# sort() : 升序排列元素
my_list.sort()
print('Sorted: ', my_list)

# 使用 sorted() 得到一个新列表,原来的列表不受影响
# sorted() 对任何可迭代类型起作用,不只是列表
my_list = ["banana", "cherry", "apple"]
new_list = sorted(my_list)

# 创建具有重复元素的列表
list_with_zeros = [0] * 5
print(list_with_zeros)

# 列表拼接
list_concat = list_with_zeros + my_list
print(list_concat)

# 字符串转列表
string_to_list = list('Hello')
print(string_to_list)
		Length: 3
    ['banana', 'blueberry', 'cherry', 'apple', 'orange']
    Popped item:  orange
    ['banana', 'blueberry', 'apple']
    []
    Reversed:  ['apple', 'cherry', 'banana']
    Sorted:  ['apple', 'banana', 'cherry']
    [0, 0, 0, 0, 0]
    [0, 0, 0, 0, 0, 'banana', 'cherry', 'apple']
    ['H', 'e', 'l', 'l', 'o']

复制列表

复制引用(references)时要小心。

list_org = ["banana", "cherry", "apple"]

# 这只是将引用复制到列表中,要小心
list_copy = list_org

# 现在,修改复制的列表也会影响原来的列表
list_copy.append(True)
print(list_copy)
print(list_org)

# 使用 copy(), 或者 list(x) 来真正复制列表
# 切片(slicing)也可以复制:list_copy = list_org[:]
list_org = ["banana", "cherry", "apple"]

list_copy = list_org.copy()
# list_copy = list(list_org)
# list_copy = list_org[:]

# 现在,修改复制的列表不会影响原来的列表
list_copy.append(True)
print(list_copy)
print(list_org)
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple', True]
    ['banana', 'cherry', 'apple']

迭代

# 使用for循环迭代列表
for i in list_1:
    print(i)
		banana
    cherry
    lemon

检查元素是否存在

if "banana" in list_1:
    print("yes")
else:
    print("no")
    yes

切片

和字符串一样,使用冒号( :)访问列表的子部分。

# a[start:stop:step], 默认步长为 1
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = a[1:3] # 注意,最后一个索引不包括
print(b)
b = a[2:] # 直到最后
print(b)
b = a[:3] # 从第一个元素开始
print(b)
a[0:3] = [0] # 替换子部分,需要可迭代
print(a)
b = a[::2] # 从头到为每隔两个元素
print(b)
a = a[::-1] # 使用负步长翻转列表
print(a)
b = a[:] # 使用切片复制元素
print(b)
		[2, 3]
    [3, 4, 5, 6, 7, 8, 9, 10]
    [1, 2, 3]
    [0, 4, 5, 6, 7, 8, 9, 10]
    [0, 5, 7, 9]
    [10, 9, 8, 7, 6, 5, 4, 0]
    [10, 9, 8, 7, 6, 5, 4, 0]

列表推导

一种从现有列表创建新列表的简便快捷方法。

列表推导方括号内包含一个表达式,后跟for语句。

a = [1, 2, 3, 4, 5, 6, 7, 8]
b = [i * i for i in a] # 每个元素平方
print(b)
		[1, 4, 9, 16, 25, 36, 49, 64]

嵌套列表

a = [[1, 2], [3, 4]]
print(a)
print(a[0])
		[[1, 2], [3, 4]]
    [1, 2]

02. 元组

元组(Tuple)是对象的集合,它有序且不可变。 元组类似于列表,主要区别在于不可变性。 在Python中,元组用圆括号和逗号分隔的值书写。

my_tuple = ("Max", 28, "New York")

使用元组而不使用列表的原因

  • 通常用于属于同一目标的对象。

  • 将元组用于异构(不同)数据类型,将列表用于同类(相似)数据类型。

  • 由于元组是不可变的,因此通过元组进行迭代比使用列表进行迭代要快一些。

  • 具有不可变元素的元组可以用作字典的键。 使用列表做为键是不可能的。

  • 如果你有不变的数据,则将其实现为元组将确保其有写保护。

创建元组

用圆括号和逗号分隔的值创建元组,或使用内置的 tuple 函数。

tuple_1 = ("Max", 28, "New York")
tuple_2 = "Linda", 25, "Miami" # 括弧可选

# 特殊情况:只有一个元素的元组需要在在最后添加逗号,否则不会被识别为元组
tuple_3 = (25,)
print(tuple_1)
print(tuple_2)
print(tuple_3)

# 或者使用内置 tuple 函数将可迭代对象(list,dict,string)转变为元组
tuple_4 = tuple([1,2,3])
print(tuple_4)
    ('Max', 28, 'New York')
    ('Linda', 25, 'Miami')
    (25,)
    (1, 2, 3)

访问元素

可以通过引用索引号访问元组项。 请注意,索引从0开始。

item = tuple_1[0]
print(item)
# 你也可以使用负索引,比如 -1 表示最后一个元素,-2 表示倒数第二个元素,以此类推
item = tuple_1[-1]
print(item)
    Max
    New York

添加或者修改元素

不可能,会触发 TypeError 错误。

tuple_1[2] = "Boston"
---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-5-c391d8981369> in <module>
    ----> 1 tuple_1[2] = "Boston"

    TypeError: 'tuple' object does not support item assignment

删除元组

del tuple_2

迭代

# 使用 for 循环迭代元组
for i in tuple_1:
    print(i)
    Max
    28
    New York

检查元素是否存在

if "New York" in tuple_1:
    print("yes")
else:
    print("no")
    yes

有用的方法

my_tuple = ('a','p','p','l','e',)

# len() : 获取元组元素个数
print(len(my_tuple))

# count(x) : 返回与 x 相等的元素个数
print(my_tuple.count('p'))

# index(x) : 返回与 x 相等的第一个元素索引
print(my_tuple.index('l'))

# 重复
my_tuple = ('a', 'b') * 5
print(my_tuple)

# 拼接
my_tuple = (1,2,3) + (4,5,6)
print(my_tuple)

# 将列表转为元组,以及将元组转为列表
my_list = ['a', 'b', 'c', 'd']
list_to_tuple = tuple(my_list)
print(list_to_tuple)

tuple_to_list = list(list_to_tuple)
print(tuple_to_list)

# convert string to tuple
string_to_tuple = tuple('Hello')
print(string_to_tuple)
    5
    2
    3
    ('a', 'b', 'a', 'b', 'a', 'b', 'a', 'b', 'a', 'b')
    (1, 2, 3, 4, 5, 6)
    ('a', 'b', 'c', 'd')
    ['a', 'b', 'c', 'd']
    ('H', 'e', 'l', 'l', 'o')

切片

和字符串一样,使用冒号(:)访问列表的子部分。

# a[start:stop:step], 默认步长为 1
a = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
b = a[1:3] # 注意,最后一个索引不包括
print(b)
b = a[2:] # 知道最后
print(b)
b = a[:3] # 从最前头开始
print(b)
b = a[::2] # 从前往后没两个元素
print(b)
b = a[::-1] # 翻转元组
print(b)
    (2, 3)
    (3, 4, 5, 6, 7, 8, 9, 10)
    (1, 2, 3)
    (1, 3, 5, 7, 9)
    (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

元组解包

# 变量个数必需与元组元素个数相同
tuple_1 = ("Max", 28, "New York")
name, age, city = tuple_1
print(name)
print(age)
print(city)

# 提示: 使用 * 解包多个元素到列表
my_tuple = (0, 1, 2, 3, 4, 5)
item_first, *items_between, item_last = my_tuple
print(item_first)
print(items_between)
print(item_last)
    Max
    28
    New York
    0
    [1, 2, 3, 4]
    5

嵌套元组

a = ((0, 1), ('age', 'height'))
print(a)
print(a[0])
    ((0, 1), ('age', 'height'))
    (0, 1)

比较元组和列表

元组的不可变性使Python可以进行内部优化。 因此,在处理大数据时,元组可以更高效。

# 比较大小
import sys
my_list = [0, 1, 2, "hello", True]
my_tuple = (0, 1, 2, "hello", True)
print(sys.getsizeof(my_list), "bytes")
print(sys.getsizeof(my_tuple), "bytes")

# 比较列表和元组创建语句的执行时间
import timeit
print(timeit.timeit(stmt="[0, 1, 2, 3, 4, 5]", number=1000000))
print(timeit.timeit(stmt="(0, 1, 2, 3, 4, 5)", number=1000000))
    104 bytes
    88 bytes
    0.12474981700000853
    0.014836141000017733

03. 字典

字典是无序,可变和可索引的集合。 字典由键值对的集合组成。 每个键值对将键映射到其关联值。 字典用大括号书写。 每对键值均以冒号( : )分隔,并且各项之间以逗号分隔。

my_dict = {"name":"Max", "age":28, "city":"New York"}

创建字典

使用大括号或者内置的 dict 函数创建。

my_dict = {"name":"Max", "age":28, "city":"New York"}
print(my_dict)

# 或者使用字典构造器,注意:键不需要引号。
my_dict_2 = dict(name="Lisa", age=27, city="Boston")
print(my_dict_2)
    {'name': 'Max', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 27, 'city': 'Boston'}

访问元素

name_in_dict = my_dict["name"]
print(name_in_dict)

# 如果键没有找到,引发 KeyError 错误
# print(my_dict["lastname"])
    Max

添加或修改元素

只需添加或访问键并分配值即可。

# 添加新键
my_dict["email"] = "max@xyz.com"
print(my_dict)

# 覆盖已经存在的键
my_dict["email"] = "coolmax@xyz.com"
print(my_dict)
    {'name': 'Max', 'age': 28, 'city': 'New York', 'email': 'max@xyz.com'}
    {'name': 'Max', 'age': 28, 'city': 'New York', 'email': 'coolmax@xyz.com'}

删除元素

# 删除键值对
del my_dict["email"]

# pop 返回值并删除键值对
print("popped value:", my_dict.pop("age"))

# 返回并移除最后插入的价值对
# (在 Python 3.7 之前,移除任意键值对)
print("popped item:", my_dict.popitem())

print(my_dict)

# clear() : 移除所有键值对
# my_dict.clear()
    popped value: 28
    popped item: ('city', 'New York')
    {'name': 'Max'}

检查键

my_dict = {"name":"Max", "age":28, "city":"New York"}
# 使用 if .. in ..
if "name" in my_dict:
    print(my_dict["name"])

# 使用 try except
try:
    print(my_dict["firstname"])
except KeyError:
    print("No key found")
    Max
    No key found

遍历字典

# 遍历键
for key in my_dict:
    print(key, my_dict[key])

# 遍历键
for key in my_dict.keys():
    print(key)

# 遍历值
for value in my_dict.values():
    print(value)

# 遍历键和值
for key, value in my_dict.items():
    print(key, value)
    name Max
    age 28
    city New York
    name
    age
    city
    Max
    28
    New York
    name Max
    age 28
    city New York

复制字典

复制索引时请注意。

dict_org = {"name":"Max", "age":28, "city":"New York"}

# 这只复制字典的引用,需要小心
dict_copy = dict_org

# 修改复制字典也会影响原来的字典
dict_copy["name"] = "Lisa"
print(dict_copy)
print(dict_org)

# 使用 copy() 或者 dict(x) 来真正复制字典
dict_org = {"name":"Max", "age":28, "city":"New York"}

dict_copy = dict_org.copy()
# dict_copy = dict(dict_org)

# 现在修改复制字典不会影响原来的字典
dict_copy["name"] = "Lisa"
print(dict_copy)
print(dict_org)
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Lisa', 'age': 28, 'city': 'New York'}
    {'name': 'Max', 'age': 28, 'city': 'New York'}

合并两个字典

# 使用 update() 方法合两个字典
# 存在的键会被覆盖,新键会被添加
my_dict = {"name":"Max", "age":28, "email":"max@xyz.com"}
my_dict_2 = dict(name="Lisa", age=27, city="Boston")

my_dict.update(my_dict_2)
print(my_dict)
    {'name': 'Lisa', 'age': 27, 'email': 'max@xyz.com', 'city': 'Boston'}

可能的键类型

任何不可变的类型(例如字符串或数字)都可以用作键。 另外,如果元组仅包含不可变元素,则可以使用它作为键。

# 使用数字做键,但要小心
my_dict = {3: 9, 6: 36, 9:81}
# 不要将键误认为是列表的索引,例如,在这里无法使用 my_dict[0]
print(my_dict[3], my_dict[6], my_dict[9])

# 使用仅包含不可变元素(例如数字,字符串)的元组
my_tuple = (8, 7)
my_dict = {my_tuple: 15}

print(my_dict[my_tuple])
# print(my_dict[8, 7])

# 不能使用列表,因为列表是可变的,会抛出错误:
# my_list = [8, 7]
# my_dict = {my_list: 15}
    9 36 81
    15

嵌套字典

值也可以是容器类型(例如列表,元组,字典)。

my_dict_1 = {"name": "Max", "age": 28}
my_dict_2 = {"name": "Alex", "age": 25}
nested_dict = {"dictA": my_dict_1,
               "dictB": my_dict_2}
print(nested_dict)
    {'dictA': {'name': 'Max', 'age': 28}, 'dictB': {'name': 'Alex', 'age': 25}}

04. 集合

集合是无序的容器数据类型,它是无索引的,可变的并且没有重复的元素。 集合用大括号创建。

my_set = {"apple", "banana", "cherry"}

创建集合

使用花括号或内置的 set 函数。

my_set = {"apple", "banana", "cherry"}
print(my_set)

# 或者使用 set 函数从可迭代对象创建,比如列表,元组,字符串
my_set_2 = set(["one", "two", "three"])
my_set_2 = set(("one", "two", "three"))
print(my_set_2)

my_set_3 = set("aaabbbcccdddeeeeeffff")
print(my_set_3)

# 注意:一个空的元组不能使用 {} 创建,这个会识别为字典
# 使用 set() 进行创建
a = {}
print(type(a))
a = set()
print(type(a))
    {'banana', 'apple', 'cherry'}
    {'three', 'one', 'two'}
    {'b', 'c', 'd', 'e', 'f', 'a'}
    <class 'dict'>
    <class 'set'>

添加元素

my_set = set()

# 使用 add() 方法添加元素
my_set.add(42)
my_set.add(True)
my_set.add("Hello")

# 注意:顺序不重要,只会影响打印输出
print(my_set)

# 元素已经存在是没有影响
my_set.add(42)
print(my_set)
    {True, 42, 'Hello'}
    {True, 42, 'Hello'}

移除元素

# remove(x): 移除 x, 如果元素不存在则引发 KeyError 错误
my_set = {"apple", "banana", "cherry"}
my_set.remove("apple")
print(my_set)

# KeyError:
# my_set.remove("orange")

# discard(x): 移除 x, 如果元素不存在则什么也不做
my_set.discard("cherry")
my_set.discard("blueberry")
print(my_set)

# clear() : 移除所有元素
my_set.clear()
print(my_set)

# pop() : 移除并返回随机一个元素
a = {True, 2, False, "hi", "hello"}
print(a.pop())
print(a)
    {'banana', 'cherry'}
    {'banana'}
    set()
    False
    {True, 2, 'hi', 'hello'}

检查元素是否存在

my_set = {"apple", "banana", "cherry"}
if "apple" in my_set:
    print("yes")
    yes

迭代

# 使用 for 循环迭代集合
# 注意:顺序不重要
my_set = {"apple", "banana", "cherry"}
for i in my_set:
    print(i)
    banana
    apple
    cherry

并集和交集

odds = {1, 3, 5, 7, 9}
evens = {0, 2, 4, 6, 8}
primes = {2, 3, 5, 7}

# union() : 合并来自两个集合的元素,不重复
# 注意这不会改变两个集合
u = odds.union(evens)
print(u)

# intersection(): 选择在两个集合中都存在的元素
i = odds.intersection(evens)
print(i)

i = odds.intersection(primes)
print(i)

i = evens.intersection(primes)
print(i)
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    set()
    {3, 5, 7}
    {2}

集合的差

setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}

# difference() : 返回集合 setA 中不在集合 setB 中的元素的集合
diff_set = setA.difference(setB)
print(diff_set)

# A.difference(B) 与 B.difference(A) 不一样
diff_set = setB.difference(setA)
print(diff_set)

# symmetric_difference() : 返回集合 setA 和 setB 中不同时在两个集合中的元素的集合
diff_set = setA.symmetric_difference(setB)
print(diff_set)

# A.symmetric_difference(B) = B.symmetric_difference(A)
diff_set = setB.symmetric_difference(setA)
print(diff_set)
    {4, 5, 6, 7, 8, 9}
    {10, 11, 12}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}

更新集合

setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setB = {1, 2, 3, 10, 11, 12}

# update() : 通过添加其他集合的元素进行更新
setA.update(setB)
print(setA)

# intersection_update() : 通过保留共同的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.intersection_update(setB)
print(setA)

# difference_update() : 通过移除与其他集合中相同的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.difference_update(setB)
print(setA)

# symmetric_difference_update() : 通过保留只出现在一个集合而不出现在另一个集合中的元素进行更新
setA = {1, 2, 3, 4, 5, 6, 7, 8, 9}
setA.symmetric_difference_update(setB)
print(setA)

# 注意:所有的更新方法同时适用于其他可迭代对象作为参数,比如列表,元组
# setA.update([1, 2, 3, 4, 5, 6])
    {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
    {1, 2, 3}
    {4, 5, 6, 7, 8, 9}
    {4, 5, 6, 7, 8, 9, 10, 11, 12}

复制

set_org = {1, 2, 3, 4, 5}

# 只是引用的复制,需要注意
set_copy = set_org

# 修改复制集合也会影响原来的集合
set_copy.update([3, 4, 5, 6, 7])
print(set_copy)
print(set_org)

# 使用 copy() 真正复制集合
set_org = {1, 2, 3, 4, 5}
set_copy = set_org.copy()

# 现在修改复制集合不会影响原来的集合
set_copy.update([3, 4, 5, 6, 7])
print(set_copy)
print(set_org)
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5, 6, 7}
    {1, 2, 3, 4, 5}

子集,超集和不交集

setA = {1, 2, 3, 4, 5, 6}
setB = {1, 2, 3}
# issubset(setX): 如果 setX 包含集合,返回 True
print(setA.issubset(setB))
print(setB.issubset(setA)) # True

# issuperset(setX): 如果集合包含 setX,返回 True
print(setA.issuperset(setB)) # True
print(setB.issuperset(setA))

# isdisjoint(setX) : 如果两个集合交集为空,比如没有相同的元素,返回 True
setC = {7, 8, 9}
print(setA.isdisjoint(setB))
print(setA.isdisjoint(setC))
    False
    True
    True
    False
    False
    True

Frozenset

Frozenset 只是普通集和的不变版本。 尽管可以随时修改集合的元素,但 Frozenset 的元素在创建后保持不变。 创建方式:

my_frozenset = frozenset(iterable)
a = frozenset([0, 1, 2, 3, 4])

# 以下操作不允许:
# a.add(5)
# a.remove(1)
# a.discard(1)
# a.clear()

# 同时,更新方法也不允许:
# a.update([1,2,3])

# 其他集合操作可行
odds = frozenset({1, 3, 5, 7, 9})
evens = frozenset({0, 2, 4, 6, 8})
print(odds.union(evens))
print(odds.intersection(evens))
print(odds.difference(evens))
    frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
    frozenset()
    frozenset({1, 3, 5, 7, 9})

05. 字符串

字符串是字符序列。 Python中的字符串用双引号或单引号引起来。

my_string = 'Hello'

Python字符串是不可变的,这意味着它们在创建后就无法更改。

创建

# 使用单引号后者双引号
my_string = 'Hello'
my_string = "Hello"
my_string = "I' m  a 'Geek'"

# 转义反斜杠
my_string = 'I\' m  a "Geek"'
my_string = 'I\' m a \'Geek\''
print(my_string)

# 多行字符串使用三个引号
my_string = """Hello
World"""
print(my_string)

# 如果需要字符串在下一行继续,使用反斜杠
my_string = "Hello \
World"
print(my_string)
    I' m a 'Geek'
    Hello
    World
    Hello World

访问字符和子字符串

my_string = "Hello World"

# 使用索引获取字符
b = my_string[0]
print(b)

# 通过切片获取子字符串
b = my_string[1:3] # 注意,最后一个索引不包括
print(b)
b = my_string[:5] # 从第一个元素开始
print(b)
b = my_string[6:] # 直到最后
print(b)
b = my_string[::2] # 从头到为每隔两个元素
print(b)
b = my_string[::-1] # 使用负步长翻转列表
print(b)
    H
    el
    Hello
    World
    HloWrd
    dlroW olleH

连接两个或多个字符串

# 使用 + 拼接字符串
greeting = "Hello"
name = "Tom"
sentence = greeting + ' ' + name
print(sentence)
Hello Tom

迭代

# 使用for循环迭代列表
my_string = 'Hello'
for i in my_string:
    print(i)
    H
    e
    l
    l
    o

检查字符或子字符串是否存在

if "e" in "Hello":
    print("yes")
if "llo" in "Hello":
    print("yes")
    yes
    yes

有用的方法

my_string = "     Hello World "

# 去除空格
my_string = my_string.strip()
print(my_string)

# 字符的个数
print(len(my_string))

# 大小写
print(my_string.upper())
print(my_string.lower())

# startswith 和 endswith
print("hello".startswith("he"))
print("hello".endswith("llo"))

# 找到子字符串的第一个索引,没有则返回 -1
print("Hello".find("o"))

# 计算字符或者子字符串的个数
print("Hello".count("e"))

# 使用其他字符串代替子字符串(当且仅当子字符串存在时)
# 注意:原字符串保持不变
message = "Hello World"
new_message = message.replace("World", "Universe")
print(new_message)

# 将字符串切分为为列表
my_string = "how are you doing"
a = my_string.split() # default argument is " "
print(a)
my_string = "one,two,three"
a = my_string.split(",")
print(a)

# 将列表拼接为字符串
my_list = ['How', 'are', 'you', 'doing']
a = ' '.join(my_list) # 给出的字符串是分隔符,比如在每个元素之间添加 ' '
print(a)
    Hello World
    11
    HELLO WORLD
    hello world
    ['how', 'are', 'you', 'doing']
    ['one', 'two', 'three']
    True
    True
    4
    1
    Hello Universe
    How are you doing

格式化

新样式使用 format() 方法,旧样式使用 % 操作符。

# 使用大括号做占位符
a = "Hello {0} and {1}".format("Bob", "Tom")
print(a)

# 默认顺序时位置可以不写
a = "Hello {} and {}".format("Bob", "Tom")
print(a)

a = "The integer value is {}".format(2)
print(a)

# 一些数字的特殊格式化规则
a = "The float value is {0:.3f}".format(2.1234)
print(a)
a = "The float value is {0:e}".format(2.1234)
print(a)
a = "The binary value is {0:b}".format(2)
print(a)

# old style formatting by using % operator
# 旧的方式使用 % 操作符
print("Hello %s and %s" % ("Bob", "Tom")) # 多个参数时必需是元组
val =  3.14159265359
print("The decimal value is %d" % val)
print("The float value is %f" % val)
print("The float value is %.2f" % val)
    Hello Bob and Tom
    Hello Bob and Tom
    The integer value is 2
    The float value is 2.123
    The float value is 2.123400e+00
    The binary value is 10
    Hello Bob and Tom
    The decimal value is 10
    The float value is 10.123450
    The float value is 10.12

f-Strings

从 Python 3.6 起,可以直接在花括号内使用变量。

name = "Eric"
age = 25
a = f"Hello, {name}. You are {age}."
print(a)
pi = 3.14159
a = f"Pi is {pi:.3f}"
print(a)
# f-Strings 在运行时计算,可以允许表达式
a = f"The value is {2*60}"
print(a)
    Hello, Eric. You are 25.
    Pi is 3.142
    The value is 120

更多关于不变性和拼接

# 因为字符串不可变,所以使用 + 或者 += 拼接字符串总是生成新的字符串
# 因此,多个操作时更加耗时。使用 join 方法更快。
from timeit import default_timer as timer
my_list = ["a"] * 1000000

# bad
start = timer()
a = ""
for i in my_list:
    a += i
end = timer()
print("concatenate string with + : %.5f" % (end - start))

# good
start = timer()
a = "".join(my_list)
end = timer()
print("concatenate string with join(): %.5f" % (end - start))
    concat string with + : 0.34527
    concat string with join(): 0.01191
# a[start:stop:step], 默认步长为 1
a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
b = a[1:3] # 注意,最后一个索引不包括
print(b)
b = a[2:] # 直到最后
print(b)
b = a[:3] # 从第一个元素开始
print(b)
a[0:3] = [0] # 替换子部分,需要可迭代
print(a)
b = a[::2] # 从头到为每隔两个元素
print(b)
a = a[::-1] # 使用负步长翻转列表
print(a)
b = a[:] # 使用切片复制元素
print(b)

06. collections

Python 中的 collections 模块实现了专门的容器数据类型,提供了 Python 通用内置容器dict,list,set和tuple的替代方案。

包含以下工具:

  • namedtuple:用于创建具有命名字段的元组子类的工厂函数

  • OrderedDict:用于记住条目添加顺序的dict子类

  • Counter:用于计算可哈希对象的dict子类

  • defaultdict:调用工厂函数以提供缺失值的dict子类

  • deque: 列表式容器,支持两端都有快速追加和弹出

在Python 3中,还存在其他一些模块(ChainMap,UserDict,UserList,UserString)。 有关更多参考,请参见 https://docs.python.org/3/library/collections.html

Counter

计数器是一个将元素存储为字典键的容器,而它们的计数则存储为字典值。

from collections import Counter
a = "aaaaabbbbcccdde"
my_counter = Counter(a)
print(my_counter)

print(my_counter.items())
print(my_counter.keys())
print(my_counter.values())

my_list = [0, 1, 0, 1, 2, 1, 1, 3, 2, 3, 2, 4]
my_counter = Counter(my_list)
print(my_counter)

# 出现最多的元素
print(my_counter.most_common(1))

# 返回元素的迭代器,每个元素重复其计数次数
# 元素返回顺序任意
print(list(my_counter.elements()))
    Counter({'a': 5, 'b': 4, 'c': 3, 'd': 2, 'e': 1})
    dict_items([('a', 5), ('b', 4), ('c', 3), ('d', 2), ('e', 1)])
    dict_keys(['a', 'b', 'c', 'd', 'e'])
    dict_values([5, 4, 3, 2, 1])
    Counter({1: 4, 2: 3, 0: 2, 3: 2, 4: 1})
    [(1, 4)]
    [0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4]

namedtuple

namedtuple 是容易创建,轻量级的对象类型。 它们为元组中的每个位置分配含义,并允许使用更具可读性的带文档代码。 它们可以在使用常规元组的任何地方使用,并且它们增加了按名称而不是位置索引访问字段的能力。

from collections import namedtuple
# 创建一个namedtuple,其类名称为string,其字段为string
# 给定字符串中的字段必须用逗号或空格分隔
Point = namedtuple('Point','x, y')
pt = Point(1, -4)
print(pt)
print(pt._fields)
print(type(pt))
print(pt.x, pt.y)

Person = namedtuple('Person','name, age')
friend = Person(name='Tom', age=25)
print(friend.name, friend.age)
    Point(x=1, y=-4)
    ('x', 'y')
    <class '__main__.Point'>
    1 -4
    Tom 25

OrderedDict

OrderedDict 就像常规dict一样,但是它们记住条目插入的顺序。 在 OrderedDict 上进行迭代时,将按照条目的键首次添加的顺序返回项。 如果新条目覆盖了现有条目,则原始插入位置将保持不变。 既然内置dict类获得了记住插入顺序的能力(自python 3.7起),它们的重要性就变得不那么重要了。 但是仍然存在一些差异,例如 OrderedDict 被设计为擅长重新排序操作。

from collections import OrderedDict
ordinary_dict = {}
ordinary_dict['a'] = 1
ordinary_dict['b'] = 2
ordinary_dict['c'] = 3
ordinary_dict['d'] = 4
ordinary_dict['e'] = 5
# 在Python 3.7之前,这个可能是任意顺序
print(ordinary_dict)

ordered_dict = OrderedDict()
ordered_dict['a'] = 1
ordered_dict['b'] = 2
ordered_dict['c'] = 3
ordered_dict['d'] = 4
ordered_dict['e'] = 5
print(ordered_dict)
# 与普通dict具有相同的功能,但始终有序
for k, v in ordinary_dict.items():
    print(k, v)
    {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
    OrderedDict([('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)])
    a 1
    b 2
    c 3
    d 4
    e 5

defaultdict

defaultdict是一个与通常的dict容器相似的容器,但是唯一的区别是,如果尚未设置该键,则defaultdict将具有默认值。 如果不使用defaultdict,则你必须检查该键是否存在,如果不存在,则将其设置为所需的键。

from collections import defaultdict

# 初始化一个默认int值,即 0
d = defaultdict(int)
d['yellow'] = 1
d['blue'] = 2
print(d.items())
print(d['green'])

# 初始化一个默认列表值,即空列表
d = defaultdict(list)
s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 5)]
for k, v in s:
    d[k].append(v)

print(d.items())
print(d['green'])
    dict_items([('yellow', 1), ('blue', 2)])
    0
    dict_items([('yellow', [1, 3]), ('blue', [2, 4]), ('red', [5])])
    []

deque

deque是双端队列。 它可用于在两端添加或删除元素。 deque支持从队列的任一侧线程安全,内存高效地追加和弹出,在任一方向上大致相同的 O(1) 性能。 更常用的栈和队列是双端队列的退化形式,其中输入和输出限制为单端。

from collections import deque
d = deque()

# append() : 添加元素到右端
d.append('a')
d.append('b')
print(d)

# appendleft() : 添加元素到左端
d.appendleft('c')
print(d)

# pop() : 返回并删除右端元素
print(d.pop())
print(d)

# popleft() : 返回并删除左端元素
print(d.popleft())
print(d)

# clear() : 删除所有元素
d.clear()
print(d)

d = deque(['a', 'b', 'c', 'd'])

# 在右端或者左端扩展
d.extend(['e', 'f', 'g'])
d.extendleft(['h', 'i', 'j']) # 主语 'j' 现在在最左侧 
print(d)

# count(x) : 返回找到的元素个数
print(d.count('h'))

# 向右旋转1个位置
d.rotate(1)
print(d)

向左旋转2个位置
d.rotate(-2)
print(d)
    deque(['a', 'b'])
    deque(['c', 'a', 'b'])
    b
    deque(['c', 'a'])
    c
    deque(['a'])
    deque([])
    deque(['j', 'i', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g'])
    1
    deque(['g', 'j', 'i', 'h', 'a', 'b', 'c', 'd', 'e', 'f'])
    deque(['i', 'h', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'j'])

07. Itertools

Python itertools 模块是用于处理迭代器的工具集合。 简而言之,迭代器是可以在for循环中使用的数据类型。 Python中最常见的迭代器是列表。

有关所有可能的 itertools,请参见 https://docs.python.org/3/library/itertools.html

product()

该工具计算输入可迭代项的笛卡尔积。

它等效于嵌套的for循环。 例如,product(A, B)返 回的结果与 ((x,y) for x in A for y in B) 相同。

from itertools import product

prod = product([1, 2], [3, 4])
print(list(prod)) # 请注意,我们将迭代器转换为列表进行打印

# 为了允许可迭代对象自身做乘积,指定重复次数
prod = product([1, 2], [3], repeat=2)
print(list(prod)) # 请注意,我们将迭代器转换为列表进行打印
    [(1, 3), (1, 4), (2, 3), (2, 4)]
    [(1, 3, 1, 3), (1, 3, 2, 3), (2, 3, 1, 3), (2, 3, 2, 3)]

permutations()

此工具以所有可能的顺序,以可迭代的方式返回元素的连续长度排列,并且没有重复的元素。

from itertools import permutations

perm = permutations([1, 2, 3])
print(list(perm))

# 可选:排列元组的长度
perm = permutations([1, 2, 3], 2)
print(list(perm))
    [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]
    [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]

combinations() and combinations_with_replacement()

长度r的元组,按排序顺序。 因此,如果对输入的可迭代对象进行排序,则将按排序顺序生成组合元组。 combinations()不允许重复的元素,但 combinations_with_replacement() 允许。

from itertools import combinations, combinations_with_replacement

# 第二个参数是必需的,它指定输出元组的长度。
comb = combinations([1, 2, 3, 4], 2)
print(list(comb))

comb = combinations_with_replacement([1, 2, 3, 4], 2)
print(list(comb))
    [(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
    [(1, 1), (1, 2), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), (3, 3), (3, 4), (4, 4)]

accumulate()

使迭代器返回累加的总和或其他二进制函数的累加结果。

from itertools import accumulate

# 返回累积和
acc = accumulate([1,2,3,4])
print(list(acc))

# 其他可能的函数是可能的
import operator
acc = accumulate([1,2,3,4], func=operator.mul)
print(list(acc))

acc = accumulate([1,5,2,6,3,4], func=max)
print(list(acc))
    [1, 3, 6, 10]
    [1, 2, 6, 24]
    [1, 5, 5, 6, 6, 6]

groupby()

创建一个迭代器,从迭代器返回连续的键和组。 键是为每个元素计算键值的函数。 如果未指定或为None,则键默认为标识函数,并返回不变的元素。 通常,可迭代项需要已经在相同的键函数上进行了排序。

from itertools import groupby

# 使用函数作为键
def smaller_than_3(x):
    return x < 3

group_obj = groupby([1, 2, 3, 4], key=smaller_than_3)
for key, group in group_obj:
    print(key, list(group))

# 或者使用 lambda 表达式,比如:包含 'i' 的词
group_obj = groupby(["hi", "nice", "hello", "cool"], key=lambda x: "i" in x)
for key, group in group_obj:
    print(key, list(group))
    
persons = [{'name': 'Tim', 'age': 25}, {'name': 'Dan', 'age': 25}, 
           {'name': 'Lisa', 'age': 27}, {'name': 'Claire', 'age': 28}]

for key, group in groupby(persons, key=lambda x: x['age']):
    print(key, list(group))
    True [1, 2]
    False [3, 4]
    True ['hi', 'nice']
    False ['hello', 'cool']
    25 [{'name': 'Tim', 'age': 25}, {'name': 'Dan', 'age': 25}]
    27 [{'name': 'Lisa', 'age': 27}]
    28 [{'name': 'Claire', 'age': 28}]

无限迭代器:count(), cycle(), repeat()

from itertools import count, cycle, repeat
# count(x): 从 x 开始计数: x, x+1, x+2, x+3...
for i in count(10):
    print(i)
    if  i >= 13:
        break

# cycle(iterable) : 通过迭代无限循环
print("")
sum = 0
for i in cycle([1, 2, 3]):
    print(i)
    sum += i
    if sum >= 12:
        break

# repeat(x): 无限重复x或重复n次
print("")
for i in repeat("A", 3):
    print(i)
    10
    11
    12
    13

    1
    2
    3
    1
    2
    3

    A
    A
    A

08. Lambda 函数

Lambda函数是一个小的(一行)匿名函数,没有函数名称。 Lambda函数可以接受任意数量的参数,但只能具有一个表达式。 虽然使用def关键字定义了普通函数,但在Python中,使用lambda关键字定义了匿名函数。

lambda arguments: expression

当简单函数仅在代码中使用一次或短时间时,可以使用Lambda函数。 最常见的用途是作为高阶函数(将其他函数作为参数的函数)的参数。 它们还与诸如 map()filter()reduce()之类的内置函数一起使用。

# 一个给参数加10的lambda函数
f = lambda x: x+10
val1 = f(5)
val2 = f(100)
print(val1, val2)

# 一个返回两个参数乘积的lambda函数
f = lambda x,y: x*y
val3 = f(2,10)
val4 = f(7,5)
print(val3, val4)
    15 110
    20 35

使用示例:另一个函数内的Lambda函数

从另一个函数返回定制的lambda函数,并根据需要创建不同的函数变体。

def myfunc(n):
    return lambda x: x * n

doubler = myfunc(2)
print(doubler(6))

tripler = myfunc(3)
print(tripler(6))
    12
    18

使用lambda函数作为key参数的自定义排序

key函数会在排序之前转换每个元素。

points2D = [(1, 9), (4, 1), (5, -3), (10, 2)]
sorted_by_y = sorted(points2D, key= lambda x: x[1])
print(sorted_by_y)

mylist = [- 1, -4, -2, -3, 1, 2, 3, 4]
sorted_by_abs = sorted(mylist, key= lambda x: abs(x))
print(sorted_by_abs)
    [(5, -3), (4, 1), (10, 2), (1, 9)]
    [-1, 1, -2, 2, -3, 3, -4, 4]

在 map 函数中使用 Lambda 函数

map(func, seq) ,使用函数转换每个元素。

a  = [1, 2, 3, 4, 5, 6]
b = list(map(lambda x: x * 2 , a))

# 但是,尝试使用列表推导
# 如果你已经定义了函数,请使用 map
c = [x*2 for x in a]
print(b)
print(c)
    [2, 4, 6, 8, 10, 12]
    [2, 4, 6, 8, 10, 12]

在 filter 函数中使用 Lambda 函数

filter(func, seq) ,返回其 func 计算为 True 的所有元素。

a = [1, 2, 3, 4, 5, 6, 7, 8]
b = list(filter(lambda x: (x%2 == 0) , a))

# 同样可以使用列表推导实现
c = [x for x in a if x%2 == 0]
print(b)
print(c)
    [2, 4, 6, 8]
    [2, 4, 6, 8]

reduce

reduce(func, seq) ,重复将 func 应用于元素并返回单个值。func 需要2个参数。

from functools import reduce
a = [1, 2, 3, 4]
product_a = reduce(lambda x, y: x*y, a)
print(product_a)
sum_a = reduce(lambda x, y: x+y, a)
print(sum_a)
    24
    10

09. 异常和错误

Python程序在遇到错误后立即终止。在Python中,错误可以是语法错误或异常。 在本文中,我们将关注以下内容:

  • 语法错误与异常

  • 如何抛出异常

  • 如何处理异常

  • 常见的内置异常

  • 如何定义自己的异常

语法错误

当解析器检测到语法不正确的语句时发生语法错误。 语法错误可以是例如拼写错误,缺少括号,没有新行(请参见下面的代码)或错误的标识(这实际上会引发它自己的IndentationError,但它是SyntaxError的子类)。

a = 5 print(a)
    File "<ipython-input-5-fed4b61d14cd>", line 1
    a = 5 print(a)
                  ^
    SyntaxError: invalid syntax

异常

即使一条语句在语法上是正确的,执行该语句也可能导致错误,这称为 异常错误。 有几种不同的错误类别,例如,尝试对数字和字符串求和将引发 TypeError

a = 5 + '10'
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-6-893398416ed7> in <module>
    ----> 1 a = 5 + '10'

    TypeError: unsupported operand type(s) for +: 'int' and 'str'

抛出异常

如果要在满足特定条件时强制发生异常,则可以使用 raise 关键字。

x = -5
if x < 0:
    raise Exception('x should not be negative.')
    ---------------------------------------------------------------------------
    Exception                                 Traceback (most recent call last)
    <ipython-input-4-2a9e7e673803> in <module>
          1 x = -5
          2 if x < 0:
    ----> 3     raise Exception('x should not be negative.')

    Exception: x should not be negative.

你还可以使用 assert 语句,如果你的断言不是 True,则将引发 AssertionError。 这样,你可以主动测试必须满足的某些条件,而不必等待程序中途崩溃。 断言还用于单元测试

x = -5
assert (x >= 0), 'x is not positive.'
# --> 如果 x >= 0,代码将正常运行
    ---------------------------------------------------------------------------
    AssertionError                            Traceback (most recent call last)
    <ipython-input-7-f9b059c51e45> in <module>
          1 x = -5
    ----> 2 assert (x >= 0), 'x is not positive.'
          3 # --> Your code will be fine if x >= 0
    AssertionError: x is not positive.

处理异常

你可以使用 tryexcept 块来捕获和处理异常。 如果你可以捕获异常,则你的程序将不会终止,并且可以继续。

# 这将捕获所有可能的异常
try:
    a = 5 / 0
except:
    print('some error occured.')
    
# 可以捕获异常类型
try:
    a = 5 / 0
except Exception as e:
    print(e)
    
# 最好指定要捕获的异常类型
# 因此,你必须知道可能的错误
try:
    a = 5 / 0
except ZeroDivisionError:
    print('Only a ZeroDivisionError is handled here')
    
# 你可以在try块中运行多个语句,并捕获不同的可能的异常
try:
    a = 5 / 1 # 注意:这里没有 ZeroDivisionError
    b = a + '10'
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
except TypeError as e:
    print('A TypeError occured:', e)
    Some error occured.
    Division by zero
    Only a ZeroDivisionError is handled here
    A TypeError occured: unsupported operand type(s) for +: 'float' and 'str'

else 语句

如果没有发生异常,则可以使用else语句运行。

try:
    a = 5 / 1
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
else:
    print('Everything is ok')
    Everything is ok

finally 语句

你可以使用始终运行的 finally 语句,无论是否存在异常。 例如,这可用于进行一些清理操作。

try:
    a = 5 / 1 # 注意:这里没有 ZeroDivisionError
    b = a + '10'
except ZeroDivisionError as e:
    print('A ZeroDivisionError occured:', e)
except TypeError as e:
    print('A TypeError occured:', e)
else:
    print('Everything is ok')
finally:
    print('Cleaning up some stuff...')
    A TypeError occured: unsupported operand type(s) for +: 'float' and 'str'
    Cleaning up some stuff...

常见的内置异常

你可以在此处找到所有内置的异常:https://docs.python.org/3/library/exceptions.html

  • ImportError:如果无法导入模块

  • NameError:如果你尝试使用未定义的变量

  • FileNotFoundError:如果你尝试打开一个不存在的文件或指定了错误的路径

  • ValueError:当某个操作或函数收到类型正确但值不正确的参数时,例如尝试从不存在的列表中删除值

  • TypeError:将操作或函数应用于不适当类型的对象时引发。

  • IndexError:如果你尝试访问序列的无效索引,例如列表或元组。

  • KeyError:如果你尝试访问字典中不存在的键。

# ImportError
import nonexistingmodule

# NameError
a = someundefinedvariable

# FileNotFoundError
with open('nonexistingfile.txt') as f:
    read_data = f.read()

# ValueError
a = [0, 1, 2]
a.remove(3)

# TypeError
a = 5 + "10"

# IndexError
a = [0, 1, 2]
value = a[5]

# KeyError
my_dict = {"name": "Max", "city": "Boston"}
age = my_dict["age"]

如何定义自己的异常

你可以定义自己的异常类,该异常类应从内置的 Exception 类派生。 与标准异常的命名类似,大多数异常都以“错误”结尾的名称定义。 可以像定义其他任何类一样定义异常类,但是它们通常保持简单,通常仅提供一定数量的属性,这些属性允许处理程序提取有关错误的信息。

# 自定义异常类的最小示例
class ValueTooHighError(Exception):
    pass

# 或者为处理者添加一些信息
class ValueTooLowError(Exception):
    def __init__(self, message, value):
        self.message = message
        self.value = value

def test_value(a):
    if a > 1000:
        raise ValueTooHighError('Value is too high.')
    if a < 5:
        raise ValueTooLowError('Value is too low.', a) # 注意,构造器接受两个参数
    return a

try:
    test_value(1)
except ValueTooHighError as e:
    print(e)
except ValueTooLowError as e:
    print(e.message, 'The value is:', e.value)
    Value is too low. The value is: 1

10. 日志记录

Python中的日志记录模块是功能强大的内置模块,因此你可以快速将日志记录添加到应用程序中。

import logging

日志级别

有5种不同的日志级别指示事件的严重程度。 默认情况下,系统仅记录 警告(WARNING) 级别及更高级别的事件。

import logging
logging.debug('This is a debug message')
logging.info('This is an info message')
logging.warning('This is a warning message')
logging.error('This is an error message')
logging.critical('This is a critical message')
    WARNING:root:This is a warning message
    ERROR:root:This is an error message
    CRITICAL:root:This is a critical message

配置

使用 basicConfig(**kwargs),你可以自定义根记录器。 最常见的参数是 levelformatfilename。查看全部可能的参数:https://docs.python.org/3/library/logging.html#logging.basicConfig。查看可能的 format :https://docs.python.org/3/library/logging.html#logrecord-attributes。查看如何设置时间字符串:https://docs.python.org/3/library/time.html#time.strftime。请注意,此函数仅应调用一次,通常在导入模块后首先调用。 如果根记录器已经配置了处理程序,则该设置无效。 例如,在 basicConfig 之前调用 logging.info(...) 将提前设置处理程序。

import logging
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%m/%d/%Y %H:%M:%S')
# 现在,调试消息也将以其他格式记录。
logging.debug('Debug message')

# 这将记录到文件而不是控制台。
# logging.basicConfig(level=logging.DEBUG, filename='app.log')

模块内记录和记录器层次结构

在具有多个模块的应用程序中,最佳实践是使用 __name__ 全局变量创建内部记录器。 这将使用你的模块名称创建一个记录器,并确保没有名称冲突。 日志记录模块创建记录器的层次结构,从根记录器开始,然后将新的记录器添加到该层次结构中。 如果随后将模块导入另一个模块,则可以通过记录器名称将日志消息与正确的模块关联。 请注意,更改根记录器的 basicConfig 还将影响层次结构中其他(下部)记录器的日志事件。

# helper.py
# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 当运行 main.py 时的输出
# helper - INFO - HELLO

传播

默认情况下,除了附加到创建的记录器的任何处理程序外,所有创建的记录器还将日志事件传递给高级记录器的处理程序。 你可以通过设置 propagate = False 来禁用此功能。 有时,当你想知道为什么看不到来自另一个模块的日志消息时,则可能是此属性。

# -------------------------------------
import logging
logger = logging.getLogger(__name__)
logger.propagate = False
logger.info('HELLO')

# main.py
# -------------------------------------
import logging
logging.basicConfig(level=logging.INFO, format='%(name)s - %(levelname)s - %(message)s')
import helper

# --> 运行main.py时无输出,因为 helper 模块记录器不会将其消息传播到根记录器

日志处理程序

处理程序对象负责将适当的日志消息调度到处理程序的特定目标。 例如,你可以使用不同的处理程序通过HTTP或通过电子邮件将消息发送到标准输出流,文件。 通常,你为每个处理程序配置一个级别( setLevel() ),一个格式化程序( setFormatter())和一个可选的过滤器( addFilter() )。 有关可能的内置处理程序,请参见 https://docs.python.org/3/howto/logging.html#useful-handlers。 当然,你也可以通过派生这些类来实现自己的处理程序。

import logging

logger = logging.getLogger(__name__)

# 创建处理器
stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler('file.log')

# 配置级别和格式化程序,并添加到处理器上
stream_handler.setLevel(logging.WARNING) # 警告及以上级别日志记录到流中
file_handler.setLevel(logging.ERROR) # 错误及以上级别记录到文件中

stream_format = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
file_format = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stream_handler.setFormatter(stream_format)
file_handler.setFormatter(file_format)

# 添加处理器到日志记录器上
logger.addHandler(stream_handler)
logger.addHandler(file_handler)

logger.warning('This is a warning') # 记录到流中
logger.error('This is an error') # 记录到流和文件中

过滤器例子

class InfoFilter(logging.Filter):
    
    # 覆盖此方法。 仅此方评估为True的日志记录将通过过滤器。
    def filter(self, record):
        return record.levelno == logging.INFO

# 现在只有 INFO 级别的消息会被记录。
stream_handler.addFilter(InfoFilter())
logger.addHandler(stream_handler)

其他配置方法

我们已经看到了如何配置日志,从而在代码中显式地创建日志记录器,处理程序和格式化程序。 还有其他两种配置方法:

.conf文件

创建一个 .conf(或有时存储为 .ini)文件,定义记录器,处理程序和格式化程序,并提供名称作为键。 定义其名称后,可以通过在其名称之间用下划线分隔之前添加单词 loggerhandlerformatter 进行配置。 然后,你可以为每个记录器,处理程序和格式化程序设置属性。 在下面的示例中,将使用 StreamHandler 配置根记录器和名为 simpleExample 的记录器。

# logging.conf
[loggers]
keys=root,simpleExample

[handlers]
keys=consoleHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=consoleHandler

[logger_simpleExample]
level=DEBUG
handlers=consoleHandler
qualname=simpleExample
propagate=0

[handler_consoleHandler]
class=StreamHandler
level=DEBUG
formatter=simpleFormatter
args=(sys.stdout,)

[formatter_simpleFormatter]
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
# 在代码中使用配置文件
import logging
import logging.config

logging.config.fileConfig('logging.conf')

# 使用配置文件中的名称创建记录器。
# 该记录器现在具有带有 DEBUG 级别和指定格式的 StreamHandler
logger = logging.getLogger('simpleExample')

logger.debug('debug message')
logger.info('info message')

捕获堆栈跟踪

将跟踪记录记录在异常日志中对于解决问题非常有用。 你可以通过将 excinfo 参数设置为True来捕获 logging.error() 中的回溯。

import logging

try:
    a = [1, 2, 3]
    value = a[3]
except IndexError as e:
    logging.error(e)
    logging.error(e, exc_info=True)
    ERROR:root:list index out of range
    ERROR:root:list index out of range
    Traceback (most recent call last):
      File "<ipython-input-6-df97a133cbe6>", line 5, in <module>
        value = a[3]
    IndexError: list index out of range

如果未捕获正确的 Exception,则还可以使用 traceback.formatexc() 方法记录该异常。

滚动 FileHandler

当你有一个大型应用程序将许多事件记录到一个文件中,而你只需要跟踪最近的事件时,请使用RotatingFileHandler来使文件保持较小。 当日志达到一定数量的字节时,它将被“滚动”。 你还可以保留多个备份日志文件,然后再覆盖它们。

import logging
from logging.handlers import RotatingFileHandler

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 2KB后滚动,并保留备份日志为 app.log.1, app.log.2 等.
handler = RotatingFileHandler('app.log', maxBytes=2000, backupCount=5)
logger.addHandler(handler)

for _ in range(10000):
    logger.info('Hello, world!')

TimedRotatingFileHandler

如果你的应用程序将长时间运行,则可以使用 TimedRotatingFileHandler。 这将根据经过的时间创建一个轮换日志。 when 参数的可能时间条件是:

  • second (s)

  • minute (m)

  • hour (h)

  • day (d)

  • w0-w6 (工作日, 0=星期一)

  • midnight

import logging
import time
from logging.handlers import TimedRotatingFileHandler
 
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

# 这将每分钟创建一个新的日志文件,并在覆盖旧日志之前创建一个带有时间戳的5个备份文件。
handler = TimedRotatingFileHandler('timed_test.log', when='m', interval=1, backupCount=5)
logger.addHandler(handler)
 
for i in range(6):
    logger.info('Hello, world!')
    time.sleep(50)

以JSON格式登录

如果你的应用程序从不同的模块(特别是在微服务体系结构中)生成许多日志,那么定位重要的日志以进行分析可能会很困难。 因此,最佳实践是以JSON格式记录你的消息,并将其发送到集中式日志管理系统。 然后,你可以轻松地搜索,可视化和分析日志记录。

我建议使用此开源JSON记录器:https://github.com/madzak/python-json-logger

pip install python-json-logger
import logging
from pythonjsonlogger import jsonlogger

logger = logging.getLogger()

logHandler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter()
logHandler.setFormatter(formatter)
logger.addHandler(logHandler)

11. JSON

JSON(JavaScript对象表示法)是一种轻量级数据格式,用于数据交换。 在Python中具有用于编码和解码JSON数据的内置 json 模块。 只需导入它,就可以使用JSON数据了:

import json

JSON的一些优点:

  • JSON作为“字节序列”存在,在我们需要通过网络传输(流)数据的情况下非常有用。

  • 与XML相比,JSON小得多,可转化为更快的数据传输和更好的体验。

  • JSON非常文本友好,因为它是文本形式的,并且同时也是机器友好的。

JSON格式

{
    "firstName": "Jane",
    "lastName": "Doe",
    "hobbies": ["running", "swimming", "singing"],
    "age": 28,
    "children": [
        {
            "firstName": "Alex",
            "age": 5
        },
        {
            "firstName": "Bob",
            "age": 7
        }
    ]
}

JSON支持基本类型(字符串,数字,布尔值)以及嵌套的数组和对象。 根据以下转换,将简单的Python对象转换为JSON:

|Python|JSON| |—|—| |dict|object| |”list, tuple”|array| |str|string| |”int, long, float”|number| |True|true| |False|false| |None|null|

从Python到JSON(序列化,编码)

使用 json.dumps() 方法将Python对象转换为JSON字符串。

import json

person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]}

# 转为 JSON:
person_json = json.dumps(person)
# 使用不用的格式
person_json2 = json.dumps(person, indent=4, separators=("; ", "= "), sort_keys=True)

# 结果为 JSON 字符串
print(person_json) 
print(person_json2)
    {"name": "John", "age": 30, "city": "New York", "hasChildren": false, "titles":["engineer", "programmer"]}
    {
        "age"= 30; 
        "city"= "New York"; 
        "hasChildren"= false; 
        "name"= "John"; 
        "titles"= [
            "engineer"; 
            "programmer"
        ]
    }

或将Python对象转换为JSON对象,然后使用 json.dump() 方法将其保存到文件中。

import json

person = {"name": "John", "age": 30, "city": "New York", "hasChildren": False, "titles": ["engineer", "programmer"]}

with open('person.json', 'w') as f:
    json.dump(person, f) # 你也可以设置缩进等

从JSON到Python(反序列化,解码)

使用 json.loads() 方法将JSON字符串转换为Python对象。 结果将是一个Python字典。

import json
person_json = """
{
    "age": 30, 
    "city": "New York",
    "hasChildren": false, 
    "name": "John",
    "titles": [
        "engineer",
        "programmer"
    ]
}
"""
person = json.loads(person_json)
print(person)
    {'age': 30, 'city': 'New York', 'hasChildren': False, 'name': 'John', 'titles': ['engineer', 'programmer']}

或从文件加载数据,然后使用 json.load()方法将其转换为Python对象。

import json

with open('person.json', 'r') as f:
    person = json.load(f)
    print(person)
    {'name': 'John', 'age': 30, 'city': 'New York', 'hasChildren': False, 'titles': ['engineer', 'programmer']}

使用自定义对象

编码

使用默认的 JSONEncoder 编码自定义对象将引发 TypeError。 我们可以指定一个自定义的编码函数,该函数将类名和所有对象变量存储在字典中。 将此函数用作 json.dump() 方法中的 default 参数。

import json
def encode_complex(z):
    if isinstance(z, complex):
        # 只是类名的键很重要,值可以是任意的。
        return {z.__class__.__name__: True, "real":z.real, "imag":z.imag}
    else:
        raise TypeError(f"Object of type '{z.__class__.__name__}' is not JSON serializable")

z = 5 + 9j
zJSON = json.dumps(z, default=encode_complex)
print(zJSON)
    {"complex": true, "real": 5.0, "imag": 9.0}

你还可以创建一个自定义的 Encoder 类,并覆盖 default() 方法。 将其用于 json.dump() 方法中的 cls 参数,或直接使用编码器。

from json import JSONEncoder
class ComplexEncoder(JSONEncoder):
    
    def default(self, o):
        if isinstance(z, complex):
            return {z.__class__.__name__: True, "real":z.real, "imag":z.imag}
        # 让基类的默认方法处理其他对象或引发TypeError
        return JSONEncoder.default(self, o)
    
z = 5 + 9j
zJSON = json.dumps(z, cls=ComplexEncoder)
print(zJSON)
# 或者直接使用编码器
zJson = ComplexEncoder().encode(z)
print(zJSON)
    {"complex": true, "real": 5.0, "imag": 9.0}
    {"complex": true, "real": 5.0, "imag": 9.0}

解码

可以使用默认 JSONDecoder 解码自定义对象,但是它将被解码为字典。 编写一个自定义解码函数,该函数将以字典作为输入,并在可以在字典中找到对象类名称的情况下创建自定义对象。 将此函数用于 json.load() 方法中的 object_hook 参数。

# 可能但解码为字典
z = json.loads(zJSON)
print(type(z))
print(z)

def decode_complex(dct):
    if complex.__name__ in dct:
        return complex(dct["real"], dct["imag"])
    return dct

# 现在,对象在解码后的类型为complex
z = json.loads(zJSON, object_hook=decode_complex)
print(type(z))
print(z)
    <class 'dict'>
    {'complex': True, 'real': 5.0, 'imag': 9.0}
    <class 'complex'>
    (5+9j)

模板编码和解码函数

如果在 __init__ 方法中提供了所有类变量,则此方法适用于所有自定义类。

class User:
		# 自定义类在 __init__() 中包含所有类变量
    def __init__(self, name, age, active, balance, friends):
        self.name = name
        self.age = age
        self.active = active
        self.balance = balance
        self.friends = friends
        
class Player:
    # 其他自定义类
    def __init__(self, name, nickname, level):
        self.name = name
        self.nickname = nickname
        self.level = level
          
            
def encode_obj(obj):
    """
    接受一个自定义对象,并返回该对象的字典表示形式。 此字典表示形式还包括对象的模块和类名称。
    """
  
		# 用对象元数据填充字典
    obj_dict = {
      "__class__": obj.__class__.__name__,
      "__module__": obj.__module__
    }
  
    # 用对象属性填充字典
    obj_dict.update(obj.__dict__)
  
    return obj_dict

def decode_dct(dct):
    """
    接受字典并返回与该字典关联的自定义对象。
    它利用字典中的 "__module__" 和 "__class__" 元数据来了解要创建的对象类型。
    """
    if "__class__" in dct:
        # Pop ensures we remove metadata from the dict to leave only the instance arguments
        class_name = dct.pop("__class__")
        
        # Get the module name from the dict and import it
        module_name = dct.pop("__module__")
        
        # We use the built in __import__ function since the module name is not yet known at runtime
        module = __import__(module_name)
        
        # Get the class from the module
        class_ = getattr(module,class_name)

        # Use dictionary unpacking to initialize the object
        # Note: This only works if all __init__() arguments of the class are exactly the dict keys
        obj = class_(**dct)
    else:
        obj = dct
    return obj

# User 类适用于我们的编码和解码方法
user = User(name = "John",age = 28, friends = ["Jane", "Tom"], balance = 20.70, active = True)

userJSON = json.dumps(user,default=encode_obj,indent=4, sort_keys=True)
print(userJSON)

user_decoded = json.loads(userJSON, object_hook=decode_dct)
print(type(user_decoded))

# Player 类也适用于我们的编码和解码方法
player = Player('Max', 'max1234', 5)
playerJSON = json.dumps(player,default=encode_obj,indent=4, sort_keys=True)
print(playerJSON)

player_decoded = json.loads(playerJSON, object_hook=decode_dct)
print(type(player_decoded))
    {
        "__class__": "User",
        "__module__": "__main__",
        "active": true,
        "age": 28,
        "balance": 20.7,
        "friends": [
            "Jane",
            "Tom"
        ],
        "name": "John"
    }
    <class '__main__.User'>
    {
        "__class__": "Player",
        "__module__": "__main__",
        "level": 5,
        "name": "Max",
        "nickname": "max1234"
    }
    <class '__main__.Player'>

12. 随机数

Python定义了一组用于生成或操作随机数的函数。 本文介绍:

  • random 模块

  • random.seed() 再生产数字

  • 使用 secrets 模块创建密码学上强的随机数

  • numpy.random 创建随机 nd 数组

random 模块

该模块为各种版本实现伪随机数生成器。它使用Mersenne Twister算法(https://en.wikipedia.org/wiki/Mersenne_Twister)作为其核心生成器。 之所以称其为伪随机数,是因为数字看起来是随机的,但是是可重现的。

import random

# [0,1) 之间随机浮点数
a = random.random()
print(a)

# [a,b] 之间随机浮点数
a = random.uniform(1,10)
print(a)

# [a,b] 之间随机整数,b 包括。
a = random.randint(1,10)
print(a)

# 之间随机整数,b 不包括。
a = random.randrange(1,10)
print(a)

# 参数为 mu 和 sigma 的正态分布随机浮点数
a = random.normalvariate(0, 1)
print(a)

# 从序列中随机选择元素
a = random.choice(list("ABCDEFGHI"))
print(a)

# 从序列中随机选择 k 个唯一元素
a = random.sample(list("ABCDEFGHI"), 3)
print(a)

# 选择可重复的k个元素,并返回大小为k的列表
a = random.choices(list("ABCDEFGHI"),k=3)
print(a)

# 原地随机排列
a = list("ABCDEFGHI")
random.shuffle(a)
print(a)
    0.10426373452067317
    3.34983979352444
    3
    4
    -1.004568769635799
    E
    ['G', 'C', 'B']
    ['E', 'D', 'E']
    ['D', 'I', 'G', 'H', 'E', 'B', 'C', 'F', 'A']

种子生成器

使用 random.seed(),可以使结果可重复,并且 random.seed() 之后的调用链将产生相同的数据轨迹。 随机数序列变得确定,或完全由种子值确定。

print('Seeding with 1...\n')

random.seed(1)
print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 42...\n')
random.seed(42)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 1...\n')
random.seed(1)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))

print('\nRe-seeding with 42...\n')
random.seed(42)  # 重设随机种子

print(random.random())
print(random.uniform(1,10))
print(random.choice(list("ABCDEFGHI")))
    Seeding with 1...

    0.13436424411240122
    8.626903632435095
    B

    Re-seeding with 42...

    0.6394267984578837
    1.2250967970040025
    E

    Re-seeding with 1...

    0.13436424411240122
    8.626903632435095
    B

    Re-seeding with 42...

    0.6394267984578837
    1.2250967970040025
    E

secrets 模块

secrets 模块用于生成适合于管理数据(例如密码,帐户身份验证,安全令牌和相关机密)的密码学上强的随机数。

特别是,应优先使用secrets 而不是 random 模块中默认的伪随机数生成器,后者是为建模和仿真而设计的,而不是安全或加密技术。

import secrets

# [0, n) 之间的随机整数。
a = secrets.randbelow(10)
print(a)

# 返回具有k个随机位的整数。
a = secrets.randbits(5)
print(a)

# 从序列中选择一个随机元素
a = secrets.choice(list("ABCDEFGHI"))
print(a)
    6
    6
    E

NumPy的随机数

为多维数组创建随机数。NumPy伪随机数生成器与Python标准库伪随机数生成器不同。

重要的是,设置Python伪随机数生成器种子不会影响NumPy伪随机数生成器,必须单独设置和使用。

import numpy as np

np.random.seed(1)
# rand(d0,d1,…,dn)
# 生成随机浮点数的多维数组, 数组大小为 (d0,d1,…,dn)
print(np.random.rand(3))
# 重设随机种子
np.random.seed(1)
print(np.random.rand(3))

# 生成 [a,b) 之间随机整数的多维数组,大小为 n
values = np.random.randint(0, 10, (5,3))
print(values)

# 使用正态分布值生成多维数组,数组大小为 (d0,d1,…,dn)
# 来自标准正态分布的平均值为0.0且标准偏差为1.0的值
values = np.random.randn(5)
print(values)

# 随机排列一个多维数组.
# 仅沿多维数组的第一轴随机排列数组
arr = np.array([[1,2,3], [4,5,6], [7,8,9]])
np.random.shuffle(arr)
print(arr)
    [4.17022005e-01 7.20324493e-01 1.14374817e-04]
    [4.17022005e-01 7.20324493e-01 1.14374817e-04]
    [[5 0 0]
     [1 7 6]
     [9 2 4]
     [5 2 4]
     [2 4 7]]
    [-2.29230928 -1.41555249  0.8858294   0.63190187  0.04026035]
    [[4 5 6]
     [7 8 9]
     [1 2 3]]

13. 装饰器

装饰器是一个函数,它接受另一个函数并扩展该函数的行为而无需显式修改它。 这是一个非常强大的工具,可以将新功能添加到现有函数中。

装饰器有2种:

  • 函数装饰器

  • 类装饰器

函数用 @ 符号修饰:

@my_decorator
def my_function():
    pass

函数装饰器

为了理解装饰器模式,我们必须了解Python中的函数是一级对象,这意味着像其他任何对象一样,它们可以在另一个函数内定义,作为参数传递给另一个函数或从其他函数返回 。 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。 结果,修饰的函数便具有了扩展的功能!

# 装饰器是一个将另一个函数作为参数的函数,将其行为包装在内部函数中,并返回包装的函数。
def start_end_decorator(func):
    
    def wrapper():
        print('Start')
        func()
        print('End')
    return wrapper

def print_name():
    print('Alex')
    
print_name()

print()

# 现在通过将其作为参数传递给装饰器函数并将其赋值给自身来包装该函数->我们的函数已扩展了行为!
print_name = start_end_decorator(print_name)
print_name()
    Alex

    Start
    Alex
    End

装饰器语法

除了包装函数并将其分配给自身之外,我们还可以通过用 @ 装饰函数来实现相同的目的。

@start_end_decorator
def print_name():
    print('Alex')
    
print_name()
    Start
    Alex
    End

关于函数参数

如果我们的函数具有输入参数,并且我们尝试使用上面的装饰器将其包装,则它将引发 TypeError,因为我们在包装器内调用函数时也必须使用此参数。 但是,我们可以通过在内部函数中使用 *args**kwargs 来解决此问题:

def start_end_decorator_2(func):
    
    def wrapper(*args, **kwargs):
        print('Start')
        func(*args, **kwargs)
        print('End')
    return wrapper

@start_end_decorator_2
def add_5(x):
    return x + 5

result = add_5(10)
print(result)
    Start
    End
    None

返回值

请注意,在上面的示例中,我们没有取回结果,因此,下一步,我们还必须从内部函数返回值:

def start_end_decorator_3(func):
    
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result
    return wrapper

@start_end_decorator_3
def add_5(x):
    return x + 5

result = add_5(10)
print(result)
    Start
    End
    15

函数标识又如何变化呢?

如果我们看一下装饰函数的名称,并使用内置的 help 函数对其进行检查,我们会注意到Python认为我们的函数现在是装饰器函数的包装内部函数。

print(add_5.__name__)
help(add_5)
    wrapper
    Help on function wrapper in module __main__:

    wrapper(*args, **kwargs)

要解决此问题,请使用 functools.wraps 装饰器,该装饰器将保留有关原始函数的信息。 这有助于进行自省,即对象在运行时了解其自身属性的能力:

import functools
def start_end_decorator_4(func):
    
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('Start')
        result = func(*args, **kwargs)
        print('End')
        return result
    return wrapper

@start_end_decorator_4
def add_5(x):
    return x + 5
result = add_5(10)
print(result)
print(add_5.__name__)
help(add_5)
    Start
    End
    15
    add_5
    Help on function add_5 in module __main__:

    add_5(x)

装饰器的最终模板

现在,我们已经有了所有部分,用于任何装饰器的模板如下所示:

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        # Do something before
        result = func(*args, **kwargs)
        # Do something after
        return result
    return wrapper

装饰器函数参数

请注意, functools.wraps 是一个装饰器,它自己接受一个参数。 我们可以将其视为2个内部函数,即内部函数里的内部函数。 为了更清楚地说明这一点,我们来看另一个示例:以数字作为输入的 repeat 装饰器。 在此函数内,我们有实际的装饰函数,该函数包装函数并在另一个内部函数内扩展其行为。 在这种情况下,它将输入函数重复给定的次数。

def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")
    
greet('Alex')
    Hello Alex
    Hello Alex
    Hello Alex

嵌套装饰器

我们可以通过将多个装饰器彼此堆叠来将其应用到一个函数。 装饰器将按照其列出的顺序执行。

# 装饰器函数,它输出有关包装函数的调试信息
def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        args_repr = [repr(a) for a in args]
        kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
        signature = ", ".join(args_repr + kwargs_repr)
        print(f"Calling {func.__name__}({signature})")
        result = func(*args, **kwargs)
        print(f"{func.__name__!r} returned {result!r}")
        return result
    return wrapper

@debug
@start_end_decorator_4
def say_hello(name):
    greeting = f'Hello {name}'
    print(greeting)
    return greeting

# 现在 `debug` 先执行,然后调用 `@start_end_decorator_4`,后者优惠调用 `say_hello`
say_hello(name='Alex')
    Calling say_hello(name='Alex')
    Start
    Hello Alex
    End
    'say_hello' returned 'Hello Alex'

类装饰器

我们也可以使用一个类作为装饰器。 因此,我们必须实现 __call__() 方法以使我们的对象可调用。 类装饰器通常用于维护状态,例如: 在这里,我们跟踪函数执行的次数。 __call__方法与我们之前看到的 wrapper() 方法本质上是相同的。 它添加了一些功能,执行了该函数,并返回其结果。 请注意,这里我们使用 functools.update_wrapper() 代替 functools.wraps 来保留有关函数的信息。

import functools

class CountCalls:
    # 初始化需要以func作为参数并将其存储
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.num_calls = 0
    
    # 扩展功能,执行函数并返回结果
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print(f"Call {self.num_calls} of {self.func.__name__!r}")
        return self.func(*args, **kwargs)

@CountCalls
def say_hello(num):
    print("Hello!")
    
say_hello(5)
say_hello(5)
    Call 1 of 'say_hello'
    Hello!
    Call 2 of 'say_hello'
    Hello!

一些典型的用例

  • 使用计时器装饰器来计算函数的执行时间

  • 使用调试装饰器来打印出有关被调用函数及其参数的更多信息

  • 使用检查修饰符检查参数是否满足某些要求并相应地调整行为

  • 注册函数(插件)

  • 使用 time.sleep() 降低代码速度以检查网络行为

  • 缓存返回值以进行记忆化(https://en.wikipedia.org/wiki/Memoization)

  • 添加信息或更新状态

14. 生成器

生成器是可以在运行中暂停和恢复的函数,返回可以迭代的对象。 与列表不同,它们是懒惰的,因此一次仅在被询问时才产生一项。 因此,在处理大型数据集时,它们的内存效率更高。

生成器的定义类似于普通函数,但是使用 yield 语句而不是 return

def my_generator():
    yield 1
    yield 2
    yield 3

执行生成器函数

调用该函数不会执行它,而是函数返回一个生成器对象,该对象用于控制执行。 生成器对象在调用 next() 时执行。 首次调用 next() 时,执行从函数的开头开始,一直持续到第一个 yield 语句,在该语句中返回语句右边的值。 随后对 next() 的调用从 yield 语句继续(并循环),直到达到另一个 yield。 如果由于条件而未调用 yield 或到达末尾,则会引发 StopIteration 异常:

def countdown(num):
    print('Starting')
    while num > 0:
        yield num
        num -= 1

# 这不会打印 'Starting'
cd = countdown(3)

# 这会打印 'Starting' 以及第一个值
print(next(cd))

# 会打印第二个值
print(next(cd))
print(next(cd))

# 这会引发 StopIteration
print(next(cd))
    Starting
    3
    2
    1
    ---------------------------------------------------------------------------
    StopIteration                             Traceback (most recent call last)
    <ipython-input-1-3941498e0bf0> in <module>
         16 
         17 # this will raise a StopIteration
    ---> 18 print(next(cd))

    StopIteration:
# 你可以使用 for 循环来遍历一个生成器对象
cd = countdown(3)
for x in cd:
    print(x)
    Starting
    3
    2
    1
# 你可以将其用于接受可迭代对象作为输入的函数
cd = countdown(3)
sum_cd = sum(cd)
print(sum_cd)

cd = countdown(3)
sorted_cd = sorted(cd)
print(sorted_cd)
    Starting
    6
    Starting
    [1, 2, 3]

最大的优点:迭代器节省内存!

由于这些值是延迟生成的,即仅在需要时才生成,因此可以节省大量内存,尤其是在处理大数据时。 此外,我们不必等到所有元素生成后再开始使用它们。

# 如果没有生成器,则必须将完整序列存储在此处的列表中
def firstn(n):
    num, nums = 0, []
    while num < n:
        nums.append(num)
        num += 1
    return nums

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
    499999500000
    8697464 bytes
# 使用生成器,不需要额外的序列来存储数字
def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1

sum_of_first_n = sum(firstn(1000000))
print(sum_of_first_n)
import sys
print(sys.getsizeof(firstn(1000000)), "bytes")
    499999500000
    120 bytes

另一个例子:斐波那契数列

def fibonacci(limit):
    a, b = 0, 1 # 前两个数
    while a < limit:
        yield a
        a, b = b, a + b

fib = fibonacci(30)
# 生成器对象可以被转为列表(这儿只是用来打印)
print(list(fib))
    [0, 1, 1, 2, 3, 5, 8, 13, 21]

生成器表达式

就像列表推导一样,生成器可以用相同的语法编写,除了用括号代替方括号。 注意不要混淆它们,因为由于函数调用的开销,生成器表达式通常比列表理解要慢(https://stackoverflow.com/questions/11964130/list-comprehension-vs-generator-expressions-weird-timeit-results/11964478#11964478)。

# 生成器表达式
mygenerator = (i for i in range(1000) if i % 2 == 0)
print(sys.getsizeof(mygenerator), "bytes")

# 列表推导式
mylist = [i for i in range(1000) if i % 2 == 0]
print(sys.getsizeof(mylist), "bytes")
    120 bytes
    4272 bytes

生成器背后的概念

这个类将生成器实现为可迭代的对象。 它必须实现 __iter____next__ 使其可迭代,跟踪当前状态(在这种情况下为当前数字),并注意 StopIteration。 它可以用来理解生成器背后的概念。 但是,有很多样板代码,其逻辑并不像使用 yield 关键字的简单函数那样清晰。

class firstn:
    def __init__(self, n):
        self.n = n
        self.num = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.num < self.n:
            cur = self.num
            self.num += 1
            return cur
        else:
            raise StopIteration()
             
firstn_object = firstn(1000000)
print(sum(firstn_object))
    499999500000

15. 多线程和多进程

我们有两种常用的方法来并行运行代码(实现多任务并加快程序速度):通过线程或通过多进程。

进程

进程是程序的一个实例,例如Python解释器。它们彼此独立,并且不共享相同的内存。

关键事实:

  • 一个新进程独立于第一个进程启动

  • 充分利用多个CPU和内核

  • 单独的内存空间

  • 进程之间不共享内存

  • 每个进程一个GIL(全局解释器锁),即避免了GIL限制

  • 非常适合CPU密集型处理

  • 子进程可中断/可终止

  • 启动进程慢于启动线程

  • 更大的内存占用

  • IPC(进程间通信)更加复杂

线程

线程是可以调度执行的进程(也称为“轻量级进程”)中的实体。一个进程可以产生多个线程。主要区别在于,进程中的所有线程共享同一内存。

关键事实:

  • 可以在一个进程中产生多个线程

  • 内存在所有线程之间共享

  • 启动线程比启动进程要快

  • 适用于 I/O 密集型任务

  • 轻量

  • 内存占用少

  • 所有线程使用一个GIL,即线程受GIL限制

  • 由于GIL,多线程处理对CPU密集的任务无效

  • 不可中断/杀死->注意内存泄漏

  • 出现竞态情况的可能性增加

Python中的线程

使用 threading 模块。

注意:由于受CPU限制,以下示例通常不会从多个线程中受益。 它应显示如何使用线程的示例。

from threading import Thread

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    threads = []
    num_threads = 10

    # 创建线程,并给每一个线程分配函数
    for i in range(num_threads):
        thread = Thread(target=square_numbers)
        threads.append(thread)

    # 启动所有线程
    for thread in threads:
        thread.start()

    # 等待所有线程结束
    # 阻塞主线程直到所有线程结束
    for thread in threads:
        thread.join()

线程何时有用

尽管使用了GIL,但在程序必须与速度较慢的设备(例如硬盘驱动器或网络连接)进行通讯时,它仍可用于 I/O 密集型任务。 通过线程化,程序可以花费时间等待这些设备并同时智能地执行其他任务。

示例:从多个站点下载网站信息。 为每个站点使用一个线程。

多进程

使用 multiprocessing 模块。 语法与上面非常相似。

from multiprocessing import Process
import os

def square_numbers():
    for i in range(1000):
        result = i * i

if __name__ == "__main__":
    processes = []
    num_processes = os.cpu_count()

    # 创建进程,并给每一个线程分配函数
    for i in range(num_processes):
        process = Process(target=square_numbers)
        processes.append(process)

    # 启动所有进程
    for process in processes:
        process.start()

    # 等待所有进程结束
    # 阻塞主线程直到所有进程结束
    for process in processes:
        process.join()

什么时候多进程有用

这对于必须对大量数据执行大量CPU操作且需要大量计算时间的CPU密集型任务很有用。通过多进程,你可以将数据分成相等的部分,然后在不同的CPU上进行并行计算。

示例:计算从1到1000000的所有数字的平方数。将数字分成相等大小的部分,并对每个子集使用一个过程。

GIL-全局解释器锁

这是一个互斥锁(或锁),仅允许一个线程控制Python解释器。这意味着即使在多线程体系结构中,GIL一次也只允许一个线程执行。

为什么需要它?

之所以需要它,是因为CPython(Python的引用实现)的内存管理不是线程安全的。 Python使用引用计数进行内存管理。这意味着在Python中创建的对象具有引用计数变量,该变量跟踪指向该对象的引用数。当此计数达到零时,将释放对象占用的内存。问题在于该引用计数变量需要保护,以防止两个线程同时增大或减小其值的竞争条件。如果发生这种情况,则可能导致从未释放的内存泄漏,或者在仍然存在对该对象的引用的情况下错误地释放了内存。

如何避免GIL

GIL在Python社区中引起很大争议。避免GIL的主要方法是使用多线程而不是线程。另一个(但是很不舒服)的解决方案是避免CPython实现,而使用 JythonIronPython 之类的自由线程Python实现。第三种选择是将应用程序的部分移到二进制扩展模块中,即使用Python作为第三方库的包装器(例如在C / C ++中)。这是 numpyscipy 采取的路径。

16. 多线程

在本文中,我们讨论了如何在Python中使用 threading 模块。

  • 如何创建和启动多个线程

  • 如何等待线程完成

  • 如何在线程之间共享数据

  • 如何使用锁( lock )来防止竞态情况

  • 什么是守护线程

  • 如何使用 Queue 进行线程安全的数据/任务处理。

创建和运行线程

你可以使用 threading.Thread() 创建一个线程。 它包含两个重要的参数:

  • target:线程启动时要调用的该线程的可调用对象(函数)

  • args:目标函数的(函数)参数。 这必须是一个元组

使用 thread.start() 启动线程

调用 thread.join() 告诉程序在继续执行其余代码之前,应等待该线程完成。

from threading import Thread

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    threads = []
    num_threads = 10

    # 创建线程,并给每一个线程分配函数
    for i in range(num_threads):
        thread = Thread(target=square_numbers)
        threads.append(thread)

    # 启动所有线程
    for thread in threads:
        thread.start()

    # 等待所有线程结束
    # 阻塞主线程直到所有线程结束
    for thread in threads:
        thread.join()

在线程之间共享数据

由于线程位于相同的内存空间中,因此它们可以访问相同的(公共)数据。 因此,例如,你可以简单地使用所有线程都具有读取和写入访问权限的全局变量。

任务:创建两个线程,每个线程应访问当前数据库值,对其进行修改(在这种情况下,仅将其增加1),然后将新值写回到数据库值中。 每个线程应执行10次此操作。

from threading import Thread
import time

# 所有线程可以访问全局变量
database_value = 0

def increase():
    global database_value # 需要可以修改全局变量
    
    # 获取本地副本(模拟数据获取)
    local_copy = database_value

    # 模拟一些修改操作
    local_copy += 1
    time.sleep(0.1)

    # 将计算的性质写入全局变量
    database_value = local_copy

if __name__ == "__main__":

    print('Start value: ', database_value)

    t1 = Thread(target=increase)
    t2 = Thread(target=increase)

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print('End value:', database_value)

    print('end main')
    Start value:  0
    End value: 1
    end main

如何使用锁

请注意,在上面的示例中,2个线程将值递增1,因此将执行2个递增操作。但是,为什么最终值是1而不是2?

竞态条件

这里发生了竞态情况。当两个或多个线程可以访问共享数据并且它们试图同时更改它们时,就会发生竞态情况。因为线程调度算法可以随时在线程之间交换,所以你不知道线程尝试访问共享数据的顺序。在我们的例子中,第一个线程访问 database_value(0)并将其存储在本地副本中。然后将其递增( local_copy 现在为1)。利用我们的 time.sleep() 函数,该函数仅模拟一些耗时的操作,在此期间,程序将交换到第二个线程。这还将检索当前的 database_value(仍为0),并将 local_copy 递增为1。现在,两个线程都有一个值为1的本地副本,因此两个线程都将1写入全局 database_value。这就是为什么最终值是1而不是2的原因。

使用锁避免竞态条件

锁(也称为互斥锁)是一种同步机制,用于在存在许多执行线程的环境中强制限制对资源的访问。锁具有两种状态:锁定解锁。如果状态是锁定的,则在状态再次被解锁之前,不允许其他并发线程进入此代码段。

两个函数很重要:

  • lock.acquire():这将锁定状态并阻塞

  • lock.release():这将再次解锁状态。

重要提示:块获得后,你应始终再次释放它!

在我们的示例中,检索和修改数据库值的关键代码部分现已锁定。这样可以防止第二个线程同时修改全局数据。我们的代码没有太大变化。所有新更改都在下面的代码中进行了注释。

# import Lock
from threading import Thread, Lock
import time

database_value = 0

def increase(lock):
    global database_value 
    
    # 锁定状态
    lock.acquire()
    
    local_copy = database_value
    local_copy += 1
    time.sleep(0.1)
    database_value = local_copy
    
    # 解锁状态
    lock.release()

if __name__ == "__main__":

    # 创建锁
    lock = Lock()
    
    print('Start value: ', database_value)

    # 将锁传递给目标函数
    t1 = Thread(target=increase, args=(lock,)) # 注意锁后的逗号,因为args必须是一个元组
    t2 = Thread(target=increase, args=(lock,))

    t1.start()
    t2.start()

    t1.join()
    t2.join()

    print('End value:', database_value)

    print('end main')
    Start value:  0
    End value: 2
    end main

使用锁作为上下文管理器

lock.acquire() 之后,你应该永远不要忘记调用 lock.release() 来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:

def increase(lock):
    global database_value 
    
    with lock: 
        local_copy = database_value
        local_copy += 1
        time.sleep(0.1)
        database_value = local_copy

在Python中使用队列

队列可用于多线程和多进程环境中的线程安全/进程安全的数据交换和数据处理。

队列

队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。

from queue import Queue

# 创建队列
q = Queue()

# 添加元素
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1 

# 现在 q 看起来是这样的:
# back --> 3 2 1 --> front

# 获取和移除第一个元素
first = q.get() # --> 1
print(first) 

# q 现在看起来是这样的:
# back --> 3 2 --> front
    1

在多线程中使用队列

带有队列的操作是线程安全的。重要方法是:

  • q.get():删除并返回第一项。默认情况下,它会阻塞,直到该项可用为止。

  • q.put(item):将元素放在队列的末尾。默认情况下,它会阻塞,直到有空闲插槽可用为止。

  • q.task_done():指示先前入队的任务已完成。对于每个 get(),在完成此项任务后,都应调用此函数。

  • q.join():阻塞直到队列中的所有项目都已获取并处理(已为每个项目调用 task_done())。

  • q.empty():如果队列为空,则返回True。

以下示例使用队列来交换0至19之间的数字。每个线程都调用worker方法。在无限循环内,线程等待直到由于阻塞 q.get() 调用而使项可用为止。项可用时,将对其进行处理(即,仅在此处打印),然后 q.task_done() 告知队列处理已完成。在主线程中,创建10个守护线程。这意味着它们在主线程死亡时自动死亡,因此不再调用worker方法和无限循环。然后,队列中填充了项,并且worker方法可以继续使用可用项。最后,需要 q.join() 来阻塞主线程,直到获得并处理所有项为止。

from threading import Thread, Lock, current_thread
from queue import Queue

def worker(q, lock):
    while True:
        value = q.get()  # 阻塞知道有可用项

        # 做一些处理...
        with lock:
            # 使用锁阻止其他打印
            print(f"in {current_thread().name} got {value}")
        # ...

        # 对弈每一个 get(),随后对 task_done() 的调用告诉队列该项的处理已完成。
        # 如果完成所有任务,则 q.join() 可以取消阻塞
        q.task_done()

if __name__ == '__main__':
    q = Queue()
    num_threads = 10
    lock = Lock()

    for i in range(num_threads):
        t = Thread(name=f"Thread{i+1}", target=worker, args=(q, lock))
        t.daemon = True  # 当主线程死亡时死亡
        t.start()
    
    # 使用项填充队列
    for x in range(20):
        q.put(x)

    q.join()  # 阻塞直到队列中的所有项被获取并处理

    print('main done')
    in Thread1 got 0
    in Thread2 got 1
    in Thread2 got 11
    in Thread2 got 12
    in Thread2 got 13
    in Thread2 got 14
    in Thread2 got 15
    in Thread2 got 16
    in Thread2 got 17
    in Thread2 got 18
    in Thread2 got 19
    in Thread8 got 5
    in Thread4 got 9
    in Thread1 got 10
    in Thread5 got 2
    in Thread6 got 3
    in Thread9 got 6
    in Thread7 got 4
    in Thread10 got 7
    in Thread3 got 8
    main done

守护线程

在以上示例中,使用了守护线程。 守护线程是后台线程,它们在主程序结束时自动消失。 这就是为什么可以退出 worker 方法内的无限循环的原因。 没有守护进程,我们将不得不使用诸如 threading.Event 之类的信号机制来停止 worker。 但请注意守护进程:它们会突然停止,并且它们的资源(例如打开的文件或数据库事务)可能无法正确释放/完成。

17. 多进程

在本文中,我们讨论了如何在Python中使用 multiprocessing 模块。

  • 如何创建和启动多个进程

  • 如何等待进程完成

  • 如何在进程之间共享数据

  • 如何使用 lock 来防止竞态情

  • 如何使用 Queue 进行进程安全的数据/任务处理

  • 如何使用 Pool 来管理多个工作进程。

创建和运行进程

你可以使用 multiprocessing.Process() 创建一个进程。 它包含两个重要的参数:

  • target:进程启动时要调用的可调用对象(函数)

  • args:目标函数的(函数)参数。 这必须是一个元组。

使用 process.start() 启动一个进程

调用 process.join() 告诉程序在继续执行其余代码之前,应等待该进程完成。

from multiprocessing import Process
import os

def square_numbers():
    for i in range(1000):
        result = i * i

        
if __name__ == "__main__":        
    processes = []
    num_processes = os.cpu_count()
    # 机器CPU的数量,通常是确定进程数量的一个好选择

    # 创建进程并分配每个进程一个函数
    for i in range(num_processes):
        process = Process(target=square_numbers)
        processes.append(process)

    # 启动所有进程
    for process in processes:
        process.start()

    # 等待所有进程结束
    # 阻塞主程序直到所有进程结束
    for process in processes:
        process.join()

在进程之间共享数据

由于进程不在同一个内存空间中,因此它们无法访问相同(公共)数据。 因此,它们需要特殊的共享内存对象来共享数据。

可以使用 Value 或者 Array 将数据存储在共享内存变量中。

  • Value(type, value):创建类型为 typectypes 对象。 使用 .target 访问该值。

  • Array(type, value):使用类型为 type 的元素创建一个 ctypes 数组。 用 [] 访问值。

任务:创建两个进程,每个进程都应该有权访问一个共享变量并对其进行修改(在这种情况下,只是将其重复增加1达100次)。 创建另外两个共享一个数组的进程,然后修改(增加)该数组中的所有元素。

from multiprocessing import Process, Value, Array
import time

def add_100(number):
    for _ in range(100):
        time.sleep(0.01)
        number.value += 1

def add_100_array(numbers):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            numbers[i] += 1

if __name__ == "__main__":

    shared_number = Value('i', 0) 
    print('Value at beginning:', shared_number.value)

    shared_array = Array('d', [0.0, 100.0, 200.0])
    print('Array at beginning:', shared_array[:])

    process1 = Process(target=add_100, args=(shared_number,))
    process2 = Process(target=add_100, args=(shared_number,))

    process3 = Process(target=add_100_array, args=(shared_array,))
    process4 = Process(target=add_100_array, args=(shared_array,))

    process1.start()
    process2.start()
    process3.start()
    process4.start()

    process1.join()
    process2.join()
    process3.join()
    process4.join()

    print('Value at end:', shared_number.value)
    print('Array at end:', shared_array[:])

    print('end main')
    Value at beginning: 0
    Array at beginning: [0.0, 100.0, 200.0]
    Value at end: 144
    Array at end: [134.0, 237.0, 339.0]
    end main

如何使用锁

请注意,在上面的示例中,两个进程应将共享值增加1达100次。 这样一来,总共进行了200次操作。 但是为什么终值不是200?

竞态条件

这里发生了竞态情况。当两个或多个进程或线程可以访问共享数据并且它们试图同时更改它们时,就会发生竞态情况。在我们的示例中,两个进程必须读取共享值,将其增加1,然后将其写回到共享变量中。如果这同时发生,则两个进程将读取相同的值,将其增加并写回。因此,两个进程都将相同的增加的值写回到共享对象中,并且该值未增加2。有关竞态条件的详细说明,请参见 16. 多线程

避免带锁的竞态条件

锁(也称为互斥锁)是一种同步机制,用于在存在许多执行进程/线程的环境中强制限制对资源的访问。锁具有两种状态:锁定和解锁。如果状态为锁定,则在状态再次被解锁之前,不允许其他并发进程/线程进入此代码段。

两个函数很重要:

  • lock.acquire():这将锁定状态并阻塞

  • lock.release():这将再次解锁状态。

重要提示:块获得后,你应始终再次释放它!

在我们的示例中,读取并增加了共享变量的关键代码部分现已锁定。这样可以防止第二个进程同时修改共享库。我们的代码没有太大变化。所有新更改都在下面的代码中进行了注释。

# import Lock
from multiprocessing import Lock
from multiprocessing import Process, Value, Array
import time

def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        # lock the state
        lock.acquire()
        
        number.value += 1
        
        # 解锁状态
        lock.release()

def add_100_array(numbers, lock):
    for _ in range(100):
        time.sleep(0.01)
        for i in range(len(numbers)):
            lock.acquire()
            numbers[i] += 1
            lock.release()

if __name__ == "__main__":

    # 创建锁
    lock = Lock()
    
    shared_number = Value('i', 0) 
    print('Value at beginning:', shared_number.value)

    shared_array = Array('d', [0.0, 100.0, 200.0])
    print('Array at beginning:', shared_array[:])

    # 将锁传入目标函数
    process1 = Process(target=add_100, args=(shared_number, lock))
    process2 = Process(target=add_100, args=(shared_number, lock))

    process3 = Process(target=add_100_array, args=(shared_array, lock))
    process4 = Process(target=add_100_array, args=(shared_array, lock))

    process1.start()
    process2.start()
    process3.start()
    process4.start()

    process1.join()
    process2.join()
    process3.join()
    process4.join()

    print('Value at end:', shared_number.value)
    print('Array at end:', shared_array[:])

    print('end main')
    Value at beginning: 0
    Array at beginning: [0.0, 100.0, 200.0]
    Value at end: 200
    Array at end: [200.0, 300.0, 400.0]
    end main

使用锁作为上下文管理器

lock.acquire() 之后,你应该永远不要忘记调用 lock.release() 来解锁代码。 你还可以将锁用作上下文管理器,这将安全地锁定和解锁你的代码。 建议以这种方式使用锁:

def add_100(number, lock):
    for _ in range(100):
        time.sleep(0.01)
        with lock:
            number.value += 1

在Python中使用队列

数据也可以通过队列在进程之间共享。 队列可用于多线程和多进程环境中的线程安全/进程安全数据交换和数据处理,这意味着你可以避免使用任何同步原语(例如锁)。

队列 队列是遵循先进先出(FIFO)原理的线性数据结构。 一个很好的例子是排队等候的客户队列,其中首先服务的是第一位的客户。

from multiprocessing import Queue

# 创建队列
q = Queue()

# 添加元素
q.put(1) # 1
q.put(2) # 2 1
q.put(3) # 3 2 1 

# 现在 q 看起来是这样的:
# back --> 3 2 1 --> front

# 获取和移除第一个元素
first = q.get() # --> 1
print(first) 

# q 现在看起来是这样的:
# back --> 3 2 --> front
    1

在多进程中使用队列

带有队列的操作是进程安全的。 除了 task_done()join() 之外,多进程队列实现了 queue.Queue 的所有方法。 重要方法是:

  • q.get():删除并返回第一项。 默认情况下,它会阻塞,直到该项可用为止。

  • q.put(item):将元素放在队列的末尾。 默认情况下,它会阻塞,直到有空闲插槽可用为止。

  • q.empty():如果队列为空,则返回True。

  • q.close():指示当前进程不会再将更多数据放入此队列。

# 使用多进程队列在进程之间进行通信
# 队列是线程和进程安全的
from multiprocessing import Process, Queue

def square(numbers, queue):
    for i in numbers:
        queue.put(i*i)

def make_negative(numbers, queue):
    for i in numbers:
        queue.put(i*-1)

if __name__ == "__main__":
    
    numbers = range(1, 6)
    q = Queue()

    p1 = Process(target=square, args=(numbers,q))
    p2 = Process(target=make_negative, args=(numbers,q))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    # 顺序可能不是按序列的
    while not q.empty():
        print(q.get())
        
    print('end main')
    1
    4
    9
    16
    25
    -1
    -2
    -3
    -4
    -5
    end main

进程池

进程池对象控制可以向其提交作业的工作进程池。它支持带有超时和回调的异步结果,并具有并行映射实现。它可以自动管理可用的处理器,并将数据拆分为较小的块,然后由不同的进程并行处理。有关所有可能的方法,请参见 https://docs.python.org/3.7/library/multiprocessing.html#multiprocessing.pool。重要方法有

  • map(func, iterable[, chunksize]):此方法将 Iterable 分成许多块,作为单独的任务提交给进程池。这些块的(大约)大小可以通过将 chunksize 设置为正整数来指定。它会阻塞,直到结果准备好为止。

  • close():阻止将更多任务提交到池中。一旦完成所有任务,工作进程将退出。

  • join():等待工作进程退出。使用 join() 之前,必须先调用 close()terminate()

  • apply(func, args):使用参数args调用func。它会阻塞,直到结果准备好为止。 func仅在池的一个工作程序中执行。

注意:也有不会阻塞的异步变体 map_async()apply_async()。结果准备好后,他们可以执行回调。

from multiprocessing import Pool 

def cube(number):
    return number * number * number

    
if __name__ == "__main__":
    numbers = range(10)
    
    p = Pool()

    # 默认情况下,这将分配此任务的最大可用处理器数 --> os.cpu_count()
    result = p.map(cube,  numbers)
    
    # or 
    # result = [p.apply(cube, args=(i,)) for i in numbers]
    
    p.close()
    p.join()
    
    print(result)
    [0, 1, 8, 27, 64, 125, 216, 343, 512, 729]

18. 函数参数

在本文中,我们将详细讨论函数形参(parameters)和函数实参(arguments)。 我们将学习:

  • 形参和实参之间的区别

  • 位置和关键字参数

  • 默认参数

  • 变长参数( *args**kwargs

  • 容器拆包成函数参数

  • 局部与全局参数

  • 参数传递(按值还是按引用?)

形参和实参之间的区别

  • 形数是定义函数时在括号内定义或使用的变量

  • 实参是调用函数时为这些参数传递的值

def print_name(name): # name 是形参
    print(name)

print_name('Alex') # 'Alex' 是实参

位置和关键字参数

我们可以将参数作为位置参数或关键字参数传递。 关键字参数的一些好处可能是:

  • 我们可以通过名称来调用参数,以使其更清楚地表示其含义

  • 我们可以通过重新排列参数的方式来使参数最易读

def foo(a, b, c):
    print(a, b, c)
    
# 位置参数
foo(1, 2, 3)

# 关键字参数
foo(a=1, b=2, c=3)
foo(c=3, b=2, a=1) # 注意此处顺序不重要

# 混合使用
foo(1, b=2, c=3)

# 以下不允许
# foo(1, b=2, 3) # 位置参数在关键字参数之后
# foo(1, b=2, a=3) # 'a' 有多个值
    1 2 3
    1 2 3
    1 2 3
    1 2 3

默认参数

函数可以具有带有预定义值的默认参数。 可以忽略此参数,然后将默认值传递给函数,或者可以将参数与其他值一起使用。 注意,必须将默认参数定义为函数中的最后一个参数。

# 默认参数
def foo(a, b, c, d=4):
    print(a, b, c, d)

foo(1, 2, 3, 4)
foo(1, b=2, c=3, d=100)

# 不允许:默认参数必需在最后
# def foo(a, b=2, c, d=4):
#     print(a, b, c, d)
    1 2 3 4
    1 2 3 100

变长参数( *args**kwargs

  • 如果用一个星号( * )标记参数,则可以将任意数量的位置参数传递给函数(通常称为 *args

  • 如果用两个星号( ** )标记参数,则可以将任意数量的关键字参数传递给该函数(通常称为 **kwargs )。

def foo(a, b, *args, **kwargs):
    print(a, b)
    for arg in args:
        print(arg)
    for kwarg in kwargs:
        print(kwarg, kwargs[kwarg])

# 3, 4, 5 合并入 args
# six and seven 合并入 kwargs
foo(1, 2, 3, 4, 5, six=6, seven=7)
print()

# 也可以省略 args 或 kwargs
foo(1, 2, three=3)
    1 2
    3
    4
    5
    six 6
    seven 7

    1 2
    three 3

强制关键字参数

有时你想要仅使用关键字的参数。 你可以执行以下操作:

  • 如果在函数参数列表中输入 *,,则此后的所有参数都必须作为关键字参数传递。

  • 变长参数后面的参数必须是关键字参数。

def foo(a, b, *, c, d):
    print(a, b, c, d)

foo(1, 2, c=3, d=4)
# 不允许:
# foo(1, 2, 3, 4)

# 变长参数后面的参数必须是关键字参数
def foo(*args, last):
    for arg in args:
        print(arg)
    print(last)

foo(8, 9, 10, last=50)
    1 2 3 4
    8
    9
    10
    50

拆包成参数

  • 如果容器的长度与函数参数的数量匹配,则列表或元组可以用一个星号( * )拆包为参数。

  • 字典可以拆包为带有两个星号( ** )的参数,其长度和键与函数参数匹配。

def foo(a, b, c):
    print(a, b, c)

# list/tuple 拆包,长度必需匹配
my_list = [4, 5, 6] # or tuple
foo(*my_list)

# dict 拆包,键和长度必需匹配
my_dict = {'a': 1, 'b': 2, 'c': 3}
foo(**my_dict)

# my_dict = {'a': 1, 'b': 2, 'd': 3} # 不可能,因为关键字错误
    4 5 6
    1 2 3

局部变量与全局变量

可以在函数体内访问全局变量,但是要对其进行修改,我们首先必须声明 global var_name 才能更改全局变量。

def foo1():
    x = number # 全局变量只能在这里访问
    print('number in function:', x)

number = 0
foo1()

# 修改全局变量
def foo2():
    global number # 现在可以访问和修改全局变量
    number = 3

print('number before foo2(): ', number)
foo2() # 修改全局变量
print('number after foo2(): ', number)
    number in function: 0
    number before foo2():  0
    number after foo2():  3

如果我们不写 global var_name 并给与全局变量同名的变量赋一个新值,这将在函数内创建一个局部变量。 全局变量保持不变。

number = 0

def foo3():
    number = 3 # 这是局部变量

print('number before foo3(): ', number)
foo3() # 不会修改全局变量
print('number after foo3(): ', number)
    number before foo3():  0
    number after foo3():  0

参数传递

Python使用一种称为“对象调用”或“对象引用调用”的机制。必须考虑以下规则:

  • 传入的参数实际上是对对象的引用(但引用是按值传递)

  • 可变和不可变数据类型之间的差异

这意味着:

  1. 可变对象(例如列表,字典)可以在方法中进行更改。但是,如果在方法中重新绑定引用,则外部引用仍将指向原始对象。

  2. 不能在方法中更改不可变的对象(例如int,string)。但是包含在可变对象中的不可变对象可以在方法中重新分配。

# 不可变对象 -> 不变
def foo(x):
    x = 5 # x += 5 也无效,因为x是不可变的,必须创建一个新变量

var = 10
print('var before foo():', var)
foo(var)
print('var after foo():', var)
    var before foo(): 10
    var after foo(): 10
# 可变对象 -> 可变
def foo(a_list):
    a_list.append(4)
    
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3, 4]
# 不可变对象包含在可变对象内 -> 可变
def foo(a_list):
    a_list[0] = -100
    a_list[2] = "Paul"
    
my_list = [1, 2, "Max"]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
# 重新绑定可变引用 -> 不变
def foo(a_list):
    a_list = [50, 60, 70] # a_list 是函数内新的局部变量
    a_list.append(50)
    
my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3]

对于可变类型,请小心使用 +== 操作。 第一个操作对传递的参数有影响,而后者则没有:

# 重新绑定引用的另一个例子
def foo(a_list):
    a_list += [4, 5] # 这会改变外部变量
    
def bar(a_list):
    a_list = a_list + [4, 5] # 在会重新绑定引用到本地变量

my_list = [1, 2, 3]
print('my_list before foo():', my_list)
foo(my_list)
print('my_list after foo():', my_list)

my_list = [1, 2, 3]
print('my_list before bar():', my_list)
bar(my_list)
print('my_list after bar():', my_list)
    my_list before foo(): [1, 2, 3]
    my_list after foo(): [1, 2, 3, 4, 5]
    my_list before bar(): [1, 2, 3]
    my_list after bar(): [1, 2, 3]

19. 星号操作符

星号( * )可用于Python中的不同情况:

  • 乘法和幂运算

  • 创建具有重复元素的列表,元组或字符串

  • *args**kwargs 和仅关键字参数

  • 拆包列表/元组/字典的函数参数

  • 拆包容器

  • 将可迭代对象合并到列表中/合并字典

乘法和幂运算

# 乘法
result = 7 * 5
print(result)

# 幂运算
result = 2 ** 4
print(result)
    35
    16

创建具有重复元素的列表,元组或字符串

# list
zeros = [0] * 10
onetwos = [1, 2] * 5
print(zeros)
print(onetwos)

# tuple
zeros = (0,) * 10
onetwos = (1, 2) * 5
print(zeros)
print(onetwos)

# string
A_string = "A" * 10
AB_string = "AB" * 5
print(A_string)
print(AB_string)
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
    (0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
    (1, 2, 1, 2, 1, 2, 1, 2, 1, 2)
    AAAAAAAAAA
    ABABABABAB

*args**kwargs 和仅关键字参数

  • 对可变长度参数使用 *args

  • 对长度可变的关键字参数使用 **kwargs

  • 使用 *,后跟更多函数参数以强制使用仅关键字的参数

def my_function(*args, **kwargs):
    for arg in args:
        print(arg)
    for key in kwargs:
        print(key, kwargs[key])
        
my_function("Hey", 3, [0, 1, 2], name="Alex", age=8)

# '*' 或 '* identifier' 之后的参数是仅关键字参数,只能使用关键字参数传递。
def my_function2(name, *, age):
    print(name)
    print(age)

# my_function2("Michael", 5) --> 这会引发 TypeError 错误
my_function2("Michael", age=5)
    Hey
    3
    [0, 1, 2]
    name Alex
    age 8
    Michael
    5

拆包函数参数

  • 如果长度与参数匹配,则列表/元组/集合/字符串可以用 * 拆成函数参数。

  • 如果长度和键与参数匹配,则字典可以用两个 ** 拆包。

def foo(a, b, c):
    print(a, b, c)

# 长度必需匹配
my_list = [1, 2, 3]
foo(*my_list)

my_string = "ABC"
foo(*my_string)

# 长度和键必需匹配
my_dict = {'a': 4, 'b': 5, 'c': 6}
foo(**my_dict)
    1 2 3
    A B C
    4 5 6

拆包容器

将列表,元组或集合的元素拆包为单个和多个剩余元素。 请注意,即使被拆包的容器是元组或集合,也将多个元素组合在一个列表中。

numbers = (1, 2, 3, 4, 5, 6, 7, 8)

*beginning, last = numbers
print(beginning)
print(last)

print()

first, *end = numbers
print(first)
print(end)

print()
first, *middle, last = numbers
print(first)
print(middle)
print(last)
    [1, 2, 3, 4, 5, 6, 7]
    8

    1
    [2, 3, 4, 5, 6, 7, 8]

    1
    [2, 3, 4, 5, 6, 7]
    8

将可迭代对象合并到列表中/合并字典

由于PEP 448(https://www.python.org/dev/peps/pep-0448/),从Python 3.5开始,这是可能的。

# 将可迭代对象合并到列表中
my_tuple = (1, 2, 3)
my_set = {4, 5, 6}
my_list = [*my_tuple, *my_set]
print(my_list)

# 用字典拆包合并两个字典
dict_a = {'one': 1, 'two': 2}
dict_b = {'three': 3, 'four': 4}
dict_c = {**dict_a, **dict_b}
print(dict_c)
    [1, 2, 3, 4, 5, 6]
    {'one': 1, 'two': 2, 'three': 3, 'four': 4}

但是,请注意以下合并解决方案。 如果字典中有任何非字符串键,则它将不起作用:https://stackoverflow.com/questions/38987/how-to-merge-two-dictionaries-in-a-single-expression/39858#39858

dict_a = {'one': 1, 'two': 2}
dict_b = {3: 3, 'four': 4}
dict_c = dict(dict_a, **dict_b)
print(dict_c)

# 以下可行:
# dict_c = {**dict_a, **dict_b}
---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-52-2660fb90a60f> in <module>
          1 dict_a = {'one': 1, 'two': 2}
          2 dict_b = {3: 3, 'four': 4}
    ----> 3 dict_c = dict(dict_a, **dict_b)
          4 print(dict_c)
          5 
    TypeError: keywords must be strings

推荐进一步阅读:

20. 浅拷贝和深拷贝

在Python中,赋值语句(obj_b = obj_a)不会创建真实副本。 它仅使用相同的引用创建一个新变量。 因此,当你想制作可变对象(列表,字典)的实际副本并且想要在不影响原始对象的情况下修改副本时,必须格外小心。

对于“真实”副本,我们可以使用 copy 模块。 但是,对于复合/嵌套对象(例如嵌套列表或字典)和自定义对象,浅拷贝深拷贝之间存在重要区别:

  • 浅拷贝: 仅深一层。 它创建一个新的集合对象,并使用对嵌套对象的引用来填充它。 这意味着修改副本中嵌套对象的深度超过一层会影响原始对象。

  • 深拷贝: 完整的独立克隆。 它创建一个新的集合对象,然后递归地使用在原始对象中找到的嵌套对象的副本填充它。

赋值操作

这只会创建具有相同引用的新变量。 修改其中一个会影响另一个。

list_a = [1, 2, 3, 4, 5]
list_b = list_a

list_a[0] = -10
print(list_a)
print(list_b)
    [-10, 2, 3, 4, 5]
    [-10, 2, 3, 4, 5]

浅拷贝

一层深。 在级别1上进行修改不会影响其他列表。 使用 copy.copy() 或特定于对象的复制函数/复制构造函数。

import copy
list_a = [1, 2, 3, 4, 5]
list_b = copy.copy(list_a)

# 不会影响其他列表
list_b[0] = -10
print(list_a)
print(list_b)
    [1, 2, 3, 4, 5]
    [-10, 2, 3, 4, 5]

但是对于嵌套对象,在2级或更高级别上进行修改确实会影响其他对象!

import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.copy(list_a)

# 会影响其他列表!
list_a[0][0]= -10
print(list_a)
print(list_b)
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]

注意:你还可以使用以下内容来创建浅拷贝:

# 浅拷贝
list_b = list(list_a)
list_b = list_a[:]
list_b = list_a.copy()

深拷贝

完全独立的克隆。 使用 copy.deepcopy()

import copy
list_a = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
list_b = copy.deepcopy(list_a)

# 不影响其他
list_a[0][0]= -10
print(list_a)
print(list_b)
    [[-10, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
    [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]

自定义对象

你可以使用 copy 模块来获取自定义对象的浅拷贝或深拷贝。

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
                
# 只复制引用
p1 = Person('Alex', 27)
p2 = p1
p2.age = 28
print(p1.age)
print(p2.age)
    28
    28
# 浅拷贝
import copy
p1 = Person('Alex', 27)
p2 = copy.copy(p1)
p2.age = 28
print(p1.age)
print(p2.age)
    27
    28

现在让我们创建一个嵌套对象:

class Company:
    def __init__(self, boss, employee):
        self. boss = boss
        self.employee = employee

# 浅拷贝会影响嵌套对象
boss = Person('Jane', 55)
employee = Person('Joe', 28)
company = Company(boss, employee)

company_clone = copy.copy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)

print()
# 深拷贝不会影响嵌套对象
boss = Person('Jane', 55)
employee = Person('Joe', 28)
company = Company(boss, employee)
company_clone = copy.deepcopy(company)
company_clone.boss.age = 56
print(company.boss.age)
print(company_clone.boss.age)
    56
    56

    55
    56

21. 上下文管理器

上下文管理器是资源管理的绝佳工具。 它们使你可以在需要时精确地分配和释放资源。 一个著名的例子是 with open() 语句:

with open('notes.txt', 'w') as f:
    f.write('some todo...')

这将打开一个文件,并确保在程序执行离开with语句的上下文之后自动将其关闭。 它还处理异常,并确保即使在发生异常的情况下也能正确关闭文件。 在内部,上面的代码翻译成这样的东西:

f = open('notes.txt', 'w')
try:
    f.write('some todo...')
finally:
    f.close()

我们可以看到,使用上下文管理器和 with 语句更短,更简洁。

上下文管理器示例

  • 打开和关闭文件

  • 打开和关闭数据库连接

  • 获取和释放锁:

from threading import Lock
lock = Lock()

# 容易出错:
lock.acquire()
# 做一些操作
# 锁应始终释放!
lock.release()

# 更好:
with lock:
    # 做一些操作

将上下文管理器实现为类

为了支持我们自己的类的 with 语句,我们必须实现 __enter____exit__ 方法。 当执行进入 with 语句的上下文时,Python调用 __enter__。 在这里,应该获取资源并将其返回。 当执行再次离开上下文时,将调用 __exit__ 并释放资源。

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        print('exit')

with ManagedFile('notes.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    init notes.txt
    enter
    doing stuff...
    exit

处理异常

如果发生异常,Python将类型,值和回溯传递给 __exit__ 方法。 它可以在这里处理异常。 如果 __exit__ 方法返回的不是 True,则 with 语句将引发异常。

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        print('exc:', exc_type, exc_value)
        print('exit')

# 没有异常
with ManagedFile('notes.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
print('continuing...')

print()

# 异常触发,但是文件仍然能被关闭
with ManagedFile('notes2.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    f.do_something()
print('continuing...')
    init notes.txt
    enter
    doing stuff...
    exc: None None
    exit
    continuing...

    init notes2.txt
    enter
    doing stuff...
    exc: <class 'AttributeError'> '_io.TextIOWrapper' object has no attribute 'do_something'
    exit

    ---------------------------------------------------------------------------
    AttributeError                            Traceback (most recent call last)
    <ipython-input-24-ed1604efb530> in <module>
         27     print('doing stuff...')
         28     f.write('some todo...')
    ---> 29     f.do_something()
         30 print('continuing...')
    AttributeError: '_io.TextIOWrapper' object has no attribute 'do_something'

我们可以在 __exit__ 方法中处理异常并返回 True

class ManagedFile:
    def __init__(self, filename):
        print('init', filename)
        self.filename = filename

    def __enter__(self):
        print('enter')
        self.file = open(self.filename, 'w')
        return self.file

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if self.file:
            self.file.close()
        if exc_type is not None:
            print('Exception has been handled')
        print('exit')
        return True

with ManagedFile('notes2.txt') as f:
    print('doing stuff...')
    f.write('some todo...')
    f.do_something()
print('continuing...')
    init notes2.txt
    enter
    doing stuff...
    Exception has been handled
    exit
    continuing...

将上下文管理器实现为生成器

除了编写类,我们还可以编写一个生成器函数,并使用 contextlib.contextmanager 装饰器对其进行装饰。 然后,我们也可以使用 with 语句调用该函数。 对于这种方法,函数必须在 try 语句中 yield 资源,并且释放资源的 __exit__ 方法的所有内容现在都在相应的 finally 语句内。

from contextlib import contextmanager

@contextmanager
def open_managed_file(filename):
    f = open(filename, 'w')
    try:
        yield f
    finally:
        f.close()

with open_managed_file('notes.txt') as f:
    f.write('some todo...')

生成器首先获取资源。 然后,它暂时挂起其自己的执行并 产生 资源,以便调用者可以使用它。 当调用者离开 with 上下文时,生成器继续执行并释放 finally 语句中的资源。

Indices and tables