基础部分学习

字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
str = '10strbcd9   '
str1 = '10str bcd9'
str2 = ' 10str bcd9 '
str3 = '-a-b-c-'
print(str)
print(str[2:])
print(str.index('0'))
#print(str.index('aaa'))#报错
print(str.find('aaa'))
print(str.find('9'))
print(max(str))
print(str.split())
print(str1.split())
print(str2.split())
print(str3.split('-'))

见上述demo中 定义了一个字符串str 及字符串常用的函数。由引号(单引号、双引号、三对引号)包裹的字符、数字均为字符串。

函数名 作用 输出
[:] 切片操作 根据索引截取所需字符串
index() 查找字符串中是否存在某个元素 有则返回第一次出现的索引,无则报错
find() 查找字符串中是否存在某个元素 有则返回第一次出现的索引,无则返回 -1
split() 字符串去空操作 去除字符串中的空白符,并返还列表(若字符串中间存在空白符,则返回列表拥有两元素)
split() 字符串去空操作 也可制定去除的符号,如’-a-b-c-‘,则开头两端返回 [‘’, ‘a’, ‘b’, ‘c’, ‘’]

上述代码运行如下图所示image-20230505203731901

列表

列表是Python中最基础的数据结构a = ['a','b',['aaa']],其中被[]包裹的内容为列表中的元素,含有3个元素分别为a、 b和列表['aaa']。列表中可嵌套列表、元组、字典、集合等数据结构。

1
b = ['a','b',['aaa'],{'a':1,'b':'这是字段'},('这是元组',),{'a','b','这是集合'}]	

上述变量b也为列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
x = ['a','b',['aaa']]
y = ['a','b',['aaa'],{'a':1,'b':'这是字段'},('这是元组',),{'a','b','这是集合'}]
z = [1,2,3,4,5]
print(len(y)) #列表长度
print(max(z)) #比较列表中最大的数
print(min(z)) #比较列表中最小的函数
print(x.pop()) #删除最后一个元素,并返回该元素
print(x)
print(x.remove('a')) #删除指定元素,无返回值
del y[3] #指定列表索引删除元素
print(x)
print(y)
x.append('ccc') #列表末尾追加元素
print(x)
x.insert(0,"a") #列表索引位置追加元素
print(x)

上述demo运行如图所示,列表常用函数均已注释说明用法及区别

image-20230505210837035

元组

特性:与上述介绍的字符串一致,均为不可变的数据结构。其他处与列表类似,元组使用小括号,列表使用方括号。

元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。若元组只有一个元素需以('元组',)这种形式出现,单个元素后跟一个逗号,否则Python解释器默认其为 ('不是元组') 字符串类型

image-20230505211440638

由于元组为不可变类型的数据结构,所以其函数只有通用的几个

函数 作用 输出
min() 比较元组中元素大小,返回最小的元素 元组中最小的元素
max() 比较元组中元素大小,返回最大的元素 元组中最大的元素
tuple() 将列表、字符串转换为元组 列表元素不变输出元组,字符串单个字符为元素输出元组

测试demo如下

1
2
3
4
5
6
a = ['aaaa','b']
b = 'str'
print(type(tuple(a)))
print(type(tuple(b)))
print(tuple(a))
print(tuple(b))

image-20230505212214838

字典

字典是另一种可变容器模型,且可存储任意类型对象,且字典无序。其中字典的表现格式为{ key : value }的格式,其中key的值保持唯一性,若重复,则后续keyvalue值会覆盖之前的value ,如demoa = {'a':1,'a':2},打印结果为:{'a': 2},会直接覆盖掉第一个键名为a的键值对。

字典常见函数如下表:

函数名 作用 输出
pop() 根据Key名删除键值对 返回删除的value
del dict[keyname] 根据Key名删除键值对 无返回值
dict[‘key’] 若此前为引用的key名,则增加元素。引用过key名则修改value值
dict.get(‘keyname’) 根据key名找value 存在则输出value,反之输出None
dict.keys() 输出字典中所有key的列表集合对象 dict_keys
dict.values() 输出字典中所有value的列表集合对象 dict_values
dict.items() 输出字典中所有键值对的集合对象 dict_items

其中可将返回的 dict_keys dict_values dict_items三种类型对象看作一个迭代器,循环即可获得该值

演示demo如下:

