【Python基础2】

  • Post author:
  • Post category:python

1 Python的装饰器

装饰器的本质是一个闭包函数(ps:闭包函数《面试宝典》P98页)。

**作用:**让其他函数在不需要做任何代码变动的前提下增加额外的功能,提高了代码的复用性。

装饰器的主要功能:

  1. 引入日志;
  2. 函数执行时间统计
  3. 执行函数前预备处理
  4. 执行函数后的清理功能
  5. 缓存
print('*'*100)
def func_(func):
	def inner(*args):
		res = func(*args)#调用原函数
		return res*2
	return inner
@func_
def func1(a,b):
	return a+b
print(func1(2, 3))
>>>10

装饰器需要在函数定义前创建,否则无法被函数引用

2 Python的构造器是什么?

构造方法也叫构造器,是指当实例化一个对象时(创建一个对象)的时候,第一个被自动调用的方法。

python中构造函数的标识就是______init______(self),也叫初始化函数。

class Student:
	def __init__(self,name):
		self.age = 18
		self.heigt =172
		self.value = name

s =Student('Bob')
print(s.value)
>>>Bob

3 Python的生成器(Generator)是什么?

在python中,一边循环一边计算的机制,被称为生成器(Generator);生成器是实现迭代器的一种机制。

3.1创建生成器的方法

方法一:生成式创建

方法跟列表生成式相似,只是将[]换成了()

L = (i for i in range(10))#L是一个生成器
P = [i for i in range(10)]#P是一个列表

方法二:使用yield创建生成器

如果一个函数定义中使用了yield关键字,那么这个函数就不再是一个普通的函数,而是一个生成器,生成器是一个返回迭代器的函数。也可以简单的理解为生成器就是一个迭代器

#以yield实现斐波那契数列
def generator(n):
	a, b, c = 0, 1, 1
	while c<=n:
		a, b = a+b,a
		yield a
		c += 1
f = generator(10)
list = []
for i in f:
    list.append(i)
print(list)
>>>>[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
    

在调用生成器运行的过程中,每次遇到会暂停并保存当前所有的运行信息,返回yield的值,并在下次执行next()方法的时候从当前位置继续运行。

3.2生成器的**send()**方法:

generator.send(value)

作用:向生成器发送一个值,随后恢复执行。
value 参数是 send 方法向生成器发送的值,这个值会作为当前所在的 yield 表达式的结果
随后生成器恢复执行,直到下一个 yield,把它后面的值作为 send 方法的结果返回。
如果恢复执行后再也没有 yield 语句,生成器退出,并抛出 StopIteration 异常。
send带参数时必须在next方法执行后才能被执行

def gen():
	x = yield 1
	print('yield的结果改为:', x)
	y = yield '这是第一个send的结果'
	print('yield的结果改为:', y)
	yield '这是第二个send的结果'

g = gen()
print(next(g))
print(g.send(3))
print(g.send(4))
>>>1
>>>yield的结果改为: 3
>>>这是第一个send的结果
>>>yield的结果改为: 4
>>>这是第二个send的结果

当send方法的参数为None时,效果等同于next方法

4 Python的迭代器(Iterator)是什么?

迭代器是一个可以记住遍历的位置的对象。迭代器对象表示的是一个数据流,可以被看作为一个有序序列,但是不能提前得到它的长度,只有在需要返回下一个数据时它才会计算,可以被next()函数调用,并不断返回下一个数据,直到,没有数据时抛出StopIteration错误。

4.1 迭代器的特点:

1、迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。

2、迭代器有两个基本方法:

(1)iter():返回一个迭代器对象

(2)next():返回迭代器对象的下一个元素

3、可以直接作用于for循环的对象是可迭代的对象。如:字符串、列表、字典、集合、元组等。生成器也是一个可迭代对象。

判断一个对象是否可迭代:

通过collections模块的Iterable类型进行判断:

from collections import Iterable
print(isinstance('abc',Iterable))#str是否可迭代
>>>True

####4.2 使用类创建迭代器

如果把一个类作为一个迭代器使用,需要在类中实现______iter______()与______next______()两个方法。

iter():返回一个特殊的迭代器对象,这个迭代器对象实现了______next______()方法并通过StopIteration异常标识迭代的完成。

next():返回下一个迭代器对象。

class Iterations:
	def __iter__(self):
		self.num = 1
		return self

	def __next__(self):
		ne_num = self.num
		self.num += 1
		return ne_num
I = Iterations()
i = iter(I) #调用了__iter__方法
print(next(i))#调用了__next__
print(next(i))
print(next(i))
>>>1
>>>2
>>>3

使用iter()函数时自动调用了类里面的______iter______()方法

使用next()函数时自动调用了类里面的______next______()方法

5 isinstance()和type()的区别

isinstance()和type()是python中两个验证类型的函数。

二者的区别是:

1、isinstance()会认为子类是一种父类类型,考虑继承关系。返回的是布尔类型(True,False)

2、type()不会认为子类是一种父类类型,不考虑继承关系。返回的数据类型。

a = '哇哦,金色传说'
print(type(a))
print(isinstance(a,str))
>>> <class 'str'>
>>> True

isinstance(object,数据类型(父类));type(object)

6 Python中的浅拷贝,深拷贝和赋值之间的区别

赋值(=):使用“=”可以对一个变量进行赋值,赋值就是创建一个对象的一个新的引用赋值不会产生一个独立的对象,只是将原来的数据对象添加了一个新标签。

浅拷贝(copy.copy):使用copy.copy()可以进行对象的浅拷贝。浅拷贝只拷贝对象本身,不会拷贝其内部的嵌套对象。

浅拷贝可分为两种情况讨论:

(一)当浅拷贝对象为不可变对象(数值,字符串,元组)时和“赋值”的情况一样,依然指向同一个对象,Id不变

(二)当浅拷贝对象为可变对象(列表,集合,字典)时分为两种情况:

​ (1)拷贝对象中无嵌套复杂对象时,创建一个新的独立的对象。

​ (2)拷贝对象中有复杂嵌套对象时**,只拷贝对象本身**,对象内部的嵌套对象的引用依旧是原来的引用。改变任一个对象中嵌套对象的数值,另一个对象的嵌套对象数值也改变,但外层数据是互不影响的,创建一个新的“不怎么独立”的对象

深拷贝(copy.deepcopy):使用copy.deepcopy()可以进行对象的深拷贝。深拷贝将复制的对象完全再复制一遍作为一个新的独立对象。原对象与新对象互不影响。

import copy
list = [1,2,3,[4,5,6]]
l1 = copy.copy(list)
l2 = copy.deepcopy(list)
print(id(list))#2052343342088
print(id(l1))#2052343341896
print(id(l2))#2052343341960
print(l1)#[1, 2, 3, [4, 5, 6]]
print(l2)#[1, 2, 3, [4, 5, 6]]
list[3][0] = 6
print('*'*10)
print(f'浅拷贝{l1}')#浅拷贝[1, 2, 3, [6, 5, 6]]
print(f'深拷贝{l2}')#深拷贝[1, 2, 3, [4, 5, 6]]
print(f'原对象{list}')#原对象[1, 2, 3, [6, 5, 6]]

拷贝对象为不可变类型时,无论是浅拷贝还是深拷贝,都不会创建新的对象,效果跟“赋值”等同。

拷贝对象为可变对象时将创建一个新的对象

7 Python的内存管理。

7.1 引用计数:

使用引用计数来保持追踪内存中对象,所有对象都有引用计数。当对象被创建并被赋值给变量时,引用计数设置为1 ,可以使用sys.getrefcount(a)获取a引用计数。

对于不可变数据,解释器会在内存的不同部分共享内存,以便节约内存。

7.2 垃圾回收机制:

为解决内存泄漏问题,采用了对象引用计数,并基于引用计数实现垃圾回收。通过引用计数机制实现垃圾自动回收功能。

内存泄漏(循环引用):当两个对象a和b相互引用时,del可以减少a和b的引用计数,并销毁引用底层对象的名称,然而由于每个对象都包含一个对其他对象的引用,因此引用计数不会归零。,对象也不会被销毁,从而导致内存泄漏。为解决这一问题,解释器会定期执行循环检测器,搜索不可访问对象的循环并删除。

7.3 内存池机制:

垃圾回收机制将不用的内存放到内存池,而不是返回给操作系统。

1)Pymalloc机制。为了加速python的执行效率,python引入内存池机制,用于管理对小块内存的申请和释放。

