目录
前言
本文介绍python高级编程内容。
基础
python官方文档
根据需要,选择查看版本:https://docs.python.org/zh-cn/3/
字符串
字符串转化成列表:list(字符串)
列表
- 新增:append
- 删除
- remove(删除元素)
- del 列表对象[删除索引]
- 遍历
for index,value in enumerate(数组):
私有变量
- 公共的(public):变量名
- 受保护的(protect):变量前面加单下划线
其他文件正常导入,会忽略受保护的变量,但可以硬导入,比如import 受保护变量
- 私有的(private):变量前面加双下划线
自省
Python的自省:Python运行的时候知道对象自身有哪些东西
- dir(对象):查看对象有哪些属性
- hasattr(对象,属性名):查看对象是否有某个属性
- isinstance(对象):检查变量类型
- type(对象):查看对象类型
- id(对象):查看对象id
- callable(对象):查看对象是否可调用的
- help(对象):查看对象的帮助信息
- getattr(对象):获得对象的属性
- setattr(对象):设置对象的属性
random
从数组中随机挑选一个:obj = random.choice(数组)
序列化和反序列化(pickle)
序列化
将结果数据流写入到文件对象中
import pickle
l1 = [1, 2, 3]
pickle.dump(l1, open("demo.data", "wb"))
反序列化
将文件中的数据解析为一个Python对象
import pickle
l = pickle.load(open("demo.data","rb"))
print(l)
推导式
列表推导式
def test_derivation_list():
"""
推导式的格式:
[处理迭代中的每一个元素 for语句 条件判断]
"""
# 返回1~100所有的偶数 [2,4,6,8.....100]
result = []
for i in range(1, 101):
if i % 2 == 0:
result.append(i)
print(result)
print([i for i in range(1, 101) if i % 2 == 0])
字典推导式
def test_derivation_dict():
# 还可以应用于字典的解析
cookie = "_ga=GA1.2.930106050.1598708338; Hm_lvt_39b794a97f47c65b6b2e4e1741dcba38=1601376712,1601378209,1601378225,1601378754; __gads=ID=d51165f71c40f9ea:T=1598708341:R:S=ALNI_MZNqeB0nRFuLFMHnaiqkRUxBzf4FQ; UM_distinctid=176b8fb1876443-00bac4ce20b2-3323767-1fa400-176b8fb18776f; CNZZDATA1278819128=727475643-1609418695-https%253A%252F%252Fwww.baidu.com%252F%7C1609418695; Hm_lvt_e2fcb48ccb07c3714ef5b2d8ebbe5bac=1609421232; .AspNetCore.Antiforgery.b8-pDmTq1XM=CfDJ8EklyHYHyB5Oj4onWtxTnxZK-SyYDsAq4COxRCEyMCXVnz1Dm-qDFJUvwPYvokG_RRwY17vcaB5If8VM7G0ko8cvd0RmS5frC7yOg0-xGo7Xzim9enGJ45ZqPXn24gU1XorwYZg3T2KdYGRMNKBQitw; _gid=GA1.2.1426547664.1614343372"
# cookie格式: cookie键1=cookie值1;键2=值2;键3:值3;.....
# cookie新格式: {键1:值1,键2:值2......}
new_cookie = {}
for item in cookie.split(';'): # 先用;分割cookie
s = item.split('=') # 用 = 分割
new_cookie[s[0]] = s[1] # =左边作为键,=右边作为值,添加到new_cookie字典中
print(new_cookie) # 输出new_cookie
# 字典推导式 字典解析 格式如下:
# {键:值 for语句}
print({item.split('=')[0]: item.split('=')[1] for item in cookie.split(';')})
可迭代对象、迭代器、生成器
可迭代对象
可以用for操作的对象都是可迭代对象,比如list、tuple str、set、dict
如何判断一个对象是不是可迭代对象
- isinstance(obj,Iterable)
- hasattr(obj, “__iter__”)
# list tuple str set dict
for i in [1, 2, 3]:
print(i)
print(hasattr(list, "__iter__"))
print(hasattr(tuple, "__iter__"))
print(hasattr(str, "__iter__"))
print(hasattr(set, "__iter__"))
print(hasattr(dict, "__iter__"))
print(hasattr(int, "__iter__")) # False
for i in 1: # TypeError: 'int' object is not iterable
print(i)
可迭代对象(iterable)和迭代器(iterator)区别
- 可迭代对象(list,set,dict)可以重复迭代,只能使用for循环;
- 迭代器只能迭代一次,可以通过next或for进行迭代
迭代器(比列表,节省内存)
如何判断对象是不是迭代器
- 迭代器类型必须实现__iter__和__next__(Python2中是next)
- __iter__方法必须 返回 self
- __next__必须返回下一个值,如果没有下一个则抛出StopIteration异常
- 对迭代器进行for操作时,每次操作都会执行__next__方法
- 只能迭代一遍
- for语句的迭代,会忽略StopIteration异常
迭代器 与 list相比,迭代器省内存
from typing import Iterator # list tuple dict int
# isinstance 是内置函数
obj = iter(range(1, 2)) # 把range(1,2)转换为Iterator类型
for attr in dir(list):
print(attr) # __iter__
for attr in dir(obj):
print(attr) # __iter__ ... __next__
print(isinstance([1, 2], list)) # True
print(isinstance(obj, Iterator)) # True
print(isinstance([1, 2], Iterator)) # False
迭代器的协议
- 迭代器类型必须实现__iter__和 __next__(Python2中是next)
- __iter__方法必须 返回 self
- __next__必须返回下一个值,如果没有下一个则抛出StopIterator异常
lass Next(object):
def __init__(self, stop, start=0):
self.start = 0
self.stop = stop
def __iter__(self):
return self
def __next__(self):
"""如果有下一个数,则返回下一个数;如果没有下一个数,则抛出StopIteration异常"""
if self.start >= self.stop - 1:
raise StopIteration
self.start += 1
return self.start
if __name__ == '__main__':
obj = Next(5)
for i, value in enumerate(obj):
print(i, value)
for i in obj:
print(i)
print(obj.__next__()) # 异常
print(obj.__next__())
print(obj.__next__())
print(obj.__next__())
生成器(快速创建迭代器)
使用yield的关键字,快速方便地创建迭代器,所以生成器一定是一个迭代器
实现生成器
案例:手动实现 平方, 传参(1,3) 返回:1 4 9
优化前 - 常规迭代器替代list,好处:更节省内存
result = []
for i in [1, 2, 3]:
result.append(i * i)
print(result)
class Squares(object):
def __init__(self, start, stop):
self.start = start
self.stop = stop
def __iter__(self):
return self
def __next__(self):
if self.start > self.stop:
raise StopIteration
current = self.start * self.start
self.start += 1
return current
if __name__ == '__main__':
iterator = Squares(1, 3)
for i, value in enumerate(iterator):
print(i, value)
优化后 - 在函数中用yield,实现生成器,好处:快速创建迭代器,更快速方便
def squares(start, stop): # 第二种
for i in range(start, stop + 1):
yield i * i
squares2 = (i * i for i in range(1, 4)) # 第三种
print(type(squares(1, 4))) # <class 'generator'>
print(type(squares2)) # <class 'generator'>
if __name__ == '__main__':
iterator = squares(1, 3)
for i, value in enumerate(iterator):
print(i, value)
生成器执行
- 如果一个函数中有yield关键字,调用函数的时候不会执行函数的内容,会返回一个对象(这个对象类型是生成器类)
- 当要访问生成器的__next__方法时,函数会编程running状态,当执行完yield时,函数变成非running状态(即挂起)
- 只有再次执行生成器对象的__next__方法时函数才会被唤醒
def f():
print("开始执行")
a = 1
yield a
print("~~~~~~~~~~~")
a = 2
yield a
def f2():
result = []
result.append(1)
result.append(2)
return result
# 当要访问生成器的__next__方法时,函数会编程running状态,当执行完yield时,函数变成非running状态(即挂起),
# 只有再次执行生成器对象的__next__方法时函数才会被唤醒。
# 想一想: 什么情况下会执行生成器对象的__next__方法呢?(获取生成器下一个值的时候)
if __name__ == '__main__':
obj = f() # 如果一个函数中有yield关键字,调用函数的时候不会执行函数的内容,会返回一个对象(这个对象类型是生成器类)
for i, value in enumerate(obj):
print(i, value)
# print(obj.__next__())
# print(obj.__next__())
# print(obj.__next__())
print(type(obj))
print(hasattr(obj, "__next__"))
递归、回调、闭包
递归(调用本身)
递归函数:在函数内循环调用函数若干次
"""
遍历目录:请用递归方法输出某文件夹下的所有文件的绝对路径
"""
import os
def print_all_files(file_path):
for root, dirs, files in os.walk(file_path):
for filename in files:
print(os.path.join(root, filename))
def print_all_files2(file_path):
"""
思路:获得file_path下的所有文件及文件夹 # os.scandir(file_path) os.listdir(file_path)
如果是文件,直接输出
如果是文件夹,递归调用print_all_files2(文件夹)
"""
for item in os.scandir(file_path):
if item.is_file():
print(item.path) # 输出文件夹下的所有文件
elif item.is_dir():
print("~~~~~~")
print_all_files2(item.path) # 有多少个文件夹,print_all_files2函数就被调用多少次
def recoder(n):
print(f"这是第{n}层录屏")
if n == 1:
return None # 结束递归,因为不结束就会无限递归
else:
n -= 1 # 每递归一次,把层数-1
recoder(n) # 第n层递归
if __name__ == '__main__':
p = "C:/Users/10795/PycharmProjects/python_advance_programming"
print_all_files2(p)
recoder(4)
回调(参数是函数类型)
如果一个函数的参数是函数类型,那么我们可以把这个参数叫做回调函数
# 比较2个数的大小,并用不同风格输出 例如: compare 1 and 2, min = 1 1和2比较,最小的是1
# 借用参数调用的别的函数,叫回调;
def get_min(a, b, func): # 来: 传来的函数func; 回:我把参数回传给你——func
# func是回调函数的引用
result = a if a < b else b
func(a, b, result) # 回调
# 回调函数一
def call_back_print_en(a, b, _min):
print(f"compare {a} and {b},min={_min}")
# 回调函数二
def call_back_print_zh(a, b, _min):
print(f"{a} 和 {b}比较,{_min}最小")
if __name__ == '__main__':
get_min(1, 2, call_back_print_zh)
闭包
如果内函数使用了外函数的局部变量,并且外函数把内函数返回出来的过程,叫做闭包,里面的内函数是闭包函数
作用
- 读取外部函数内的变量
- 将外层函数的变量持久地保存在内存中
应用场景
保留当前的状态,实现基于现在状态的更新
计算器
def calculate():
"""当做计算器使用"""
num = 0
def add(value):
nonlocal num # 内嵌作用域需要使用nonlocal关键字,global(全局) local(局部) nonlocal(不在本局部,但不是全局)
num += value
return num
# 不加括号,返回内部函数的函数名(即引用)
return add
add1 = calculate()
print(add1(5))
print(add1(10))
print(add1(15))
add2 = calculate()
print(add2(3))
print(add2(13))
print(add2(16))
print(add1(70))
统计函数的被调用次数
def counter(func):
"""统计函数的被调用次数"""
count = 0
def closure(*args, **kwargs):
nonlocal count
count += 1
print(f"{func.__name__}被调用了{count}次了")
return func(*args, **kwargs)
return closure
def add(a, b):
print(f"SUM: {a + b}")
def say_hello():
print("hello")
counter_add = counter(add)
say_hello = counter(say_hello)
counter_add(11, 22)
say_hello()
counter_add(33, 22)
say_hello()
say_hello()
say_hello()
counter_add(33, 22)
当装饰器使用
# 案例一:打印函数调用日志
import logging
# 用闭包实现
def logger(func):
def log_func(*args, **kwargs):
logging.basicConfig(filename="demo.log", level=logging.INFO)
logging.info(f"{func.__name__} is running, arguments is {args}")
func(*args, **kwargs)
logging.info(f"{func.__name__} complete running")
# 返回 log_func 不加括号 ,这里就是闭包
return log_func # 把log_func函数的引用 传给 logger的调用者
@logger
def f1(a, b):
print("hello")
@logger
def f2(x, y):
pass
f1(1, 2)
f2(10, 20)
为什么使用闭包
- 提供某种数据隐藏作为回调函数,减少了全局变量的使用
- 如果我们的对象中只有一个方法时或者代码中函数较少时,使用闭包是会比用类来实现更优雅,可以减少代码大小,节省内存空间
- 闭包非常适合替换硬编码常量
- 闭包在装饰函数中非常有用
注意:闭包是将外层函数的变量持久地保存在内存中,容易引起内存消耗,要注意使用场景
内置函数(高级函数)
所有内置函数在builtins.py
文件中,可以通过ctrl+点击某个内置函数,找到这个文件
max、min(求最大、最小值)
略
zip(打包成元祖)
拉链函数,将可迭代对象作为参数,将里面对应的元素打包成一个元组
def zip_test():
"""
zip(*iterables) --> A zip object yielding tuples until an input is exhausted.
>>> list(zip('abcdefg', range(3), range(4)))
[('a', 0, 0), ('b', 1, 1), ('c', 2, 2)]
"""
print(list(zip('abcdefg', range(3), range(4))))
reduce(累积运算)
把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算
def reduce_test():
# 累加功能: 1~5做累加 [1,2,3,4,5] = > (((1+2) + 3) + 4)
def f(x, y):
result = x + y
return result
print(sum([1, 2, 3, 4, 5]))
print(reduce(f, [1, 2, 3, 4, 5]))
map(映射)
map(如何映射,[要映射的对象1,要映射的对象2…]:映射函数,将可迭代对象中每一个元素来进行映射,分别执行函数
def map_test():
# map(func, *iterables) --> map object (map对象是一个迭代器)
# [1,2,3,4] [10,20,30,40] [1, 1, 1] => [12,22,32,42]
nums1 = [1, 2, 3]
nums2 = [10, 20, 30, 40]
nums3 = [1, 1, 1]
def f(x, y, z):
return x + y + z
print("使用map实现:", list(map(f, nums1, nums2, nums3)))
print("使用map实现:", list(map(lambda x, y, z: x + y + z, nums1, nums2, nums3)))
return result
# return map(lambda x: (2 * x + 2), [1, 2, 3]) # 对 [1,2,3]中的每一个元素 做 2*x +2 的处理
filter(过滤)
filter(怎么过滤,要过滤的对象):从可迭代对象中筛选出满足条件的元素
def filter_test(nums, mark=5):
"""
从nums中过滤出比mark大的数
:param nums:有数字组成的可迭代对象
:param mark:过滤的比对标准,默认是5
:return:
"""
return list(filter(lambda x: x > mark, nums))
if __name__ == '__main__':
print(filter_test([1, 2, 3, 4, 5, 6, 7, 8]))
装饰器
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构
作用
- 用装饰器写的代码更优雅,比如框架的路由传参
- 在不更改函数的基础上,为函数添加额外的功能
应用场景
- 可以在外层函数加上时间计算函数,计算函数运行时间;
import time
def calc_time(func):
def wrapper(*args, **kwargs):
# 记录函数运行前的时间 start
start = time.time()
# 执行函数
func(*args, **kwargs)
# 记录函数运行后的时间 end
end = time.time()
# 输出函数的运行时间 end - start
print(f'{func.__name__}运行了{end - start}时间')
return wrapper
@calc_time # f 会传给calc_time
def f(a, b):
time.sleep(1)
if __name__ == '__main__':
f(1, 2)
- 计算函数运行次数;
- 可以用在框架的路由传参上;
- 插入日志,作为函数的运行日志;
- 事务处理,可以让函数实现事务的一致性,让函数要么一起运行成功,要么一起运行失败;
- 缓存,实现缓存处理;
- 权限的校验,在函数外层套上权限校验的代码,实现权限校验;
带参数的装饰器
"""
用来记录日志的装饰器。
@log(filename='info.log')
def add(a,b):
...
@log(filename='xxx.log')
def modify(item):
...
"""
print('======')
def log(filename: str):
def decorator(func):
print(f'{func.__name__}')
def wrapper(*args, **kwargs):
print(args)
func(*args, **kwargs)
return wrapper
return decorator
# 效果等同于add=log(filename="xxx.log")(add)
@log(filename="xxx.log")
def add(a, b):
return a + b
add(1, 2)
用函数装饰类
"""
@some_func
class A(object):
...
pass
A传给some_func
"""
def some_func(cls):
return cls
@some_func
class A(object):
pass
if __name__ == '__main__':
a = A()
用类装饰函数
"""
@A
def f(a,b):
pass
f 传给 A
"""
import time
class CalTime(object):
def __init__(self, func): # 返回一个对象:__init__方法首先会初始化对象,并把对象返回。
self.func = func
print("~~~~~~")
def __call__(self, *args, **kwargs): # __call__当对一个对象加()进行调用的时候,此方法会自动执行
print(f"{self.func.__name__}函数运行前")
start = time.time()
result = self.func(*args, **kwargs)
print(f"{self.func.__name__}函数运行后")
end = time.time()
self.log()
print(f'{self.func.__name__}的运行时间为{end - start}')
return result
def log(self):
pass
@CalTime # 类,f会传给类,然会返回一个A的实例,效果等同于f=CalTime(f)
def f(x, y):
time.sleep(1)
print(x)
pass
return x + y
if __name__ == '__main__':
print(f(1, 2))
魔术方法
所有以“__”双下划线包起来的方法,都统称为“Magic Method”(魔术方法),更多了解参考python的魔术方法大全
__cal__(判断是否可被调用)
允许一个类的实例像函数一样被调用:x(a, b) 调用 x.call(a, b)
def add(x, y):
return x + y
class MyClass(object):
pass
class MyClass2(object):
def __call__(self, *args, **kwargs):
pass
print(callable(add)) # True
print(callable(MyClass())) # False
print(callable(MyClass2())) # True
__new__和__init__(对象的创建和初始化)
-
__new__:
创建对象
的方法,当通过类去创建对象时就是调用__new__方法去创建的 -
__init__:
初始化
方法,当实例被创建的时候调用的初始化方法
class Person(object):
def __init__(self, name):
self.name = name
print("初始化对象的值")
def __new__(cls, *args, **kwargs):
print("开始创建对象并分配内存")
self = super().__new__(cls)
return self
class Student(Person):
def __init__(self, name, stu):
# 初始化父类的方法
# Person.__init__(self, name) 方法一
# super(Student, self).__init__(name) # 方法二:Python2 Python3
super().__init__(name) # 方法三: Python3
def study(self):
pass
class Teacher(Person):
pass
if __name__ == '__main__':
xiaoming = Person('小明')
__str__(定义对象输出结果)
print(hasattr(list, '__str__')) # True
print([1, 2, 3])
class MyList(list):
def __str__(self): # print 对象时 对象输出的样子
result = ''
for value in self:
result += str(value)
return f"list:{result}"
nums = MyList([1, 2, 3])
print(nums)
# 运行结果
True
[1, 2, 3]
list:123
__add__(加法)
"""
对同类型的2个对象进行 + 操作,会执行对象的 __add__方法
"""
class Myclass(object):
def __init__(self, value):
self.value = value
def __add__(self, other):
# self: 第一个对象
# other: 第二个对象
return self.value + other.value
if __name__ == '__main__':
obj1 = Myclass(1)
obj2 = Myclass(2)
print(obj1 + obj2)
# 运行结果:3
上下文管理器(with)
自定义上下文管理器
"""
with
上下文管理协议 : 对象一定包含 __enter__() 和 __exit__() 方法
被with包裹的代码块在执行前先执行__enter__方法,代码块执行结束后会执行__exit__方法
"""
class MyClass(object):
def __enter__(self):
print("enter .....")
def __exit__(self, exc_type, exc_val, exc_tb):
print('exit.....')
obj = MyClass()
with obj as o:
print("~~~~~~~~~~")
经典上下文管理器(open)
# open方法返回一个对象——文件对象,文件对象支持上下文管理协议
f = open("temp.txt", 'w')
print(hasattr(f, '__enter__')) # True
print(hasattr(f, '__exit__')) # True
f.close()
with open("temp.txt", 'w') as f:
pass
设计模式
单例模式
单例模式是最常使用的一种设计模式,该模式的目的是确保在一个系统中,一个类只有一个实例
应用场景
- 数据库连接
- Socket(套接字)创建链接
实现单例模式
具体参考:python–单例模式
使用模块导入
- 创建类的实例
- 导入实例使用
自编写类
class Singleton():
def __init__(self, name):
self.name = name
def do_something(self):
pass
singleton = Singleton('模块单例')
在其他脚本里,将自己写的类看作库调用
from my_singleton import singleton
在任何引用singleton的脚本里,singleton都是同一个对象,这就确保了系统中只有一个Singleton的实例。
基于__new__方法实现
- 如果对象已经创建,就直接把创建好的对象返回
- 如果对象没有创建,就创建对象并返回
"""
单例模式: 只有一个实例
实现方法:
方法一:import
方法二:单例模式
方法三:装饰器
"""
class Person(object):
obj = None
def __init__(self):
pass
def __new__(cls, *args, **kwargs):
"""如果对象已经创建,就直接把创建好的对象返回
如果对象没有创建,就创建对象,并返回"""
if cls.obj is None:
cls.obj = super().__new__(cls) # 分配内存
return cls.obj
if __name__ == '__main__':
xiaoming = Person()
xiaohong = Person()
print(xiaoming is xiaohong) # False
print(id(xiaoming), id(xiaohong))
使用装饰器
def Singleton(cls):
instance = {}
def _singleton_wrapper(*args, **kargs):
if cls not in instance:
instance[cls] = cls(*args, **kargs)
return instance[cls]
return _singleton_wrapper
@Singleton
class SingletonTest(object):
def __init__(self, name):
self.name = name
slt_1 = SingletonTest('第1次创建')
print(slt_1.name)
slt_2 = SingletonTest('第2次创建')
print(slt_1.name, slt_2.name)
print(slt_1 is slt_2)
# 输出结果
第1次创建
第1次创建 第1次创建
True
创建slt_2 对象时,instance 字典中已经存在SingletonTest 这个key,因此直接返回了第一次创建的对象,slt_1 和 slt_2 是同一个对象。
鸭子模型
在程序设计中,鸭子类型(duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定
# 应用场景:发送消息
class Text(object):
def send(self):pass
class Image(object):
def send(self):pass
def send_msg(obj):
....
obj.send()
.,..
send_msg(Text())
send_msg(Image())
进程&线程&协程
相关概念
- 单任务:单任务的应用程序 (cmd.exe),没办法同时执行多条命令
- 多任务:多任务的应用 windows操作系统, 迅雷 (同时下载多个文件,边下边播)
- 多任务的实现方式:多进程、多线程、协程
- 串行运行:在Linux中串行执行多条命令用 & , apt install python & apt install mysql
- 并行运行:在Linux中并行执行多条命令用 && apt install python && apt install mysql
- 并发:迅雷下载同时下载5个电影(单核CPU)
1个收银员 “同时” 操作5台 收款机 —— 并发 - 并行:3台电脑分别安装一个MySQL8.0,然后组成MySQL集群,3个MySQL是同时运行(并行)
5个收银员 同时 操作 5台 收款机 —— 并行
基础概念
进程、线程、协程
进程 | 线程 | 协程 | |
---|---|---|---|
定义 | 一个在内存中运行的应用程序 ,是操作系统资源分配 的基本单位,不共享资源 | 处理器(CPU)调度 的基本单位(或者说是程序执行 的最小单元),同一进程的线程共享本进程的地址空间和资源 | 作为线程上层的抽象 ,比线程更加轻量 级,是用户程序自己控制调度 ,在线程的基础之上通过分时复用 的方式运行多个协程,即通过单线程实现并发 |
所属空间 | 系统空间 | 系统空间 | 用户空间 |
调度者 | 操作系统 | 操作系统 | 编译器/编码 |
依赖 | 程序、数据、系统资源 | 进程 | 线程 |
独立的计算机资源 | 拥有 | 不拥有 | 不拥有 |
主要目的 | 提高系统的资源利用率和增加系统的吞吐量 | 提高系统并发性能。对于IO消耗型进程而言,多线程会极大提高CPU利用率。 | 减少并发系统的开销。由用户替换调度系统级的资源调度和上下文切换,节省CPU的切换开销。 |
关系 | 拥有 n ≥ 1 个线程 | 一个只属于一个进程,但可以包含多个协程 | - |
使用方法 | 所有应用程序运行时都是一个进程。 | 一般的开发语言都支持多线程开发,但运行时需要操作系统支持多线程 | python支持协程,可以直接使用;java等语言目前不支持,但是可以使用现有框架实现或者利用状态机、事件驱动等方法自己编程实现 |
执行 | 默认主进程结束后,子进程会继续执行 | 默认主线程结束后,子线程会继续执行,进程会等待所有子线程执行完毕后才结束 | 默认主程序结束,所有代码结束 |
多进程、多线程
- 多进程:同一时刻
并行运行多个应用程序
。真正的并行多进程只能在多核的CPU
上实现,由于任务数量是远远多于CPU的核数,所以操作系统会自动将多任务短时间轮流切换执行,给我们的感觉就像同时在执行一样 - 多线程:把一个程序划分为若干个任务,
多个任务并发执行
,每一个任务就是一个线程。因为CPU在执行程序时每个时间刻度上只会存在一个线程
,因此多线程实际上提高了进程的使用率从而提高了CPU的使用率。但线程过多时,操作系统在他们直接来回切换,影响性能。
进程池、线程池
- 池:保证计算机
硬件能够正常工作
的情况下,最大限度的利用资源
的量 - 进程池:众所周知,Python 的多线程是假的,但我们可以不关注进程的并发细节,只需要把并发的任务仍到进程池里就好了
- 线程池:是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
进程和线程的联系和区别
进程是资源分配
的最小单位,线程是CPU调度
的最小单位。一个进程中至少包含一个线程,线程不能独立于进程而存在。多个进程间不能共享资源,每个进程有自己的堆、栈、虚存空间(页表)、文件描述符等信息,而线程可以共享进程资源文件(堆和方法区)。线程可以看作是轻量级的进程
总结
- cpu密集型计算:建议使用多进程/多进程+多线程
- io密集型计算:建议使用多线程/协程
- 无论是多线程还是多进程,它的创建和消耗都会消耗cpu,所以建议使用
线程池、进程池
的方式,减少频繁的创建和消耗线程、进程而消耗cpu
进程
进程基础
getpid(获取进程id)
import os
# 获得进程ID
print('此进程的id是:', os.getpid())
input('按任意键结束程序')
# python.exe C:\Users\10795\PycharmProjects\python_advance_programming\dev_07day\进程基础.py
# 用python.exe 去 解释执行 C:\Users\10795\PycharmProjects\python_advance_programming\dev_07day\进程基础.py
join(阻塞进程)&daemon(守护进程)& is_alive
- join:若阻塞的进程里任务没有执行完, 无法执行join以后的语句
- daemon:该进程会随着主进程代码执行完毕而结束
- is_alive:判断进程是否存活
import os
import time
from multiprocessing import Process
def play_music():
print("子进程p1:", os.getpid())
for i in range(5):
print('play music ...')
time.sleep(1)
def play_lol():
print("子进程p2:", os.getpid())
for i in range(5):
print('play lol ...')
time.sleep(1)
if __name__ == '__main__':
print('主进程:', os.getpid())
start_time = time.time()
p1 = Process(target=play_music)
p2 = Process(target=play_lol)
# p1.daemon = True # 默认值是False,当为True的时候,主进程结束,会强制结束子进程
# p2.daemon = True
p1.start() # 操作系统 启动一个进程 用来运行target=play_music 的 进程,子进程 (不是完整的代码)
# p1.join() # 上面的代码任务没有执行完,不会运行下面的代码
p2.start()
print(help(p2))
# p1.join()
# p2.join()
# p1 p2 都结束后再 计算时间
# while True:
# if p1.is_alive() or p2.is_alive():
# time.sleep(1)
# else:
# break
end_time = time.time()
print(end_time - start_time)
# 怎么才能保证让主进程最后再结束呢? 用join
# 主进程: 进程基础.py (代码) python.exe 进程基础.py 运行的时候, 操作系统会 分一个进程, 这个进程我们称为主进程
# p1.daemon
# is_alive()
多进程
案例一:多进程实现同一任务
"""
进程: 进程是动态的
静态(软件、程序、代码、函数...)
动态(软件运行、程序运行、代码运行、函数运行)
程序是怎么运行的? 操作系统 创建一个进程(加载代码到内存,为变量分配内存,建个档案,process id PID
"""
from multiprocessing import Process, Queue, set_start_method
import requests
import os
"""
需求:使用多进程实现并发下载图片。
- 多进程会独享资源, 2个进程不能共享变量(例如:不能共享list) ,如何实现共享资源呢?(队列Queue,内存数据库redis....)
- 多线程占用的资源少,2个线程是共享变量的,迅雷就是使用多线程下载技术。
"""
def download(q: Queue):
while True:
url = q.get()
r = requests.get(url) # 就像浏览器发请求一样
img_name = url.split('/')[-1]
with open(img_name, 'wb') as f:
f.write(r.content) # 把服务器返回的信息(图片)保存到文件中
if q.empty():
break
if __name__ == '__main__':
# set_start_method('fork') # mac电脑运行需要加上
tasks = Queue()
# put相当于list中的 append方法
tasks.put("https://www.ketangpai.com/images/common/logo_blue.png")
tasks.put("https://photo-static-api.fotomore.com/creative/vcg/176/new/862018abb4f74bd0b4f9ad53a3c1250e.jpg")
p1 = Process(target=download, args=(tasks,)) # 创建一个用来执行download函数的进程p1
p2 = Process(target=download, args=(tasks,)) # 创建一个用来执行download函数的进程p2
p1.start() # 启动进程p1
p2.start() # 启动进程p2
# 进程启动后,是由操作系统控制执行的
注:出现self._semlock = _multiprocessing.SemLock._rebuild(*state) FileNotFoundError
报错,是因为Python创建的子线程执行的内容, 和启动该进程的方式有关. 而不同的操作系统,启动进程的方式不同,详细了解参考https://blog.csdn.net/weixin_45592364/article/details/126464756
案例二:多进程实现多个任务
队列Queue:保障进程安全,防止多个进程或者多个线程争夺同一资源
"""
multiprocessing.Queue : 管道 序列化 进程安全
了解: MQ (Message Queue) Redis rabitMq kafka
"""
from multiprocessing import Process, Queue
def work01(q: Queue):
"""
:param q: 要处理的所有任务
:return: None
"""
print('work01中的参数q的id:', id(q))
while not q.empty():
print(f'work01从q中获得了{q.get()}')
def work02(q: Queue):
"""
:param q: 要做的所有任务
:return: None
"""
print('work02中的参数q的id:', id(q))
while not q.empty():
print(f'work02 从q中获得了{q.get()}')
if __name__ == '__main__':
q = Queue()
q.put('a')
q.put('b')
q.put('c')
p1 = Process(target=work01, args=(q,)) # 子进程 q1
p2 = Process(target=work02, args=(q,)) # 子进程 q2
p1.start()
p2.start()
print(f'主进程运行完毕!')
# 使用list: work01 work02会做重复工作 p1 p2 进程 独享 list内存
# 使用Queue: work01 work02共同完成q中的所有任务 Queue 是被 p1 p2 共享的
案例三:多进程类
from multiprocessing import Process
class MyProcess(Process):
def __init__(self):
super().__init__()
def run(self):
print("hello ")
if __name__ == '__main__':
p1 = MyProcess()
p2 = MyProcess()
p1.start() # 会调用 MyProcess中的run方法
p2.start()
进程池
使用ProcessPoolExecutor()方法(推荐)
ProcessPoolExecutor(n),括号内不填的话,会默认创建与“cpu核数”相同数量
的进程;
进程池和线程池在使用形式上是一样的,唯一不同的是:在Windows环境下,进程池要放在main方法里面,否则会报错。
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def fn(name):
for i in range(10):
print(name,i)
if __name__ == '__main__':
#创建进程池
with ProcessPoolExecutor(50) as t:
for i in range(100):
t.submit(fn,name=f"进程{i}")
使用multiprocessing.Pool方法
pool.apply/apply_async(调用函数,传递任意参数)
调用函数仅执行一次
- apply:同步,阻塞主进程
- apply_async:异步,不阻塞主进程
import time
from multiprocessing import Pool
"""
apply : 调用函数,传递任意参数
map: 把一个可迭代对象 映射 到 函数
"""
def aa(x, y, z=0): # z=0
"""执行脚本,执行过程需要30分钟"""
print('~~~~')
time.sleep(2)
return x + y
if __name__ == '__main__':
pool = Pool(2)
# apply(self,func,*args,**kwds)
# apply(self, func, args=(), kwds={})
result = pool.apply(aa, (2, 3), {'z': 1}) # 阻塞 主程序(主进程) 同步
pool.apply_async(aa, (2, 3)) # 不阻塞 主程 异步
print("finished!")
# 运行结果
~~~~
finished!
~~~~
pool.map/map_async(可迭代对象 映射 到 函数)
可迭代对象有多少个值,就会执行几次函数
- map:同步,会
阻塞主进程
,相当于异步的map加get搜集结果
import time
from multiprocessing import Pool
def square(x):
print("~~~~~~~~")
result = x * x
return result
if __name__ == '__main__':
start = time.time()
pool = Pool(2)
result = pool.map(square, [1, 2, 3, 4, 5, 6, 7, 8]) # map 会阻塞主程序
print(result)
pool.close()
end = time.time()
print(f'总共用了{(end - start):.2f}')
# 运行结果
~~~~~~~~
~~~~~~~~
~~~~~~~~
~~~~~~~~
~~~~~~~~
~~~~~~~~
~~~~~~~~
~~~~~~~~
[1, 4, 9, 16, 25, 36, 49, 64]
总共用了0.14
- map_async:异步,
所有进程将同时进行,它们不能确保工作进程的顺序
import time
from multiprocessing import Pool
def square(x):
result = x * x
print("~~~~~~~~~")
return result
if __name__ == '__main__':
start = time.time()
pool = Pool(2)
result = pool.map_async(square, [1, 2, 3, 4, 5, 6, 7, 8]) # map 会阻塞主程序
pool.close()
end = time.time()
print(f'总共用了{(end - start):.2f}')
print(result.get()) # get() 用例 获得map映射后的结果集 [1, 4, 9, 16, 25, 36, 49, 64] , 结果不出来,不往下运行 (即阻塞)
# 运行结果
总共用了0.05
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
~~~~~~~~~
[1, 4, 9, 16, 25, 36, 49, 64]
此案例中主线程先执行,调用get阻塞主线程,搜集子线程的结果
线程
多线程
多线程就是把操作系统中的这种并发执行机制原理运用在一个程序中,把一个程序划分为若干个子任务,多个子任务并发执行,每一个任务就是一个线程。
因为CPU在执行程序时每个时间刻度上只会存在一个线程,因此多线程实际上提高了进程的使用率从而提高了CPU的使用率。但线程过多时,操作系统在他们直接来回切换,影响性能
案例一:多线程实现同一任务
"""
第1页:https://so.gushiwen.org/mingjus/default.aspx?page=1&tstr=%E6%98%A5%E5%A4%A9
第2页:https://so.gushiwen.org/mingjus/default.aspx?page=2&tstr=%E6%98%A5%E5%A4%A9
第3页:https://so.gushiwen.org/mingjus/default.aspx?page=3&tstr=%E6%98%A5%E5%A4%A9
第4页:https://so.gushiwen.org/mingjus/default.aspx?page=4&tstr=%E6%98%A5%E5%A4%A9
"""
import queue
import threading
import requests
import re
def downloader(q: queue.Queue):
while not q.empty():
url = q.get()
print(url)
r = requests.get(url)
page_num = re.findall(r'page=(\d)&tstr', url)[0]
filename = f'春天{page_num}.html'
with open(filename, 'wb') as f:
f.write(r.content)
if __name__ == '__main__':
q = queue.Queue()
for i in range(1, 5):
q.put(f'https://so.gushiwen.org/mingjus/default.aspx?page={i}&tstr=%E6%98%A5%E5%A4%A9')
thread_pool = []
for i in range(4):
t = threading.Thread(target=downloader, args=(q,))
thread_pool.append(t)
t.start()
for t in thread_pool:
t.join()
print('finished!')
案例二:多线程实现多个任务
import threading
import time
def music(user):
print(f'{user}正在听音乐....')
print(f'{threading.current_thread().name}正在运行...')
time.sleep(5)
print(f'{threading.current_thread().name}运行即将结束。')
def lol(user):
time.sleep(8)
if __name__ == '__main__':
t1 = threading.Thread(target=music, args=('无极',), name='线程1')
t2 = threading.Thread(target=lol, args=('无极',), name='线程2')
t1.start()
t2.start()
# t1.join() # 阻塞主程序,但不会阻塞线程2
# t2.join()
print('主程序结束!')
案例三:线程锁
- 创建线程锁:lock = threading.Lock()
- 上锁:lock.acquire()
- 释放:lock.release()
- 判断是否存在锁:with lock
import threading
n = 200000
lock = threading.Lock()
print(hasattr(lock, '__enter__')) # True
print(hasattr(lock, '__exit__')) # True
def work():
global n
for i in range(1000000):
# # 上锁
# lock.acquire()
# n -= 1
# # 释放锁
# lock.release()
with lock:
n -= 1
if __name__ == '__main__':
# 两个线程共同操作n
t1 = threading.Thread(target=work)
t2 = threading.Thread(target=work)
t1.start()
t2.start()
t1.join()
t2.join()
print(n)
若未上锁,当数据较多时,2个进程共同操作全局变量n,由于时间问题,结果会出错
线程池
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
使用ThreadPoolExecutor()方法(推荐)
采用ThreadPoolExecutor(n),括号内是创建的线程数,不填的话,会默认开设当前计算机cpu核数的5倍
的线程。
#线程池:一次性开辟一些线程。我们用户直接给线程池子提交任务。线程任务的调度交给线程池完成。
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
def fn(name):
for i in range(1000):
print(name,i)
if __name__ == '__main__':
#创建线程池
with ThreadPoolExecutor(50) as t:
for i in range(100):
t.submit(fn,name=f"线程{i}")
#等待线程池中任务全部执行完毕,才继续执行(守护)
print('123')
使用ThreadPool方法(apply_async)
"""
1、简单描述同步和异步的区别
2、一个列表中有100个url地址(假设请求每个地址需要0.5秒),请设计程序一个程序,获取列表中的url地址,使用4个线程去发送这100个请求,计算出总耗时!
"""
import time
from multiprocessing.pool import ThreadPool
import queue
def download(q: queue.Queue):
while not q.empty():
print(q.get())
time.sleep(0.5)
q.task_done() # 这个任务做完了
def calc_time(func):
def wrap(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f"共花费时间{(end - start):.2f}秒")
return wrap
@calc_time
def main():
q = queue.Queue()
for i in range(100):
q.put(f'http://www.gushi.com/page={i}')
pool = ThreadPool(4) # 4个线程
pool.apply_async(download, args=(q,)) # pool中的多个线程异步地调用指定函数 这里是 download
q.join() # 等待所有的任务做完
print('finished!')
if __name__ == '__main__':
main()
# 运行结果
...
http://www.gushi.com/page=98
http://www.gushi.com/page=99
finished!
共花费时间50.35秒
协程
相关概念
并发:jmeter并发100个请求
并发:2个进程分别在2个CPU上并行运行
同步:IO操作,耗时,等待操作完毕
异步:不等到操作完毕
并发就是实现异步
实现异步的方法:多线程和协程
- 多线程:CPU调度多个线程(内核)
- 协程: 开发人员开启多个任务(用户)
背景
操作系统在线程等待IO的时候,会阻塞当前线程,切换到其它线程,这样在当前线程等待IO的过程中,其它线程可以继续执行。当系统线程较少的时候没有什么问题,但是当线程数量非常多的时候
,却产生了问题。一是系统线程会占用
非常多的内存空间
;二是过多的线程切换会占用
大量的系统时间
。
协程的出现有效解决上述两个问题,协程运行在线程之上,当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上。协程并没有增加线程数量,只是在线程的基础之上通过分时复用
的方式运行多个协程, 而且协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多。
基础概念
协程是一种比线程更加轻量级
的一种函数,核心思想是为了实现异步IO
。若干个协程任务,当某个任务遇到阻塞时,自动切换到非阻塞任务上。
使用
gevent实现协程
未使用gevent前,仅实现了并发效果,缺少IO检测(即遇到sleep时,未切换到另外的任务)
import time
"""
缺少IO检测
"""
def work1():
for i in range(5):
print('work1:听音乐...')
time.sleep(1)
yield
def work2():
for i in range(5):
print('work2:打游戏...')
time.sleep(1)
yield
def calc_time(func):
def wrap(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f'共花费{(end - start):.2f}秒')
return wrap
@calc_time
def main():
g1 = work1()
g2 = work2()
while True:
try:
next(g1)
next(g2)
except StopIteration:
break
if __name__ == '__main__':
main()
# 运行结果
...
work1:听音乐...
work2:打游戏...
work1:听音乐...
work2:打游戏...
共花费10.04秒
使用gevent后,实现了协程,当检测到IO(即遇到sleep时,自动切换到另外的任务),提高了执行效率
import time
from gevent import monkey
monkey.patch_all()
import gevent
def work1():
for i in range(5):
print('work1:听音乐...')
time.sleep(1)
def work2():
for i in range(5):
print('work2:打游戏...')
time.sleep(1)
def calc_time(func):
def wrap(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f'共花费{(end - start):.2f}秒')
return wrap
@calc_time
def main():
g1 = gevent.spawn(work1) # 创建协程1
g2 = gevent.spawn(work2) # 创建协程2
g1.join()
g2.join()
print('所有任务执行完毕!')
if __name__ == '__main__':
main()
# 运行效果
work1:听音乐...
work2:打游戏...
work1:听音乐...
work2:打游戏...
所有任务执行完毕!
共花费5.01秒
asyncio实现协程(推荐,python3.5之后)
python自带,比gevent效率高
- 异步执行的函数:前面加关键字
async
- asyncio.create_task:创建协程任务
- 需阻塞的函数/任务:前面加关键字
await
- asyncio.run():执行异步任务
import time
import asyncio # 异步IO库, 单线程实现并发 —— 协程
# async 关键字 await关键字
async def work1(): # aysnc 任务work1是异步的
for i in range(5):
print(f'work1:听音乐...')
await asyncio.sleep(1)
async def work2():
for i in range(5):
print(f'work2:打游戏...')
await asyncio.sleep(1)
async def tasks():
task1 = asyncio.create_task(work1()) # 任务一
task2 = asyncio.create_task(work2()) # 任务二
await task1
await task2
def calc_time(func):
def wrap(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print(f'共花费{(end - start):.2f}秒')
return wrap
@calc_time
def main():
asyncio.run(tasks())
if __name__ == '__main__':
main()