1
2
3
4
5
6
7
8
9
10
11
a = { "a":1,"a":2,'c':3 }
print(a.keys())
print(a.values())
print(a.items())
print(type(a.keys()))
print(type(a.values()))
print(type(a.items()))
for key in a.keys():
print(key)
b = [(x,y) for x,y in a.items()]
print(b)

输出如图image-20230505215035255

集合

集合为无序且不可重复元素的数据结构,集合大多数使用情况为去重 set(具有重复元素的列表),无太多使用说明。

集合的表达方式为: {a,b,c}采用大括号的方式

常用函数补充

  • join()函数

    可使用该函数将列表的元素拼接为字符串

    1
    2
    a = ['a','b','c']
    str = ''.join(a)

    print(str)的结果为 abc,也可使用str = ' '.join(a),print(str)的打印结果为a b c

  • zip()函数

1
2
3
4
5
6
7
8
9
10
11
12
a = ['a','b','c']
x = ["x","y","z"]
m = zip(a,x) #zip对象返回的是一个zip类,浅显理解为:可迭代一次性对象
print(type(m)) # <class 'zip'>
i = m.__next__() #('a', 'x')
print(i)
i = m.__next__() #('b', 'y')
print(i)
i = m.__next__() #('c', 'z')
print(i)
i = m.__next__() # 打印三次之后,指针遍历完了zip对象的所有内容
print(i) # 访问一个迭代完的迭代器 -->> 报错 StopIteration
  • 列表推导式学习
1
2
3
4
5
6
7
a = [x  for x in range(10) if x % 9 == 0]
以上代码输出结果为 :[0, 9]
可将推导式变形为
b = []
for x in range(10):
if x % 9 ==0:
b.append(x)

优点 :简化代码量。

多线程/进程的学习

多线程-threading库简单学习

可通过函数定义方法的模式引用多线程

简单代码块引用

1
2
3
4
5
6
7
8
9
10
11
12
13
def print_name(name):
print(name,"start")
for i in range(3):
print(name,i)
sleep(1)
print(name,"end")
if __name__ == '__main__':
print("主线程start")
t1 = Thread(target=print_name,args=("t1",))
t2 = Thread(target=print_name,args=("t2",))
t1.start()
t2.start()
print("主线程end")

可通过重写Thread类引用多线程

简单代码块引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyThread(Thread):
def __init__(self,name):
Thread.__init__(self)
self.name = name
def run(self):
print(self.name, "start")
for i in range(3):
print(self.name, i)
sleep(1)
print(self.name, "end")
if __name__ == '__main__':
print("主线程start")
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
print("主线程end")

join方法

上述引用代码在执行时输出均为以下执行效果

1
2
3
4
5
6
7
8
9
10
11
主线程start
t1 start
t1 0
t2 start
t2 0
主线程end
t1 1
t2 1
t1 2
t2 2
t2t1 endend

如图image-20230416135221696

t1t2线程调用start()方法后,打印出了主线程end,但是t1 t2并没有执行完,还在继续执行输出。注意:并不是打印了主线程end 就是主线程结束了,只是主线程没有等t1 t2线程结束,仍然在继续运行主线程

如果需要 t1 t2执行完成在继续执行主线程,可以调用 join()方法。代码块如下

1
2
3
4
5
6
7
8
9
if __name__ == '__main__':
print("主线程start")
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
t1.join()
t2.join()
print("主线程end")

增加t1 t2调用join()方法后执行效果如下image-20230416140316313

会需要当两个子进程t1 t2结束后,主线程才会继续运行

守护进程

守护进程为 主进程结束后,守护进程也直接结束。使用方法为 线程名.daemon = True设置该线程对象的daemon属性为True如图

image-20230416140655433

当主线程打印 主线程end后 守护线程t1也就被结束了。

这个时候有一个需要明确的重要的点,如下代码块

1
2
3
4
5
6
7
8
if __name__ == '__main__':
print("主线程start")
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.daemon = True
t1.start()
t2.start()
print("主线程end")

两个子线程,但t1为守护线程,t2并没有守护线程的属性。所以该代码在运行时,执行效果如下image-20230416141043133

当主线程打印出 主线程end后,但守护进程t1仍然在继续执行。所以主线程的真正结束时间是: 所有非守护子线程运行结束后,才会结束主线程,同理守护子线程也会结束。因为非守护进程t2没有结束,所以主进程未结束,所以t1进程在主线程打印出主线程end后仍继续执行。

互斥锁及应用场景学习