2)python中将小于256个字符的对象都使用pymalloc实现的分配器。而大的对象使用系统的malloc

3)对于python对象,如整数、浮点数和list,都有其独立的私有内存池

P148 真题:

【真题262】当退出python时,是否释放全部内存。

【真题263】引用计数有哪些优缺点。

8 内置函数:

8.1 map()函数的作用:

map(function,iterable,...)

mao()参数:

  • function:函数
  • iterable:一个或者多个序列。

function以参数序列iterable中的每个元素为参数调用function函数,返回包含每次function函数返回值的新列表。

map函数在便py2中返回列表,py3中返回迭代器。

9系统编程

9.1 进程

对操作系统来说,一个任务就是一个进程,一个运行的程序就是一个进程。进程是系统进行资源分配和调度的一个独立单位,进程拥有自己独立的内存空间,进程之间不共享数据。

真正的并行执行任务只能在多核CPU上实现

9.1.1 多进程

Python可以用multiprocessing 包中的Process模块创建进程。

Process提供了以下方法:

方法/属性 说明
start() 启动进程,调用进程中的run()方法。
run() 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 。
join([timeout]) 主线程等待子线程终止。timeout为可选择超时时间;需要强调的是,p.join只能等待start开启的进程,而不能等待run开启的进程。需要在start()执行后运行。
daemon 默认值为False,如果设置为True,代表该进程为后台守护进程;当该进程的父进程终止时,该进程也随之终止;并且设置为True后,该进程不能创建子进程,设置该属性必须在start()之前
is_alive() 判断某进程是否存活,存活返回True,否则False。
pid 进程pid
exitcode 进程运行时为None,如果为-N,表示被信号N结束了。
authkey 进程身份验证,默认是由os.urandom()随机生成32字符的字符串。这个键的用途是设计涉及网络连接的底层进程间的通信提供安全性,这类连接只有在具有相同身份验证才能成功。
from multiprocessing import Process
import os

def Child_pro(a):
	c = a**2
	print(c)
	print('子进程执行',os.getpid())

if __name__ == '__main__':
	p = Process(target=Child_pro,args=(3,))
	p.start()
	print('执行主进程',os.getppid())

>>>
执行主进程 212
9
子进程执行 16752

注:

