简介
- 上一篇介绍了python的迭代器和协程等知识点
- 这里重点说一下GIL、方法解析、类属性等问题
- Python最近又版本更新了,可以关注一下出了哪些新特性
- 任何一门语言都是一个完整的体系,但因为不同的特性,擅长处理的问题不一样
- 之前说过语言分两大块,编译型和解释型;如果能深入研究,从底层硬件开始了解,就又是一番风景
GIL锁
- 之前在协程中提出了GIL问题,可以认为这是当时草率遗留的问题
- 什么是GIL?先跑一下这段代码:
import threading def test(): while True: # 子线程死循环 pass t = threading.Thread(target = test) t.start() while True: # 主线程死循环 pass
- 观察CPU状态,结果如下:
- 两个线程利用率相加占满了一个内核;
- 变为多进程看一下结果
import multiprocessing def process(): while True: pass p = multiprocessing.Process(target = process) p.start() while True: pass
- 可以发现,两个进程分别占用了两个核
- 一般情况下,进程是资源分配的基本单位,线程是处理器调度的基本单位
- 一个进程拥有多个线程,一个进程的线程可以调用另一个进程的线程,线程让CPU调度开销更小(线程体量小,切换简单)
- 多核可以让多个进程/线程并行,提高效率
- 一个进程的多个线程需要争夺某些资源,得到资源的线程需要加锁,这个问题很常见
- 但CPython解释器解决的方法是直接让其他线程停止工作,让得到资源的某个线程执行
- 这就是GIL(全局解释器锁),多线程并不能使用多核真正实现并行,只能是单线程并发,现在可以回答问题了
- 描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因
- Python语言和GIL没有半毛钱关系。仅仅是由于历史原因在Cpython虚拟机(解释器),难以移除GIL
- GIL:全局解释器锁。每个线程在执行的过程都需要先获取GIL,保证同一时刻只有一个线程可以执行代码
- 线程释放GIL锁的情况: 在IO操作等可能会引起阻塞的system call之前,可以暂时释放GIL,但在执行完毕后,必须重新获取GIL
- Python 3.x使用计时器(执行时间达到阈值后,当前线程释放GIL)或Python 2.x,tickets计数达到100
- Python使用多进程是可以利用多核的CPU资源的
- 多线程爬取比单线程性能还是有提升的,因为遇到IO阻塞会自动释放GIL锁
- 小结
- 计算密集型程序建议使用多进程
- IO密集型可以忽略GIL锁的影响
- 可以用其他的语言替代多线程中的任务,例如变成C语言
- 通过命令
gcc dead_loop.c -shared -o libdead_loop.so
将C代码编译成.so文件 - python创建子线程,让其执行C语言编写的函数
- 通过命令
- 使用协程代替线程
深浅拷贝
-
赋值操作有三类:直接赋值(值,无子对象);深拷贝、浅拷贝(都包含子对象)
-
直接赋值:即等号
=
,变量相当于一个指针,指向某一对象(类似浅拷贝)a = 1 b = a print(id(a)) # 1816822573360 print(id(b)) # 内存地址相同
-
浅拷贝:如果对象内部不是简单的数值,而是列表之类的对象
- 浅拷贝只会拷贝父对象,不会拷贝对象内部的子对象;即子对象还是原对象的引用
>>> a = {1: [1,2,3], 2:'b'} # 直接赋值 a = dict(1=[1,2,3]) >>> b = a.copy() # 字典,包含子对象,可以看出是浅拷贝;看后面 >>> a, b ({1: [1, 2, 3], 2: 'b'}, {1: [1, 2, 3], 2: 'b'}) >>> a[1].append(4) # 注:字典不能通过下标索引取值 >>> a, b ({1: [1, 2, 3, 4], 2: 'b'}, {1: [1, 2, 3, 4], 2: 'b'}) >>> a[2] = 'c' ({1: [1, 2, 3, 4], 2: 'c'}, {1: [1, 2, 3, 4], 2: 'b'}) # b没改
-
深拷贝:copy 模块的 deepcopy 方法,完全拷贝了父对象及其子对象;即完全独立
>>>import copy >>> a = {1: [1,2,3,4]} >>> c = copy.deepcopy(a) # 深拷贝 >>> a, c ({1: [1, 2, 3, 4]}, {1: [1, 2, 3, 4]}) >>> a[1].append(5) >>> a, c ({1: [1, 2, 3, 4, 5]}, {1: [1, 2, 3, 4]}) # 也可以通过id()函数 # CPython 中 id() 函数用于获取对象的内存地址;即对象的唯一标识符,标识符是一个整数
-
总结:可以将拷贝理解为新开辟独立空间,其他都是引用;深浅拷贝都拷贝父对象,但一个指向子对象,一个拷贝子对象
- Python中很多数据结构的操作都会copy出新对象,而不会在原对象上修改,例如切片
str = 'RoyKun' result = str[0:5:1] # 起始下标:结束下标:步长 (不包含结束下标) print(result) # RoyKu 返回新对象赋值给 result
- 还要注意以下问题:
import copy a = [11,22] b = [33,44] c = [a,b] # 直接赋值(引用) d = c # 列表没有copy()方法,有子对象,就不是直接赋值,而是浅拷贝 e = copy.deepcopy(c) c.append([55,66]) # 不会改变e # 但是abc的改变都会影响d
-
小结
- 字典:有自己的
copy()
方法(浅拷贝) - 函数传参一般都是引用,会修改原对象;如果不想,深拷贝!
- 应用场景:对数据测试一般先深拷贝,在此备份上操作
- 字典:有自己的
私有化
-
Python中没有C++中的public/protected/private等关键字控制作用域和继承方式,所以有了私有化的方式
xx: 公有变量,类似public _x: 单前置下划线,私有化属性或方法,from somemodule import *禁止导入,类对象和子类可以访问,类似protected __xx:双前置下划线,避免与子类中的属性命名冲突,无法在外部直接访问(名字重整所以访问不到),类似private __xx__:双前后下划线,用户名字空间的魔法对象或属性。例如:__init__ , __ 不要自己发明这样的名字,特殊的public xx_:单后置下划线,用于避免与Python关键词的冲突,常规自定义方法
-
总结:前置下划线意义特殊
网络编程
- 主要涉及TCP/UDP/HTTP等协议,使用
socket
包建立通信的方法
Linux基础
- Linux基本操作
ctrl A
到命令行首ctrl E
到命令行末ifconfig
查看网络状态mv
文件重命名cp
拷贝文件到
- vim基本操作
- 命令模式下:
- 直接跳转到某行:行号+G
- 复制光标所在行粘贴到下一行:yyp
- 跳到行末并进入编辑模式:A
- 跳到行首:I
- 选中后剪切:d
- 粘贴:p
- 选中后左移:<
- 在光标行前面插入一行:O
- 后插入一行:o
vim xxx.py +4
打开文件后光标在第四行
- 命令模式下:
- 计算机网络基础知识
- 见本人博客
socket通信
-
完成网络通信必备的东西
-
import socket socket.socket(AddressFamily, Type) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # tcp s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # udp s.close() # 创建套接字 # 使用套接字收/发数据 # 关闭套接字
-
在Linux中使用sublime编程,相关Linux操作可以看总结“Linux面试常考点”
-
实现socket编程(见课件),缺那个Windows测试工具,应该类似postman吧
-
基于UDP协议发送数据:
# 要发送数据,必须确定对方端口 from socket import * # 1. 创建udp套接字 udp_socket = socket(AF_INET, SOCK_DGRAM) while True: if send_data == "exit": break # 2. 准备接收方的地址 # '192.168.1.103'表示目的ip地址 # 8080表示目的端口 dest_addr = ('192.168.1.103', 7788) # 注意 是元组,ip是字符串,端口是数字 # 3. 从键盘获取数据(准备数据) send_data = input("请输入要发送的数据:") # 4. 发送数据 udp_socket.sendto(send_data.encode('utf-8'), dest_addr) # 5. 关闭套接字 udp_socket.close() # ubuntu下运行程序使用: roy@ubuntu:$ python3 xxx.py
-
在发送之前
ping
一下,看网络通不通;虚拟机要改成桥接模式,如果此时IP还是不在同一网段,使用命令sudo dhclient
,静静等待; -
错误提示TypeError: need is object not str意思就是别发字符串;解决方案:在字符串前面写个
b
,即变成字节对象;或者如代码所示,encode
-
在ubuntu中
python3
和ipython3
都是交互模式,后者类似jupyter; -
UDP接收数据:
# 要接收数据,必须确定自身端口,需要绑定,这个IP不写表示要绑定任意IP的此端口,这里就应该是这台服务器的IP # 程序不是从第一行开始写的 from socket import * def main(): # 1. 创建套接字 udp_socket = socket(AF_INET, SOCK_DGRAM) # 2. 绑定端口(本地信息) local_addr = ('', 7788) udp_socket.bind(local_addr) # 3. 接收数据 recv_data = udp_socket.recvfrom(1024) # 1024表示本次接收的最大字节数 # 如果没有收到数据会在此处 阻塞 send_addr = recv_data[0] recv_msg = recv_data[1] # 4. 打印接收到的数据 print("%s:%s"%(str(send_addr), recv_msg.decode("gbk"))) # 从Windows来的数据要用gbk解码 # 5. 关闭套接字 udp_socket.close() # 从这开始写代码 if __name__ == "__main__": main()
-
小结:
- UDP协议的特点是不需要建立连接,只需要知道对方的IP和端口即可,数据的正确性依靠服务端校验
- 发送方和接收方各自搞清楚必要参数,发送方是对方的(IP,端口),接收方是自身(IP,端口);套接住咯,诶~
- 为何说“必要”参数呢?因为无论是作为发送方还是接收方,都需要有自己的端口才能发送数据的(tcp和udp是端对端的),但在这里发送方没有bind,所以OS会随机分配一个端口;
- 因为端口是套接字的对接目标,所以在同一电脑(IP)上的不同程序(端口)可以互发数据,“互发”意思就是创建一个套接字即可,可收可发;(将发送接收程序写在一起)
- 套接字是全双工的;
-
聊天器
- 程序在接收数据时若没有缓存消息,一闪而过,即OS会暂存收到的消息;这也存在弊端,可能会缓存过多信息,占用内存导致死机;(由于现在是单任务,只能实现半双工)
- 程序在接收数据时若没有缓存消息,一闪而过,即OS会暂存收到的消息;这也存在弊端,可能会缓存过多信息,占用内存导致死机;(由于现在是单任务,只能实现半双工)
-
这里发送IP可以写127.0.0.1回环地址,自己发自己收;也可以查看ubuntu自身IP,实现自娱自乐;
TCP通信
-
UDP不安全,类似写信;TCP类似打电话,需要连接,有确认机制
- 三次握手和四次挥手
-
TCP有拥塞控制和可靠传输机制
- 拥塞控制:指数递增线性增长、快开始
- 可靠传输包括:超时重传、错误校验
-
TCP是严格的客户服务器模式
- 客户端:需要链接
from socket import * # 创建socket tcp_client_socket = socket(AF_INET, SOCK_STREAM) # TCP # 服务器信息 server_ip = input("请输入服务器ip:") server_port = int(input("请输入服务器port:")) # 链接服务器 tcp_client_socket.connect((server_ip, server_port)) # 提示用户输入数据 send_data = input("请输入要发送的数据:") # 先发(请求) tcp_client_socket.send(send_data.encode("gbk")) # 接收对方发送过来的数据,最大接收1024个字节 recvData = tcp_client_socket.recv(1024) print('接收到的数据为:', recvData.decode('gbk')) # 关闭套接字 tcp_client_socket.close()
- 服务器:要绑定,再运行
from socket import * # 创建socket tcp_server_socket = socket(AF_INET, SOCK_STREAM) # 本地信息 address = ('', 7788) # tuple # 绑定 # 绑不绑定要看是否接收数据,如果只发送,不绑也行 tcp_server_socket.bind(address) # 使用socket创建的套接字默认是主动的, # 使用listen将其变为被动的,接收别人的链接,可套接 tcp_server_socket.listen(128) # 监听套接字负责等待有新客户链接 # accept()负责产生一个新的套接字 client_socket 专门为这个客户端服务 # clientAddr 即“来电显示” client_socket, clientAddr = tcp_server_socket.accept() # 默认阻塞,等待客户端connect() # 接收对方发送过来的数据(先收) recv_data = client_socket.recv(1024) # 函数的写法和UDP不同 print('接收到的数据为:', recv_data.decode('gbk')) # 发送一些数据到客户端 client_socket.send("thank you !".encode('gbk')) # 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接 client_socket.close() tcp_server_socket.close()
- 区别在于,服务器端会产生两个套接字,分别用于监听和收发数据;
- 这里,服务器端要先收数据(响应),客户端要先发数据(请求);然后互相收发;
-
和UDP的主要区别:
- 严格的客户服务器模式,分离
- 需要链接,而不是简单的告知(IP+端口),保证数据传输的质量
TCP文件下载器
-
客户端代码
from socket import * def main(): # 创建socket tcp_client_socket = socket(AF_INET, SOCK_STREAM) # 目的信息 server_ip = input("请输入服务器ip:") # 这个可以绑死 server_port = int(input("请输入服务器port:")) # 链接服务器 tcp_client_socket.connect((server_ip, server_port)) # 输入需要下载的文件名 file_name = input("请输入要下载的文件名:") # 发送文件下载请求(先建立连接,发请求,再收!) tcp_client_socket.send(file_name.encode("utf-8")) # 接收对方发送过来的数据,最大接收1024个字节(1K) recv_data = tcp_client_socket.recv(1024) # print('接收到的数据为:', recv_data.decode('utf-8')) # 如果接收到数据再创建文件,否则不创建 if recv_data: # 由于读写期间可能会出异常,需要捕获,with的作用是不用手动捕获并close with open("[接收]"+file_name, "wb") as f: f.write(recv_data) # with一般用于'w'模式,若要读取文件,加上try...except...捕获 # 关闭套接字 tcp_client_socket.close() if __name__ == "__main__": main()
-
服务端:
from socket import * import sys def get_file_content(file_name): """获取文件的内容""" # 函数注释 try: # 可能文件不存在,这是读写文件的标准写法 with open(file_name, "rb") as f: content = f.read() return content except: print("没有下载的文件:%s" % file_name) # 运行程序即启动服务器,输入参数:[0]是此程序文件名,[1]是端口号 def main(): # sys.argv[]其实就是一个列表,里边的项为用户输入的参数,且参数是从程序外部输入的,例如:python3 test.py a b c # abc就是外部输入参数,用列表接收 if len(sys.argv) != 2: # 保证输入了端口参数 print("请按照如下方式运行:python3 xxx.py 7890") return else: # 运行方式为python3 xxx.py 7890 port = int(sys.argv[1]) # 创建socket tcp_server_socket = socket(AF_INET, SOCK_STREAM) # 本地信息 address = ('', port) # 绑定本地信息(需要接收客户端信息) tcp_server_socket.bind(address) # 将主动套接字变为被动套接字 tcp_server_socket.listen(128) # 决定可以有多少个客户端连接,涉及到高并发了 while True: # 服务端继续运行 # 等待客户端的链接,即为这个客户端发送文件 client_socket, clientAddr = tcp_server_socket.accept() # 阻塞 # 接收对方发送过来的数据 recv_data = client_socket.recv(1024) # 接收1024个字节; 阻塞 file_name = recv_data.decode("utf-8") print("对方请求下载的文件名为:%s" % file_name) file_content = get_file_content(file_name) # 发送文件的数据给客户端 # 因为获取打开文件时是以rb方式打开,因此不需要encode编码 if file_content: client_socket.send(file_content) # 关闭这个套接字 client_socket.close() # 关闭监听套接字 tcp_server_socket.close() if __name__ == "__main__": main()
-
TCP注意点:
-
关闭监听套接字,已经建立连接的accept套接字不会断的哦;
-
服务端recv套接字解阻塞有两种方式:客户端关闭(挂电话)、服务端收到数据
-
结合多任务可以实现服务多个用户