无互斥锁demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class 账户:
def __init__(self,金额,户主):
self.金额 = 金额
self.户主 = 户主
class 取钱(Thread):
def __init__(self,预取金额,账户,取款人):
Thread.__init__(self)
self.预取金额 = 预取金额
self.账户 = 账户
self.取走金额 = 0
self.取款人 = 取款人
def run(self):
if self.预取金额 > self.账户.金额: #判断账户余额是否满足当前取钱的需求,避免金额为0
return
sleep(1)
self.账户.金额 -= self.预取金额
self.取走金额 += self.预取金额
print(f"{self.账户.户主}的账户余额为:{self.账户.金额}")
print(f"{self.账户.户主}的账户 共被{self.取款人}取走 {self.取走金额}")

定义了两个类 账户类中说明了账户金额和账户的户主,取钱类会判断当前取钱的取款人,预取金额,取钱的账户和取款人。直接用下列代码调用该demo时会存在一个问题

1
2
3
4
5
6
if __name__ == '__main__':
老王 = 账户(100,"老王")
t1 = 取钱(80,老王,"小明")
t2 = 取钱(80,老王,"小李")
t1.start()
t2.start()

image-20230416141853919

两个线程同时访问账户的金额资源,账户余额会变成-20.很显然在生产环境是很可能遭遇到该情况,所以就需要引进一个概念,对访问资源加上。调用threading模块的Lock()方法,对资源加锁处理

互斥锁使用方法为:

1
2
锁 = Lock() #初始化 Lock()
在需要对资源加锁处调用 `锁.acquire()`方法,当资源利用完后调用`锁.release()`对资源释放

原有demo加上锁后执行效果如下image-20230416142530797

当对该资源加锁后,避免了其他线程在使用中被人修改资源属性。

信号量的学习与场景

在互斥锁Lock()的学习使用中,可以限制资源只允许被一个线程访问,当该资源应该控制访问线程数量为一个区间时,互斥锁就无法满足需求了。引入新概念信号量 Semaphore()的学习。

demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def home(name): #定义一个只能容纳2人的房子
x.acquire()
print(f"当前{name}已经进入home中")
sleep(5)
print(f"当前{name}已走出房间")
print(datetime.now().strftime("%H:%M:%S"))
x.release()
if __name__ == '__main__':
x = Semaphore(2)
t_list = []
for i in range(10):
t = Thread(target=home,args=(f"{i}号玩家",))
t_list.append(t)
for t in t_list:
t.start()

运行该demo后输出效果如图image-20230417195228520

当该资源被两个线程访问时,其他线程均需等待。限制访问数量由x = Semaphore(2)决定。演示demo设置为2

生产者消费者queue学习

引入新的概念如图image-20230417195902754

将线程分为生产者、消费者两类。一类生产数据、一类消费数据。引入新的Pythonqueue

用法如下:

  1. 引入queue模块的Queue
  2. 初始化该类 q = Queue()
  3. 使用 q.put()对资源池进行添加资源的操作
  4. 使用 q.get()从资源池中取出资源

演示demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def 生产者():
name_list = ['张三', '李四', '王五']
if q.qsize() <2:
for name in name_list:
q.put(name)
else:
print("只能容纳两个人的大小")
def 消费者():
i = 3
while i >0:
name = q.get()
print(f'这是为{name}提供的食物')
i -= 1
if __name__ == '__main__':
q = Queue()
t1 = Thread(target=生产者)
t2 = Thread(target=消费者)
t1.start()
t2.start()

使用生产者与消费者的模式进行线程之间的通信。因为多线程特征为**共享堆、不共享栈**,可以把这句话理解为 共享全局资源、不共享函数内资源。demo中的列表name_list如果采用多线程的方式只属于调用该函数的线程独有,其他子线程无法访问。运行上述demo输出如图image-20230417201915634

但是如果不适用queue采用消费者的模式,直接让线程t2访问name_list资源的话如图image-20230417202156715

Pycharm直接报错,当然可以将name_list直接设置为全局变量,但如果场景为爬虫,url_list对应全局变量,存储需要爬取或爆破的url,但需要使用多线程爬访问爬取urlhtml页面,将生产者()函数定义为爬取,将生产者()函数定义为消费者,负责解析爬取回来的数据。这个时候就需要用到queue模式,让其中一个线程可以消费到另一个函数产生的值。

多进程multiprocessing库的学习

多线程与多进程的概念区别