Process中的args参数接收的是一个元组对象kwargs接收的 是一个字典对象

  1. 父进程和子进程的启动是异步
    1. 同步: 两件事情,一件事情做完了再去做另外一个件事情
    2. 异步: 两件事情一起做
  2. 父进程只负责通知操作系统启动子进程 接下来的工作是由操作系统接手 父进程继续执行

9.1.2使用继承方法开启多进程

可以创建一个类来启动多进程,这个类继承于Process,并且要重写父类的______init__方法

from multiprocessing import Process
import os
class Child_pro(Process):
	def __init__(self,name):
		super().__init__()#重写父类的__init__方法
		self.name = name
	def run(self) -> None:
		print('我都名字是%s'%self.name)
		print('主进程是%s,子进程是%s'%(os.getppid(),os.getpid()))

if __name__ == '__main__':
	p1 = Child_pro('张三')
	p2 = Child_pro('李四')
	p3 = Child_pro('王老五')
	p1.start()
	p2.start()
	p3.start()
>>>
我都名字是张三
主进程是8260,子进程是16860
我都名字是李四
主进程是8260,子进程是9088
我都名字是王老五
主进程是8260,子进程是11992

9.1.3 join方法

使用Process对象的join方法,可以使主进程等待子进程结束后再结束进程,但是主进程和子进程是同时启动的,是异步进行的。可以理解为,主进程与子进程赛跑,主进程跑到终点前,给子进程放水,让子进程先进终点然后再走进终点。

from multiprocessing import Process
import os

def Child_pro(a):
	c = a**2
	print(c)
	print('子进程执行',os.getpid())

if __name__ == '__main__':
	p = Process(target=Child_pro,args=(3,))
	p.start()
	# p.join()
	print('执行主进程',os.getppid())
>>>
执行主进程 16464
9
子进程执行 4504
-------------------------------------------------------------
from multiprocessing import Process
import os

def Child_pro(a):
	c = a**2
	print(c)
	print('子进程执行',os.getpid())

if __name__ == '__main__':
	p = Process(target=Child_pro,args=(3,))
	p.start()
	p.join()
	print('执行主进程',os.getppid())
>>>
9
子进程执行 11932
执行主进程 16464

9.1.4互斥锁Lock

进程之间虽然不共享数据,但却是共用同一个文件系统,所以进程之间是可以访问同一个文件的。

因为进程之间是异步执行的,所以导致多个进程在访问一个文件时,会出现错乱。

Lock互斥锁可以将进程实现简单同步。lock对象有acquire(获得锁),release(释放锁)两个方法。对于每次只允许一个进程操作的任务,可以将其放置acquire(获得锁),release(释放锁)两个方法中间。即置于acquire和release之间的进程任务每次只能执行一个,执行完上一个进程的任务才开始执行下一个进程的任务。

import json
import time
from multiprocessing import Process, Lock
def search(name):
	dic = json.load(open('db.txt')) #db.txt是共有的资源文件
	time.sleep(1)
	print(f'{name}查到余票数{dic["count"]}')

def get(name):
	dic = json.load(open('db.txt'))
	dic['count'] += 1
	json.dump(dic,open('db.txt','w'))
	print('get方法执行')

def task(name,lock):
	lock.acquire()  # 加锁,执行完上一个进程的search,get方法以后才会执行下一个进程的
	search(name)
	get(name)
	lock.release()  # 释放锁

if __name__ == '__main__':
	lock = Lock()
	for i in range(3):
		name = f'<进程{i}>'
		p = Process(target=task, args=(name,lock))
		p.start()
 >>>
<进程0>查到余票数10
get方法执行
<进程2>查到余票数11
get方法执行
<进程1>查到余票数12
get方法执行

9.1.5 信号量(Semaphore)

信号量也是一把锁,对比互斥锁同一时间只有一个任务抢到锁去执行,信号量指定信号量为n.同一时间可以有n个任务拿到锁去执行

信号量就是互斥锁的变形,从只有一个人执行某段代码,变成了有一些人执行某段代码。

from multiprocessing import Process,Semaphore
import time
import random

def wc(name,sem):
	sem.acquire()
	print('%s进入厕所'%name)
	time.sleep(random.randint(1,5))
	print('%s走出厕所'%name)
	sem.release()

if __name__ == '__main__':
	sem = Semaphore(2) #只有两个坑位,一次最多只能进两个,出来一个才能再进一个。
	for i in ['张三','李四','王五','赵六','孙八']:
		p = Process(target=wc,args=(i,sem))
		p.start()
 >>>
张三进入厕所
李四进入厕所
李四走出厕所 
王五进入厕所
张三走出厕所
赵六进入厕所
王五走出厕所
孙八进入厕所
赵六走出厕所
孙八走出厕所

9.1.6 事件(Event)

一个进程的执行需要通过判断另一个进程的状态来决定是否执行。可以使用事件Event对象。

from multiprocessing import Process,Event
import time

def student(name,event):
	print('%s在听课'%name)
	event.wait()	#等待进程teacher结束以后再执行后面的任务
	print('%s在打球'%name)

def teacher(name,event):
	print('%s在上课'%name)
	time.sleep(3)
	event.set()

if __name__ == '__main__':
	event = Event()
	p = Process(target=student,args=('张三',event))
	p1 = Process(target=teacher,args=('李四',event))
	p.start()
	p1.start()
>>>
张三在听课
李四在上课
张三在打球

