
Python-100-days
基础
常见内置函数、方法
- type():检测变量类型
- int():将一个数值或字符串转换成整数,可以指定进制。
- float():将一个字符串转换成浮点数
- str():将指定的对象转换成字符串形式,可以指定编码
- chr():将整数转换成该编码对应的字符串(一个字符)
- ord():将字符串(一个字符)转换成对应的编码(整数)
- len():计算字符串的长度
- isinstance(object, classinfo):用于检查一个对象是否是指定的类型或类的实例
- dict.items(): 以列表返回可遍历的(键, 值) 元组数组
for key,value in dict.items():#当两个参数时 返回的时元组内的k跟v
for i in dict.items():#当参数只有一个时 返回的是元组 - zip(): 用于将多个可迭代对象(例如列表、元组等)中对应的元素打包成元组,然后返回这些元组组成的迭代器。它能够按照最短的输入序列长度进行配对,当其中一个序列到达末尾时,迭代就停止。
取模与取余
通常取模运算也叫取余运算,它们返回的结果都是余数。
对于整数a,b来说,取模运算或者取余运算的方法要分如下两步进行:
- 第一步:求整数商c = a/b
- 第二步:计算模或者余数r = a - (c * b)
取模运算和取余运算唯一的差别在于第一步。
取模运算在计算整数商时,采用的是向负无穷大的方向取整。
取余运算在计算整数商时,采用的是向0方向取整。
举个栗子:
a = 4 , b = -3
第一步: 4/(-3) = -1.333…3..3(无限循环)
对于取模运算,得到的整数商将为-2(向负无穷取整)
对于取余运算,得到的整数商将为-1(向0取整)
取模结果为 c = -2
取余结果为 c = -1
第二步:
对于取模运算,r = 4 - ((-2) * (-3)) = 2
对于取余运算,r = 4 - ((-1) * (-3)) = 1
故,取模结果为2,取余结果为1。
总结
- 如果a,b符号相同,得到的整数商必然大于0,因此取模运算和取余运算取整的方向相同,因此得到的结果相同。
- 如果a,b符号不同,得到的整数商必然小于0,取模运算向负无穷方向取整,取余运算向0方向取整,两个方向不同,得到的结果必然不同。并且取模运算得到的结果的符号与b的符号相同,取余运算的结果的符号与a的结果相同。
函数的参数
我们给下面两个函数的参数都设定了默认值,这也就意味着如果在调用函数的时候如果没有传入对应参数的值时将使用该参数的默认值
所以在上面的代码中我们可以用各种不同的方式去调用add函数,这跟其他很多语言中函数重载的效果是一致的
1 | from random import randint |
其实上面的add函数还有更好的实现方案,因为我们可能会对0个或多个参数进行加法运算,而具体有多少个参数是由调用者来决定,我们作为函数的设计者对这一点是一无所知的
因此在不确定参数个数的时候,我们可以使用可变参数,代码如下所示。
1 | # 在参数名前面的*表示args是一个可变参数 |
字符串跟常见数据结构
字符串
Python为字符串类型提供了非常丰富的运算符,
我们可以使用+运算符来实现字符串的拼接,
可以使用*运算符来重复一个字符串的内容,
可以使用in和not in来判断一个字符串是否包含另外一个字符串(成员运算),
我们也可以用[]和[:]运算符从字符串取出某个字符或某些字符(切片运算)
代码如下所示。
1 | s1 = 'hello ' * 3 |
注意
print(str2[::-1]) # 654321cba
str2[::-1] 使用了 Python 中的切片(slice)操作来实现字符串的倒序输出。解释:
str2 是一个字符串。
[::] 是切片语法,它用于从一个序列(如字符串)中获取子序列。
第一个空位 :: 表示从头到尾,即包含了整个字符串。
最后一个数字 1 是步长(stride),它表示 从左到右 每次取一个字符。
当我们将步长设为 -1 时,就会 从右到左 每次取一个字符,因此,str2[::-1] 就实现了将字符串倒序输出的效果。举个例子,假如 str2 的值是 “Hello”,那么 str2[::-1] 就会返回 “olleH”,因为它是将 “Hello” 从右到左每次取一个字符。
列表切片亦是如此
索引是左开右闭的
在Python中,我们还可以通过一系列的方法来完成对字符串的处理,代码如下所示。
1 | str1 = 'hello, world!' |
f-string(格式化字符串字面量)
用法如下
1 | name = "Charlie" |
在 f-string 中,你可以使用类似于传统的字符串格式化方法来格式化数字。你可以在 f-string 中使用 :{format_spec} 语法,其中 format_spec 是一个格式规范字符串,用于指定如何格式化数字
1 | a = 3.1415 |
列表
- enumerate函数
enumerate() 是 Python 中的一个内置函数,用于将一个可遍历的数据对象(如列表、元组、字符串等)组合为一个索引序列,__同时返回元素和索引__。它的基本语法是:
enumerate(iterable, start=0)
其中:iterable
是可遍历的数据对象,如列表、元组、字符串等。start
是可选参数,表示起始的索引值,默认为0。enumerate()
返回一个可迭代的对象,每个元素是一个包含索引和对应元素的元组。
1 | list1 = [1, 3, 5, 7, 100] |
如何向列表中添加元素以及如何从列表中移除元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26list1 = [1, 3, 5, 7, 100]
# 添加元素
list1.append(200)
list1.insert(1, 400)
# 合并两个列表
# list1.extend([1000, 2000])
list1 += [1000, 2000]
print(list1) # [1, 400, 3, 5, 7, 100, 200, 1000, 2000]
print(len(list1)) # 9
# 先通过成员运算判断元素是否在列表中,如果存在就删除该元素
if 3 in list1:
list1.remove(3)
if 1234 in list1:
list1.remove(1234)
print(list1) # [1, 400, 5, 7, 100, 200, 1000, 2000]
# 从指定的位置删除元素
list1.pop(0)
list1.pop(len(list1) - 1)
print(list1) # [400, 5, 7, 100, 200, 1000]
# 清空列表元素
list1.clear()
print(list1) # []列表排序操作
1 | list1 = ['orange', 'apple', 'zoo', 'internationalization', 'blueberry'] |
列表生成式和 生成器
1 | f = [x for x in range(1, 10)] |
注意 :
[x for x in range(1, 11) if x % 2 == 0]
-> [2, 4, 6, 8, 10]
[x if x % 2 == 0 for x in range(1, 11)]
-> SyntaxError: invalid syntax
第一句之所以可以成功输出 是因为跟在for
后面的if
是一个筛选条件,不能带else
,不然如何筛选啊?🤬如果你把if
写在for
前, 那必然会报错, 因为写在前面就是个表达式了!必须带else
,它必须根据x
计算出一个结果。
可见,在一个列表生成式中,for
前面的if ... else
是表达式,而for
后面的if
是过滤条件,不能带else
。
还有一点:
两次for大的在前面奥
eg:values = [cell.value for row in scope for cell in row]
还有+
在切片的表示中,[start:stop] 表示从索引 start
开始,一直到索引 stop
之前的位置,也就是左闭右开.
还有切片是从左往右切 所以如果负数的话这样写 [-2, -1]
还有一点在切片中,如果不指定 stop
,则默认会一直取到列表的末尾(这里注意是结尾 包含了最后一个值)
元组
元组跟列表的不同之处是 元组的元素不能修改
1 | # 定义元组 |
如果可能,能用tuple代替list就尽量用tuple
坑
1 | tuple_1 = (1) |
这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1
集合
Python中的集合跟数学上的集合是一致的,不允许有重复元素,而且可以进行交集、并集、差集等运算。
1 | # 1. 创建和使用集合 |
字典
字典是另一种可变容器模型,Python中的字典跟我们生活中使用的字典是一样一样的,它可以存储任意类型对象,与列表、集合不同的是,字典的每个元素都是由一个键和一个值组成的“键值对”,键和值通过冒号分开。下面的代码演示了如何定义和使用字典。
1 | # 创建字典的字面量语法 |
文件读写
简单直接看菜鸟教程即可
主要就是
+ : 打开一个文件进行 更新 (可读可写,比如r是只读,r+时读写咯)
a : 追加 注意只有a
是追加 w跟r系列
文件指针都会放在开头,本来有内容会被覆盖
x : 也是写入,但是不一样的是如果文件已经存在会产生异常
读写JSON文件 一般只用几个
dump
: 将Python对象按照JSON格式序列化到文件中dumps
: 将Python对象处理成JSON格式的字符串load
: 将文件中的JSON数据反序列化成对象loads
: 将字符串的内容反序列化成Python对象
这里出现了两个概念,一个叫序列化,一个叫反序列化。自由的百科全书维基百科上对这两个概念是这样解释的:“序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换为可以存储或传输的形式,这样在需要的时候能够恢复到原先的状态,而且通过序列化的数据重新获取字节时,可以利用这些字节来产生原始对象的副本(拷贝)。与这个过程相反的动作,即从一系列字节中提取数据结构的操作,就是反序列化(deserialization)”。
random模块
函数 | 描述 |
---|---|
random() |
返回范围在 [0.0, 1.0) 之间的随机浮点数。 |
randint(a, b) |
返回一个整数 N,使得 a <= N <= b 。 |
uniform(a, b) |
返回范围在 [a, b) 之间的随机浮点数 区间可以不是整数 |
randrange(start, stop, step) |
从指定范围内随机选择一个元素。 |
choice(seq) |
从非空序列中随机选择一个元素。 |
shuffle(seq) |
原地随机打乱序列的元素顺序。 |
sample(population, k) |
从总体中选择 k 个唯一元素,并返回一个列表。 |
seed([x]) |
使用给定的种子值初始化随机数生成器。 |
这些函数是 Python 中 random
模块的一部分,通常用于生成随机数或以随机方式操作序列。
match case
1 | age = 15 |
菜鸟教程
能用match case
尽量不用if elif else
面向对象编程
定义: 类是对象的蓝图和模板,而对象是类的实例
定义类
1 | class Student: |
访问权限
对于上面的代码,有C++、Java、C#等编程经验的程序员可能会问,我们给Student对象绑定的name和age属性到底具有怎样的访问权限(也称为可见性)。因为在很多面向对象编程语言中,我们通常会将对象的属性设置为私有的(private)或受保护的(protected),简单的说就是不允许外界访问,而对象的方法通常都是公开的(public),因为公开的方法就是对象能够接受的消息。在Python中,属性和方法的访问权限只有两种,也就是公开的和私有的,如果希望属性是私有的,在给属性命名时可以用两个下划线作为开头,下面的代码可以验证这一点。
1 | class Test: |
但是,Python并没有从语法上严格保证私有属性或方法的私密性,它只是给私有的属性和方法换了一个名字来妨碍对它们的访问,事实上如果你知道更换名字的规则仍然可以访问到它们,下面的代码就可以验证这一点。之所以这样设定,可以用这样一句名言加以解释,就是”We are all consenting adults here”。因为绝大多数程序员都认为开放比封闭要好,而且程序员要自己为自己的行为负责。
在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问(后面会讲到)。所以大多数Python程序员会遵循一种命名惯例就是让属性名以单下划线开头来表示属性是受保护的,本类之外的代码在访问这样的属性时应该要保持慎重。这种做法并不是语法上的规则,单下划线开头的属性和方法外界仍然是可以访问的,所以更多的时候它是一种暗示或隐喻
1 | class Test: |
@property装饰器
之前我们讨论过Python中属性和方法访问权限的问题,虽然我们不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效。我们之前的建议是将属性命名以单下划线开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的getter(访问器)和setter(修改器)方法进行对应的操作。如果要做到这点,就可以考虑使用@property包装器来包装getter和setter方法,使得对属性的访问既安全又方便,代码如下所示。
1 | class Person: |
以上内容属实没看懂什么意思 以下为理解内容
property
装饰器是Python中用于管理类属性的一种特殊方法。它允许你 将一个方法标记为属性 ,从而可以 像访问普通属性一样访问该方法 。
通常情况下,我们定义一个类的属性时,会使用类的实例变量来存储数据。然而,有时我们可能需要在获取或者设置属性值时 __执行一些额外的逻辑__,例如进行数据校验或转换。
property
装饰器允许你在访问属性时 自定义其行为。 它 实际上将一个方法“伪装”成一个属性,当你访问这个属性时,实际上会调用相应的方法。
举例来说:
假设你有一个类,表示一个人的信息,其中包括了名字和姓氏:
1 | class Person: |
现在,你想要获取这个人的全名,你可以使用下面的方法:
1 | person = Person('John', 'Doe') |
但是如果你想让获取全名的方式更简单一些,就可以使用property装饰器。它让你可以像访问属性一样获取全名,而不是调用一个方法:
1 | class Person: |
现在你可以这样获取全名:
1 | person = Person('John', 'Doe') |
这就像在获取一个普通的属性一样,但实际上在后台调用了一个方法。
另外,如果你想要设置全名,你可以使用@full_name.setter
装饰器来定义一个方法,使得你可以直接给full_name
赋值:
1 | class Person: |
现在你可以这样设置全名:
1 | person = Person('John', 'Doe') |
slots魔法
__slots__
是Python中一个特殊的类属性,它允许你限制一个类实例能拥有的属性。
通常情况下,Python的类实例可以动态地添加新的属性。这意味着你可以在实例化之后随时为其添加新的属性。例如:
1 | class Person: |
在上面的例子中,我们创建了一个Person
类的实例,并随时为其添加了name
和age
两个属性。
但是,有时候我们希望限制一个类实例可以拥有的属性,这时就可以使用__slots__
。
以下是一个使用__slots__
的例子:
1 | class Person: |
在这个例子中,__slots__
被设置为一个包含了'name'
和'age'
的元组。这意味着Person
类的实例只能拥有这两个属性,任何尝试添加其他属性都会引发AttributeError
。
使用__slots__
的好处是可以减少实例的内存消耗,因为Python不再需要为每个实例维护一个字典来存储属性。相反,它直接为属性分配一个固定的槽位,提高了访问速度。
需要注意的是,__slots__
只对当前类的实例起作用,不会影响子类。如果子类也定义了__slots__
,那么它会覆盖父类的定义。
综上就是
你初始化属性时 如果不想让人再访问某些属性 可以用self.__name
定义属性 这样在访问时直接报错没这个属性 (但是也可以用特殊手段访问,比如用_Person__name
在该属性前加上下划线以及类的名称)
一般情况不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效, 就比如让你定义name
你直接person.name = 18
你叫18是吧🤬
建议是将属性命名以单下划线开头,通过这种方式来 __暗示属性是受保护的__,不建议外界直接访问,那么如果想访问属性可以通过属性的getter(访问器)和setter(修改器)方法进行对应的操作。
所以我们需要在自定义属性时 __执行一些额外的逻辑__,例如进行数据校验或转换时,就用property
property
装饰器允许你在访问属性时 自定义其行为。 它 实际上将一个方法“伪装”成一个属性,当你访问这个属性时,实际上会调用相应的方法。
需要说的一点是@property
会将一个方法转换为一个只读属性,相当于定义了一个getter
方法
在修改内容时用修改器也就是setter
方法,比如有人在修改name
时, 你可以做一些手脚
,还有就是setter包装的函数不需要 return
!__slots__
就是限制一个类实例可以拥有的属性,他是一个 元组 类型!
ps: 如果想在初始化时就进行检查,可以在__init__方法中添加相应的检查逻辑
以下代码演示:
1 | class Person: |
静态方法和类方法
静态方法比较容易理解如下
静态方法:
静态方法就像一个独立的工具,它并不依赖于特定的实例或类属性。你可以把它想象成 一个独立的函数,只是碰巧它在类的内部定义了而已
举个例子,如果我们有一个类代表一个工具箱,其中有一个静态方法叫做
hammer()
,那么你可以 直接调用这个方法,不需要先实例化一个工具箱
1 | from math import sqrt |
注意:
要在类的实例方法中调用类的静态方法,你可以使用 self
来访问类的成员,包括静态方法。在这种情况下,self
实际上是指向类的实例本身。
虽然静态方法与类的实例无关,但它们属于类的命名空间。通过 self
,你可以从实例的命名空间中访问静态方法。这是一种在实例方法中访问类级别成员(包括静态方法)的通用方式。
类方法有点难度 :
类方法的第一个参数约定名为cls,它代表的是当前 类相关的信息 的对象(类本身也是一个对象,有的地方也称之为类的元数据对象),通过这个参数我们可以 获取和类相关的信息并且可以创建出类的对象
最后一句
- 获取和类相关的信息
- 并且可以创建出类的对象!
类方法:
类方法就像一个能够访问工具箱本身的工具,但它并不依赖于具体的工具箱实例。你可以把它想象成一个能够处理工具箱的工具,但它并不关心具体是哪一个工具箱。
举个例子,如果我们有一个类代表所有工具箱,其中有一个类方法叫做
count_tools()
,它可以统计所有工具箱中的工具数量,而不需要先实例化一个具体的工具箱。
所以,静态方法和类方法的区别在于它们处理的数据和上下文不同。静态方法更像一个独立的工具函数,而类方法更像是能够操作整个类的工具
1 | from time import time, localtime, sleep |
继承
刚才我们提到了,可以在已有类的基础上创建新类,这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则。下面我们先看一个继承的例子。
关于 super().__init__()
可以参考这篇文章
1 | class Person(object): |
多态
子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写(override)。通过方法重写我们可以让父类的同一个行为在子类中拥有不同的实现版本,当我们调用这个经过子类重写的方法时,不同的子类对象会表现出不同的行为,这个就是多态(poly-morphism)。
1 | from abc import ABC, abstractmethod |
在上面的代码中,我们将Pet类处理成了一个抽象类,所谓抽象类就是不能够创建对象的类,__这种类的存在就是专门为了让其他类去继承它__。Python从语法层面并没有像Java或C#那样提供对抽象类的支持,但是我们可以通过abc模块的ABCMeta元类和abstractmethod包装器来达到抽象类的效果,如果一个类中存在抽象方法那么这个类就不能够实例化(创建对象)。上面的代码中,Dog和Cat两个子类分别对Pet类中的make_voice抽象方法进行了重写并给出了不同的实现版本,当我们在main函数中调用该方法时,这个方法就表现出了多态行为(同样的方法做了不同的事情)。
我对抽象方法的理解
抽象方法就像是一个接口规定了一些要做的事情,但它本身并不知道如何去实现这些事情。抽象方法就好比是一份合同,规定了某个类必须提供某个方法,但不规定具体的实现。
举个例子,想象你是一名游戏开发者,你设计了一个游戏中的角色类,每个角色都必须有一个叫做 攻击
的方法。但是,你不知道具体每个角色如何实现攻击,因为不同角色可能有不同的攻击方式。
这时,你可以使用抽象方法来定义这个攻击方法,但不提供具体的实现。每个具体的角色(比如战士、法师、弓手等)都必须按照合同,提供自己独特的 攻击
实现。
在这个场景下,抽象方法就是合同中的规定,而具体的角色类就是按照这个规定提供实现。这样确保了所有的角色都有攻击方法,但实现方式可以因角色而异。
上面的代码中,无论是猫还是狗都一个共同的方法 就是make_voice
但是每个动物类对make_voice
的实现方式又不同,猫有猫的叫法狗有狗的叫法,但是都会叫,这就是多态! 用相同的方法处理不同类型的动物Pet
是一个抽象类, 其中包含一个抽象方法make_voice
, 猫狗都是pet,都继承了动物类,是动物的子类,都强制实现了make_voice
方法, 抽象方法强制子类提供自己的实现,确保了在众多子类中都具有一致的接口,并且如果子类没有提供抽象方法的实现,或者在实例化时调用了抽象方法,将引发TypeError
。
并发编程
关于并发编程建议好好读读 《Python Parallel Programming Cookbook》
多线程
实现方式
多线程实现方式有两种,我喜欢用自定义线程类
切记 要理解:
- t.start() : 当前线程准备就绪(等待CPU调度,具体时间是由CPU来决定)
- t.join() :等待当前线程的任务 执行完毕 后再向下继续执行
- t.setDaemon :守护线程(必须放在start之前)
- t.setDaemon(True),设置为守护线程,主线程执行完毕后,子线程也自动关闭。
- t.setDaemon(False),设置为非守护线程,主线程等待子线程,子线程执行完毕后,主线程才结束。(默认)
1 | from threading import Thread |
线程锁
Lock:同步锁
RLock:递归锁
在多线程编程中,Lock
和RLock
(可重入锁)是用于控制多个线程对共享资源访问的工具。这两者都是互斥锁的变种,用于确保在任何给定时间内只有一个线程可以访问共享资源,以防止数据竞争和不一致的状态。
Lock
作用: 用于在任一时刻只允许一个线程访问共享资源。
特性: 一旦一个线程获取了
Lock
,其他线程就必须等待该线程释放锁,才能继续执行。例子: 如果一个线程获得了某个资源的
Lock
,那么其他线程就不能获得同一个Lock
,直到这个线程释放了Lock
。
1 | import threading |
RLock
作用: 允许同一个线程多次获得同一个锁,避免了死锁情况。
特性: 当一个线程获得了
RLock
后,它可以多次调用acquire
,而不会阻塞,但必须相应地调用相同次数的release
才能释放锁。例子: 在递归函数或嵌套调用中,
RLock
可以确保同一线程能够多次获取锁,而不会引起死锁。
1 | import threading |
总体而言,Lock
和RLock
都是为了保护共享资源而存在的,通过防止多个线程同时访问临界区,确保线程安全。RLock
相对于Lock
的特殊之处在于它允许同一个线程多次获取锁,这在某些复杂的程序结构中可能很有用。
死锁
死锁是多线程或多进程编程中常见的问题之一,它发生在两个或多个线程(或进程)互相等待对方释放资源时。简而言之,死锁是一种资源争用的情况,其中线程(或进程)之间相互等待对方释放资源,导致它们都无法继续执行。
以下是引起死锁的一般情况:
互斥条件: 资源被设计成一次只能被一个线程或进程占用,这就是互斥条件的基础。如果一个线程获得了一个资源,其他线程必须等待。
占有且等待: 一个线程在持有某个资源的同时,又请求获取其他资源,并且不释放已经占有的资源。如果所有线程都采用这种策略,就可能导致死锁。
无抢占: 已经获得资源的线程不能被强制释放资源,只能在自愿的情况下释放。如果一个线程在等待其他资源时不释放已经占有的资源,其他线程可能就无法继续执行,形成死锁。
循环等待: 一组线程形成一个循环等待链,每个线程都在等待下一个线程持有的资源,最终导致整个系统陷入死锁。
一个简单的例子可以说明死锁的发生。假设有两个线程 A 和 B,以如下方式请求资源:
- A获得资源1,等待资源2
- B获得资源2,等待资源1
如果A和B同时开始执行,并且A先获得资源1,B先获得资源2,然后它们会陷入等待对方释放资源的状态,而无法继续执行。这就是一个简单的死锁场景。
避免死锁的一般方法包括按照一定的顺序获取资源、使用超时机制、以及使用避免死锁的算法等。理解死锁的原因和常见模式是预防和解决死锁问题的关键。
线程池
事实上,线程的创建和释放都会带来较大的开销,频繁的创建和释放线程通常都不是很好的选择。利用线程池,可以提前准备好若干个线程,在使用的过程中不需要再通过自定义的代码创建和释放线程,而是直接复用线程池中的线程。Python 内置的concurrent.futures模块提供了对线程池的支持,代码如下所示。
线程池是一种并发编程的技术,它可以有效地管理和复用线程,提高程序的性能和效率。在Python中,线程池通常使用concurrent.futures
模块来实现。以下是线程池的一些基本概念和细节:
线程池的创建:
在Python中,你可以使用concurrent.futures.ThreadPoolExecutor
来创建线程池。以下是一个简单的创建线程池的例子:1
2
3
4
5
6from concurrent.futures import ThreadPoolExecutor
# 创建一个拥有4个线程的线程池
with ThreadPoolExecutor(max_workers=4) as executor:
# 在这里执行线程池中的任务
# ...任务提交:
future 模式,更加强大 submit每次传的是一个!!! 所以需要可以时用时加 不用像map一样 一次放完,结果有两种遍历方式看代码 注意如果用as_completed顺序是不定的
你可以通过
submit
方法将任务提交给线程池。submit
方法返回一个Future
对象,你可以用来跟踪任务的状态和获取结果。1
2
3with ThreadPoolExecutor(max_workers=4) as executor:
future = executor.submit(my_function, arg1, arg2)
# 在线程池中提交一个任务,线程池中如果有空闲线程,则分配一个线程去执行,执行完毕后再将线程交还给线程池;如果没有空闲线程,则等待。可以通过 future.result() 获取任务的结果批量提交
1
2
3
4
5
6
7
8with ThreadPoolExecutor() as pool:
futures = [ pool.submit(craw, url) for url in urls ]
for future in futures: # 第一种 按照url的顺序 依次获取future future.result()
print(future.result())
for future in as_completed(futures): # 第二种 不管哪个任务 先进行完了 就会先进行返回
print(future.result())
批量提交任务:
你还可以使用map
方法一次性提交多个任务,类似于map
函数, 注意map的结果和入参是顺序对应的1
2
3with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(my_function, [arg1, arg2, arg3])
# results 是一个迭代器,包含每个任务的结果Future对象:
Future
对象代表一个异步操作的结果。你可以通过它来检查任务是否完成、获取结果、取消任务等。1
2
3
4
5with ThreadPoolExecutor(max_workers=4) as executor:
future = executor.submit(my_function, arg1, arg2)
# 检查任务是否完成
if future.done():
result = future.result()异常处理:
通过add_done_callback
方法,你可以指定一个回调函数,在任务完成时执行。这可以用于处理任务中的异常。1
2
3
4
5
6
7
8
9def callback(future):
try:
result = future.result()
except Exception as e:
print(f"Exception: {e}")
with ThreadPoolExecutor(max_workers=4) as executor:
future = executor.submit(my_function, arg1, arg2)
future.add_done_callback(callback)callback
函数接受的future
参数是通过add_done_callback
方法自动传递的。这个方法会在异步任务完成时被调用,将concurrent.futures.Future
对象作为参数传递给注册的回调函数。所以,callback
函数的参数future
实际上是对应异步任务的Future
对象,它包含了异步任务的状态和结果信息。通过future.result()
,可以获取异步任务的执行结果,即下载的响应对象,进行后续的处理。回调函数:
当你使用
future.add_done_callback(callback)
时,你告诉程序:“当这个future
完成时,请调用callback
函数。”然而,有时候你的
callback
函数可能需要除了future
之外的额外信息或参数。这时,你可以通过以下方式传递额外的参数:- 使用lambda函数:
你可以创建一个小型的匿名函数(
lambda
函数),这个函数接受future
作为参数,然后调用你的callback
函数并传递额外的参数。这是一种简洁的方式。1
2extra_param = "some_value"
future.add_done_callback(lambda future: callback(future, extra_param))- 使用functools.partial:
如果你觉得
lambda
函数语法有点冗长,你可以使用functools.partial
,它可以创建一个新的函数,固定住部分参数。在这里,我们将callback
与额外的参数绑定在一起,然后将这个新的函数传递给add_done_callback
。1
2
3
4
5from functools import partial
extra_param = "some_value"
callback_with_params = partial(callback, extra_param)
future.add_done_callback(callback_with_params)这两种方法都能达到同样的目的,即在
callback
函数被调用时,确保额外的参数也能被传递给它。选择其中一种方法通常取决于你个人的偏好和代码的风格。PS: 第二种方法按关键词参数传递 🤬 后面写代码时发现的小问题, 不然callback_with_params会把extra_param当作future
即callback_with_params = partial(callback, param = extra_param)
固定多个参数也是如此关闭线程池:
在使用完线程池后,最好通过shutdown
方法来关闭它。这将确保所有的线程都被正确终止。
当然如果使用上下文管理方式不用1
2
3
4
5
6
7
8
9
10def task(video_url):
print("开始执行任务", video_url)
time.sleep(5)
pool = ThreadPoolExecutor(10)
url_list = ["www.xxxx-{}.com".format(i) for i in range(300)]
for url in url_list:
pool.submit(task, url)
print("执行中...")
pool.shutdown(True) # 等待线程池中的任务执行完毕后,在继续执行
print('继续往下走')
请注意,尽管线程池可以提高并发性,但在某些情况下,由于Python的全局解释器锁(GIL),线程并不能充分利用多核处理器。如果你需要更好的并行性能,可以考虑使用concurrent.futures.ProcessPoolExecutor
,它使用多个进程而不是线程。
示例
1 | import requests |