多线程比多进程节省资源,但是部分场景下多进程比多线程效率高。

多进程的特点:进程与进程之间既不共享堆、也不共享栈。

函数定义模式与重写Process类模式调用多进程

函数定义模式调用多进程demo如下

1
2
3
4
5
6
7
8
9
10
def print_name(name):
print(f"{name}线程的pid为{os.getpid()}")
print(f"{name}线程的父id为{os.getppid()}")
if __name__ == '__main__':
p1 = Process(target=print_name,args=("张三",))
p2 = Process(target=print_name,args=("李四",))
p3 = Process(target=print_name,args=("王五",))
p1.start()
p2.start()
p3.start()

基本上看与多线程的使用方法一致,但通过引用os库的getpid()getppid()的方法输出子进程pid号与父进程pid号可以看出区别,该demo输出如下图

image-20230417203635252

三个子进程输出的父进程pid均为18336,但三个子进程的pid均不相同。子进程都是由父进程创建的。同理

重写Process()模式调用多进程demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from multiprocessing import Process
import os
class MyProcess(Process):
def __init__(self,name):
Process.__init__(self)
self.name = name
def run(self):
print(f"{self.name}线程的pid为{os.getpid()}")
print(f"{self.name}线程的父id为{os.getppid()}")
if __name__ == '__main__':
for name in ["张三","李四","王五"]:
p = MyProcess(name)
p.start()

输出如下image-20230417204649677

函数调用与**重写Process()**的多进程使用方法基本与多线程一致,均为创建进城后需使用start()方法启动进程,均可使用join()方法使主进程等待子进程运行完成后再继续下一步运行。

区别点:生产者消费者模式

多进程与多线程在我们初阶小白水平看来,只有一个区别堆和栈的资源均不共享

之前的queue在此处也无法实现两个子进程之间的通信了,需直接使用multiprocessing库中的Queue类进行多进程之间的通信。

**错误示范-引用queue模块中的Queue()**尝试多进程之间的通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from multiprocessing import Process
from queue import Queue
def 生产者():
name_list = ['张三', '李四', '王五']
if q.qsize() <2:
for name in name_list:
q.put(name)
else:
print("只能容纳两个人的大小")
def 消费者():
i = 3
while i >0:
name = q.get()
print(f'这是为{name}提供的食物')
i -= 1
if __name__ == '__main__':
q = Queue()
t1 = Process(target=生产者)
t2 = Process(target=消费者)
t1.start()
t2.start()

该错误示范运行时报错截图如下image-20230417205456762

正确demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from multiprocessing import Process,Queue
def 生产者(q):
name_list = ['张三', '李四', '王五']
if q.qsize() <2:
for name in name_list:
q.put(name)
else:
print("只能容纳两个人的大小")
def 消费者(q):
i = 3
while i >0:
name = q.get()
print(f'这是为{name}提供的食物')
i -= 1
if __name__ == '__main__':
q = Queue()
t1 = Process(target=生产者,args=(q,))
t2 = Process(target=消费者,args=(q,))
t1.start()
t2.start()

此处,与多进程区别在于,需要将 q = Queue()当作参数发送给进程,且函数需要接收该参数才可以。运行截图如下

image-20230417205950989

线程池、进程池的使用

优点

比手动定义多线程、多进程简单方便。线程/进程数量有限,不会无限制创建线程和进程数量。可获得函数执行后的结果

测试demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor,wait
import time
def print_num(num):
print(num)
time.sleep(2)
return num*10
if __name__ == '__main__':
start = time.time()
with ThreadPoolExecutor():
x = [ThreadPoolExecutor().submit(print_num,x) for x in range(1,11)]
wait(x)
print("线程池结束")
end = time.time()
print("耗费时间:",end-start)
for i in x :
print(i.result())
start2 = time.time()
with ProcessPoolExecutor():
x = [ProcessPoolExecutor().submit(print_num,x) for x in range(1,11)]
wait(x)
print("线程池结束")
end2 = time.time()
print("耗费时间:", end2 - start2)

运行后输出如图

image-20230505223445648

可直接获得函数的返回值,也可以利用 concurrent.futures中的wait()函数来等待线程池、进程池中的线程/进程结束完后在继续执行主进程/线程,等同于 多线程/进程中的join()方法。同时可以使用ThreadPoolExecutor/ProcessPoolExecutormap()方法直接传参为函数名,参数列表即可。但缺点为不支持直接调用wait()函数。