9.1.7 队列(Queue)

Python的multiprocessing模块下的Queue方法,可以使用队列来实现进程之间的同步执行。

import time
import random
from multiprocessing import Process,Queue

def producer(q, name, food):
	for i in range(4):
		time.sleep(random.randint(1,2))
		res = f'{food}{i}'
		q.put(res)
		print(f'{name}生产了{res}')
def consumer(q,name):
	while True:
		food  = q.get()
		if food is None:
			break
		time.sleep(random.randint(1,2))
		print(f'{name}吃了{food}')


if __name__ == '__main__':
	q = Queue()
	p1 = Process(target=producer,args=(q,q,'厨师1','食物'))
	c1 = Process(target=consumer,args=(q,'食客',))
	p1.start()
	c1.start()
	p1.join()
	q.put(None)
	c1.join()
	print('结束')
>>>>>
厨师1生产了食物0
厨师1生产了食物1
食客1吃了食物0
厨师1生产了食物2
食客1吃了食物1
厨师1生产了食物3
食客1吃了食物2
食客1吃了食物3
结束

9.2 进程池Pool

如果创建子进程不多,那么可以直接使用Process类创建,然后执行。但是如果需要成百上千的子进程,那么手动去创建就特别麻烦,而且进程的创建和销毁的代价也很大,而此时就需要使用进程池Pool。

进程池Pool类可以提供指定数量的进程供用户调用。当有新的请求提交到Pool中,如果进程池没有满,就会创建新的进程执行任务;如果进程池满了,则会等待进程池中的进程结束后再创建进程执行任务。

import os
from multiprocessing import Pool
import time

def taks(name):
	print(f'{name}进入',os.getpid())
	time.sleep(5)


if __name__ == '__main__':

	pool = Pool(3)
	for i in range(11):
		pool.apply_async(taks,args=(f'name{i}',))
	print('主进程结束',os.getpid())
	pool.close()
	pool.join()
	print('主进程结束',os.getpid())

进程池对象方法:

  • apply_async(func[,args[,kwargs]]):使用非阻塞方式调用func(并行执行),args为传递给函数的参数,接收一个元组类型。kwargs为传递给函数的关键字参数。

  • close():关闭Pool,使其不再接受新的任务;

  • terminate():不管任务是否完成,立即终止。

  • join():主进程阻塞,等待子进程的退出,必须在close或terminate之后使用。

9.3 线程

线程也是异步启动的

一个进程内部如果需要执行多个任务,就要同时运行多个子任务,这些子任务就称为线程(Thread)

线程是调度执行的最小单位,不能独立存在,必须依赖于进程。一个进程至少存在一个线程,叫主线程,多个线程之间是共享内存数据的(数据共享,共享全局变量

9.3.1 创建线程

python中提供了thread和threading库进行多线程创建,其中thread 相对比较基础,不容易控制,推荐使用threading,在python3中thread被重命名为_thread。

使用threading 库中的 Thread模块进行创建多线程

Thread对象的方法有:

方法 说明
run() 用以表示线程启动
start() 启动线程活动
join(time) 等待线程终止
isAlive() 检查线程是否仍在执行
getName() 返回线程名
setName() 设置线程名

threading 模块提供的方法有:

方法 说明
threading.currentThread() 返回当前的线程id
threading.enumerate() 返回一个包含正在运行的线程的list
threading.activeCount() 返回正在运行的线程数量

threading 模块创建线程:

from threading import Thread
import time

def threading_fist(name,):
	print(f'{name}登场')
	time.sleep(3)
	print(f'{name}撤退')

if __name__ == '__main__':
	for i in ['杨过','小龙女','郭靖','黄蓉']:
		people = Thread(target=threading_fist,args=(i,))
		people.start()
>>>>
杨过登场
小龙女登场
郭靖登场
黄蓉登场
杨过撤退
小龙女撤退
郭靖撤退
黄蓉撤退

_thread 模块创建线程:

语法:thread.start_new _thread(function,args[,kwargs])

function:线程函数;args:参数,必须tuple类型;kwargs:可选参数

import _thread
import time

def new_thread(name):
	count = 0
	while count<2:
		print(f'{name} say hi')
		time.sleep(3)
		print(f'{name} say bye')
		count+=1


try:
	_thread.start_new_thread(new_thread,('Thread1',))
	_thread.start_new_thread(new_thread,('Thread2',))
except SystemExit:
	print('Error')
while 1:
	pass
#线程结束后,依旧运行,不好退出循环,不易控制

9.3.2 使用继承方法开启线程

以类的形式开启线程,该类需要继承Thread类,并重写父类的______init______ 和run方法:

from threading import Thread

class class_thread(Thread):
	def __init__(self,name,id):
		super(class_thread,self).__init__()
		self.name = name
		self.id = id

	def run(self) -> None:
		print(f'我是{self.name},代号:{self.id}')

td1 = class_thread('张三','5858')
td2= class_thread('李四','2626')
td1.start()
td2.start()
>>>
我是张三,代号:5858
我是李四,代号:2626

9.3.3 实现线程的同步

线程的启动与进程一样是异步进行,但与进程不同的是,线程之间是共享一个进程内的资源的,异步启动也就意味着所有线程会在同一时间去访问同一个资源,从而导致数据混乱,因此需要对多个线程进行同步。

  • 使用互斥锁(Lock)实现同步
from threading import Thread,Lock
import time

count = 3
class class_thread(Thread):
	def __init__(self,name):
		super(class_thread,self).__init__()
		self.name = name

	def run(self) -> None:
		task(self.name)
def search(name):
	print(f'{name}查询票数{count}')

def get(name):
	global count
	# sta = count
	if count > 0:
		time.sleep(0.2)
		print(f'{name}购买1张票')
		# count = sta-1
		count -= 1
	else:
		print(f'{name}购票失败',count)

def task(name):
	# lock.acquire()
	search(name)
	get(name)
	# lock.release()
	
# lock = Lock()
td_lis = []
for name in ['钱大','刘二','张三','李四']:
	td = class_thread(name)
	# td = Thread(target=get,args=(name,))
	td_lis.append(td)
	td.start()
for td_ in td_lis:
	td_.join()

print('count',count)
>>>
钱大查询票数3
刘二查询票数3
张三查询票数3
李四查询票数3
钱大购买1张票
刘二购买1张票
张三购买1张票
李四购买1张票
count 0


from threading import Thread,Lock
import time

count = 3
class class_thread(Thread):
	def __init__(self,name):
		super(class_thread,self).__init__()
		self.name = name

	def run(self) -> None:
		task(self.name)
def search(name):
	print(f'{name}查询票数{count}')

def get(name):
	global count
	# sta = count
	if count > 0:
		time.sleep(0.2)
		print(f'{name}购买1张票')
		# count = sta-1
		count -= 1
	else:
		print(f'{name}购票失败',count)

def task(name):
	lock.acquire()
	search(name)
	get(name)
	lock.release()

lock = Lock()
td_lis = []
for name in ['钱大','刘二','张三','李四']:
	td = class_thread(name)
	# td = Thread(target=get,args=(name,))
	td_lis.append(td)
	td.start()
for td_ in td_lis:
	td_.join()

print('count',count)
>>>
钱大查询票数3
钱大购买1张票
刘二查询票数2
刘二购买1张票
张三查询票数1
张三购买1张票
李四查询票数0
李四购票失败 0
count 0



  • 使用队列(Queue)实现同步

Queue模块中的方法:

方法 说明 方法 说明
Queue.qsize() 返回队列大小; Queue.empty() 如果队列为空返回True
Queue.full() 如果队列满了返回True Queue.maxsize() 返回队列最大容量
Queue.get() 获取队列 Queue.put(item) 写入队列
Queue.join() 等待队列为空时再执行其他任务 Queue.task_done() 在完成一项工作后,
向已完成任务队列发送信号
Queue.get_nowait() 等同于Queue.get(False),不阻塞 Queue.put_nowait(item) 等同于Queue.put(item,False)
import time
from threading import Thread,
from queue import Queue

class myThread(Thread):
	def __init__(self, name1,name2, q):
		super(myThread, self).__init__()
		self.name1 = name1
		self.name2 = name2
		self.q = q
	def run(self) :
		task(self.name2,self.q)

def task(name1, q):
	food = q.get()
	print(f'{name1}吃了{food}')
	time.sleep(2)

que = Queue(1)

for i in range(6):
	user = myThread(f'name{i}',f'user{i}',que)
	user.start()

for id in range(6):#要在线程启动以后再添加队列数据,否则会阻塞
	food = f'food{id}'
	que.put(food)
	print(f'name{id}生产了{food}')
	time.sleep(3)
>>>
name0生产了food0
user0吃了food0
name1生产了food1
user1吃了food1
name2生产了food2
user2吃了food2
name3生产了food3
user3吃了food3
name4生产了food4
user4吃了food4
name5生产了food5
user5吃了food5




9.3.4 线程池

从Python3.2开始,标准库为我们提供了concurrent.futures模块,它提供了ThreadPoolExecutor(线程池)和ProcessPoolExecutor两个类,实现了对threading和multiprocessing的进一步抽象),不仅可以帮我们自动调度线程,还可以做到:

  • 主线程可以获取某一个线程(或者任务的)的状态,以及返回值。
  • 当一个线程完成的时候,主线程能够立即知道。
  • 让多线程和多进程的编码接口一致。
#线程池
from concurrent.futures import ThreadPoolExecutor
import time

def request_url(times):
	time.sleep(times)
	print('页面加载{}s成功'.format(times))
	return times

# max_workers表示最大运行的线程数。
thread_pool = ThreadPoolExecutor(max_workers=2)
#使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图)
# 注意submit()不是阻塞的,而是立即返回。
task1 = thread_pool.submit(request_url,3)#>>>>页面加载3s成功
task2 = thread_pool.submit(request_url,2)#>>>>页面加载2s成功

# done方法用于判定某个任务是否完成,不是阻塞的
print(task1.done())

# cancel方法用于取消某个任务,该任务没有放入线程池中才能取消成功
print(task2.cancel())#返回布尔值

#使用result()方法可以获取任务的返回值。查看内部代码,发现这个方法是阻塞的,等任务结束再执行
print(task1.result())
>>>
False
False
页面加载2s成功
页面加载3s成功
3
  • ThreadPoolExecutor构造实例的时候,传入max_workers参数来设置线程池中最多能同时运行的线程数目。

  • 使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图),注意submit()不是阻塞的,而是立即返回。

  • 通过submit函数返回的任务句柄,能够使用**done()**方法判断该任务是否结束。

  • 使用**cancel()**方法可以取消提交的任务,如果任务已经在线程池中运行了,就取消不了。

  • 使用**result()**方法可以获取任务的返回值。这个方法是阻塞的,等任务结束再执行

  • as_completed方法一次取出所有任务的结果,as_completed方法接收一个线程列表,返回一个生成器对象,任务没执行完会等待

    #线程池
    from concurrent.futures import ThreadPoolExecutor,as_completed
    import time
    
    def request_url(times):
    	time.sleep(times)
    	print('页面加载{}s成功'.format(times))
    	return times
    # max_workers表示最大运行的线程数。
    thread_pool = ThreadPoolExecutor(max_workers=2)
    #使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图)
    # 注意submit()不是阻塞的,而是立即返回。
    urls = [1,2,3]
    #执行的线程列表
    thread_list = [thread_pool.submit(request_url,times) for times in urls]
    
    #as_completed方法接收一个线程列表,返回一个生成器对象
    for thread in as_completed(thread_list):
    	data = thread.result()
    	print('执行{}s成功'.format(data))
    
  • wait方法可以让主线程阻塞,直到满足设定的要求。wait方法接收3个参数,等待的任务序列、超时时间以及等待条件。等待条件return_when默认为ALL_COMPLETED,表明要等待所有的任务都结束

    #线程池
    from concurrent.futures import ThreadPoolExecutor,wait,ALL_COMPLETED,FIRST_COMPLETED
    import time
    
    def request_url(times):
    	time.sleep(times)
    	print('页面加载{}s成功'.format(times))
    	return times
    
    # max_workers表示最大运行的线程数。
    thread_pool = ThreadPoolExecutor(max_workers=2)
    #使用submit函数来提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄(类似于文件、画图)
    # 注意submit()不是阻塞的,而是立即返回。
    urls = [1,2,3]
    #执行的线程列表
    thread_list = [thread_pool.submit(request_url,times) for times in urls]
    
    #wait方法接收3个参数,等待的任务序列、超时时间(timeout)以及等待条件(return_when)
    wait(thread_list,return_when=ALL_COMPLETED)
    print('主线程执行')
    >>>>
    页面加载1s成功
    页面加载2s成功
    页面加载3s成功
    主线程执行
    3.15.4 其他
    

9.4 其他

9.4.1 python中的GIL

GIL (Global Interpreter Lock)是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行Python解释器的时候会霸占Python解释器(加了一把锁即GIL),使该进程的其他线程无法运行,直到该线程结束。如果线程在运行过程遇到耗时操作,那么解释器锁解开,使其他线程运行。

9.4.2 线程或者进程中start和run

调用start,是先执行主进程再执行子进程,调用run则相当于正常的函数调用,将按程序的顺序执行。

9.4.3 并发和并行

并发:指两个或者多个事件在同一时间间隔内发生(事件在同一时间间隔内交替执行),线程是并发的

并行:指两个或者多个事件在同一时刻发生,即同时执行(同时做着不同的事)。进程是并行的

9.4.4 异步和同步

异步:多个任务之间没有先后顺序,可以同时被启动执行;一个任务可能在必要的时候会获取另一个同时在执行的任务的结果,这称之为回调。

同步:多个任务之间有先后顺序执行,一个任务执行完以后才能执行下一个任务。

10网络编程

10.1 网络编程基础

1、计算机网络的两个重要标识:

  • Mac地址:标记一台机器的物理地址 在你的网卡上 不会变的

  • IP地址:标记一台机器的逻辑地址 可变;在Windows终端输入ipconfig查看,在Linux或Mac系统下终端下,输入ifconfig查看

    • ipv4地址:32位的二进制数 以4位点分十进制来表示 192.168.1.1( 数值大小:0 – 255)
    • ipv6地址:6位的冒号分隔的16进制 128位的二进制
    • 内网IP:IP地址里面预留的地址
    192.168.0.0 ~ 192.168.255.255
    10.0.0.0 ~ 10.255.255.255
    172.16.0.0 ~ 172.31.255.255
    
    • 公网ip:需要自己申请,可被所有人访问

2、端口号: 端口号是计算机中的应用程序的一个整数数字标号,用来区分不同的应用程序。

  • 有效端口号:0~65535

  • 知名端口 0 ~ 1024 系统应用程序 和 服务占用

  • 动态端口:1025 ~ 65535 自己安装的程序/服务使用

3、arp协议: 通过ip地址获取到mac地址;

4、rarp协议: 与arp协议相反,通过mac地址获取到ip地址。

5、网络开发架构:

  • C/S架构 :客户端和服务器端 client /sever
  • B/S 架构: 浏览器 与 服务器

6、OSI(Open System Interconnect)开放式系统互连七层模型:**

OSI七层网络模型 TCP/IP四层概念模型 对应网络协议
应用层(Application) 应用层 HTTP,TFTP,FTP,NFS,WAIS,SMTP
表示层(Presentation) TeInet,Rlogin,SNMP,Gopher TeInet
会话层(Session) SMTP,DNS SMTP,DNS
传输层(Transport) 传输层 TCP,UDP
网络层(Network) 网络层 IP,ICMP,ARP,RARP,AKP,UUCP
数据链路层
(Data Link)
数据链路层 FDDI,Ethenet,Arpanet,PDN,SLIP,PPP
物理层(Physical) IEEE802.1A,IEEE802.0至IEEE802.11

10.2 TCP/UDP协议

通信协议是网络通信中的规则,分为TCP协议和UDP协议两种。

10.2.1 TCP协议:

定义:传输控制协议,是一种面向连接的,可靠的,基于字节流的传输层通信协议,TCP编程是基于IO流编程。

特点:效率低、数据传输比较安全。

TCP三次握手四次挥手:

  • **三次握手:**三次握手其实就是指建立一个TCP连接时,需要客户端和服务端总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传输做准备。

    刚开始客户端处于Closed的状态,服务端处于Listen状态。

    • 第一次握手:客户端给服务器发一个SYN报文,并指明客户端的初始序列号ISN,此时客户端处于SYN_SENT状态。

      首部的同部位SYN=1,初始序列号seq=x,SYN=1的报文不能携带数据,但要消耗一个序号。

    • 第二次握手:服务器收到客户端的SYN报文之后会以自己的SYN报文作为应答,并且也指定了自己的初始序列号ISN(s)。同时会把客户端的ISN+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务器处于SYN_RCVD的状态。

      在确认报文段中SYN=1,ACK=1,确认号ack=x+1,序列号seq=y。此报文中不可携带数据。

    • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

      确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。

    1.1 为什么需要三次握手,两次不行吗?
    弄清这个问题,我们需要先弄明白三次握手的目的是什么,能不能只用两次握手来达到同样的目的。

    第一次握手:客户端发送网络包,服务端收到了。
    这样服务端确认:客户端的发送能力、服务端的接收能力是正常的。
    第二次握手:服务端发包,客户端收到了。
    这样客户端确认:服务端的接收、发送能力,客户端的接收、发送能力是正常的。但是此时服务端还不能确认客户端的接受能力和自己的发送能力是否正常
    第三次握手:客户端发包,服务端收到了。
    这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。
    因此,需要三次握手才能确认双方的接收与发送能力是否正常。

    1.2 什么是半连接队列?
    服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。

    当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

    关于SYN-ACK 重传次数的问题:
    服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
    每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…

    详情链接:https://blog.csdn.net/hyg0811/article/details/102366854

  • 四次挥手四次挥手
    建立一个连接需要三次握手,而终止一个连接要经过四次挥手(也有将四次挥手叫做四次握手的)。这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。

    TCP 连接的拆除需要发送四个包,因此称为四次挥手(Four-way handshake),客户端或服务端均可主动发起挥手动作。

    刚开始双方都处于ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

    • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
      • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
      • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
      • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。

    收到一个FIN只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入TIME_WAIT是正常的,服务端通常执行被动关闭,不会进入TIME_WAIT状态。

    在socket编程中,任何一方执行close()操作即可产生挥手操作。

    2.1 挥手为什么需要四次?
    因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到服务端所有的报文都发送完了,才能发送FIN报文,因此不能一起发送。故需要四次挥手。

10.2.2 UDP协议:

定义:UDP是数据报协议,是一种面向无连接的传输层通信协议。UDP不提供可靠性的传输,它只是把应用程序传给 IP 层的数据报发送出去,但是并不能保证它们能到达目的地

特点:效率高,数据传输不安全,容易丢包

10.3 Socket套接字

python提供了两个级别的网络服务:

  • 低级别的网络服务支持基本的Socket,他提供了标准的BSD Sockets API,可以访问底层操作系统Socket接口的全部方法
  • 高级别的网络服务模块SocketServer,它提供了服务器中心类,可以简化网络服务器的开发。

Socket又称”套接字”,应用程序通常通过”套接字“向网络发出请求或者应答网络请求,使主机之间或者一台计算机上的进程之间可以通信。

使用socket模块创建一个服务端的步骤如下:

  1. 创建socket对象
  2. 使用bind函数绑定IP和端口号,bindl里面的参数需要是一个元组
  3. 调用listen函数开启监听,参数为最大运行连接数
  4. 调用accept函数建立客户端连接
  5. 调用send、recv进行数据的接收与发送。
import socket
#1、创建socket对象
# socket.AF_INET 在网络传输过程当中使用的地址类型 IPV4
# socket.SOCK_STREAM  使用TCP协议
# socket.SOCK_DGRAM  使用TCP协议
serversocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

# 2、绑定IP和端口号,bindl里面的参数需要是一个元组
serversocket.bind(('127.0.0.1',8889))

#3、开启监听,最大允许连接数为5
serversocket.listen(5)

#4、建立客户端连接
while True:
	conn,addr = serversocket.accept()
	msg = '欢迎使用本次连接'
    #5、使用send、recv发送和接收数据
	conn.send(msg.encode('utf-8'))
	data = conn.recv(1024)
	print(data.decode())


使用socket模块创建一个客户端的步骤如下:

  1. 创建socket对象
  2. 调用connect函数连接服务端,参数为一个元组。
  3. 调用send、recv函数进行数据的接收和发送
import socket
cilentsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
cilentsocket.connect((('127.0.0.1',8889)))
while True:
	data = cilentsocket.recv(1024)
	print(data.decode())
	cilentsocket.send('woshiAA'.encode('utf-8'))

10.4 IO多路复用

使用select模块检测IO操作,

服务端代码

#服务端
import socket
import select

# socket.AF_INET 在网络传输过程当中使用的地址类型 IPV4
# socket.SOCK_STREAM  使用TCP协议
serversocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
serversocket.setblocking(False)#非阻塞

# 绑定IP和端口号,bindl里面的参数需要是一个元组
serversocket.bind(('127.0.0.1',8889))
# 开启监听,最大允许连接数为5
serversocket.listen(5)
#服务请求列表
server_list = [serversocket,]
# 建立客户端连接
while True:
	#使用select检测是否有请求发送过来
	r_list,w_list,l_list = select.select(server_list, [], [])
	print(server_list)
	for i in r_list:
		if i is serversocket:
			conn,addr = i.accept()
			server_list.append(conn)
		else:
			try:
				data = i.recv(1024)
				print(data.decode())
			except Exception:
				print('shia')
				i.close()
				server_list.remove(i)
				continue
			msg = input(">>>>")
			i.send(msg.encode('utf-8'))



客户端代码

import socket
cilentsocket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
cilentsocket.connect((('127.0.0.1',8889)))
while True:
	msg = input('>>>>')
	cilentsocket.send(msg.encode('utf-8'))
	data = cilentsocket.recv(1024)
	print(data.decode())

10.5 TCP粘包现象

1、什么是粘包:几个或者多个不同的数据被合并在一起被服务端接收的现在称为粘包。

2、造成粘包的两种情况:

  • tcp协议的拆包机制,当发送缓冲区的大小大于网卡的MTU(网络上传输的最大数据包)时,大数据报会被拆分程多个小包传送,接收端,接收端一次性未完全接收所有的包,会与下一次传输的时候与下一次传输的数据一起接收过来。(大数据包被拆分成几次接收)

  • 面向流的通信特点合Nagle优化算法,当客户端连续发送多个小数据包时,会合并成一个较大的包进行发送,则客户端接收的数据会看到客户端发来的两个不同包的数据合并在了一个包里。(多个小数据包被合并成大数据包接收)

3、只有TCP协议会有粘包现在,UDP不会出现粘包

应用程序所看到的数据是一个整体,或说是一个流(stream),一条消息有多少字节对应用程序是不可见的,因此TCP协议是面向流的协议,这也是容易出现粘包问题的原因。(因为TCP是流式协议,不知道啥时候开始,啥时候结束)。而UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这一点和TCP是很不同的。怎样定义消息呢?可以认为对方一次性write/send的数据为一个消息,需要明白的是当对方send一条信息的时候,无论底层怎样分段分片,TCP协议层会把构成整条消息的数据段排序完成后才呈现在内核缓冲区。

所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的

4、解决粘包的方式:使用struct模块接收的数据打包成定长的数据

https://blog.csdn.net/qq_40199698/article/details/89070439?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165055051516780269818981%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165055051516780269818981&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-4-89070439.142v9control,157v4control&utm_term=python%E7%B2%98%E5%8C%85&spm=1018.2226.3001.4187

#服务端
import socket,subprocess,struct


server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

while True:
	conn, addr = server.accept()
	print('有人来连接了', addr)
	while True:
		try:
			rec = conn.recv(1024)
			obj = subprocess.Popen(rec.decode('utf-8'),shell=True,
								   stdout=subprocess.PIPE,#标准输出
								   stderr=subprocess.PIPE,#标准错误输出
								   )
			stderr = obj.stderr.read()
			stdout = obj.stdout.read()
			# msg_size = len(stdout+stdout)
			# header = struct.pack('i',msg_size)
			total_size = len(stdout + stderr)
            #打包成固定长度的数据
			header = struct.pack('i', total_size)
			#发送数据大小
			conn.send(header)
			#发送真实的数据
			conn.send(stdout)
			conn.send(stderr)
		except ConnectionResetError:
			break
	conn.close()


#客户端
import socket,struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

client.connect(('127.0.0.1',8080))

while True:
	msg = input(">>>")
	if not msg:
		continue
	client.send(msg.encode('utf-8'))
	#接收数据的大小
	header = client.recv(4)
	msg_size = struct.unpack('i',header)[0]
	#接收真实的数据
	totial_size = 0
	recv_data = b''
	while totial_size<msg_size:
		res = client.recv(1024)
		recv_data += res
		totial_size += len(res)
	# print(recv_data.decode('utf-8'))
	print(recv_data.decode('gbk'))

    
Python 中的struct 模块:
  • 定义:

    struct 是 Python 的内置模块,在使用 socket 通信的时候, 大多数据的传输都是以二进制流的形式的存在, 而 struct 模块就提供了一种机制, 该机制可以将某些特定的结构体类型打包成二进制流的字符串然后再网络传输,而接收端也应该可以通过某种机制进行解包还原出原始的结构体数据

  • struct 模块只能转换数字, 不能转换其他的数据类型

  • struct模块的基本功能:
    • 按指定格式将python数据转换为字节流数据;
    • 将字节流数据转换为指定的python数据类型;
    • 处理二进制文件数据;
    • 处理C语言中的结构体。
  • struct 的基本方法:

函数 return explain
pack(fmt,v1,v2…) string 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回.
unpack(fmt,v1,v2……) tuple 按照给定的格式(fmt)解析字节流,并返回解析结果
calcsize(fmt) size of fmt 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式
pack_into(fmt,buffer,offset,v1,v2…) None 按照给定的格式(fmt),将数据转换成字符串(字节流),并将字节流写入以offset开始的buffer中.(buffer为可写的缓冲区,可用array模块)
pack_from(fmt,buffer,offset) tuple 按照给定的格式(fmt)解析以offset开始的缓冲区,并返回解析结果

版权声明:本文为m0_58561801原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。