目录
闭包
装饰器
普通用法
多层装饰器
设计模式
单例模式
工厂模式
多线程
基础使用
得到当前的线程
守护线程
线程阻塞join方法
线程锁 Lock
递归锁对象RLock
闭包
例如要实现一个存钱的功能,可以这么做
account_amount = 0
def atm(num,deposit = True):
global account_amount
if deposit:
account_amount += num
else:
account_amount -= num
print(account_amount)
这样是运用了外部变量构建函数实现,但是这样的缺点是,外部变量容易被篡改
那么我们可以运用闭包
def card(account_num=0):
def ATM(num,deposit=True):
nonlocal account_num
if deposit:
account_num += num
else:
account_num -= num
print(account_num)
return ATM
1.什么是闭包
定义双层嵌套函数,内层函数可以访问外层函数的变量
将内存函数作为外层函数的返回,此内层函数就是闭包函数
2.闭包的好处和缺点
- 优点:不定义全局变量,也可以让函数持续访问和修改一个外部变量
- 优点:闭包函数引用的外部变量,是外层函数的内部变量。作用域封闭难以被误操作修改
- 缺点:额外的内存占用
3.nonlocal关键字的作用
在闭包函数(内部函数中)想要修改外部函数的变量值,需要用nonlocal声明这个外部变量
装饰器
普通用法
可以参考这篇博客
python装饰器详解_谦虚且进步的博客-CSDN博客
装饰器本质上是一个Python函数(其实就是闭包),它可以让其他函数在不需要做任何代码变动的前提下增加额外功能
一般写法:
def func1():
print('我是func1')
def func2(func):
def inner():
print('我是func2开始')
func()
print('我是func2结束')
return inner
a = func2(func1)
a()
语法糖写法
def func2(func):
def inner():
print('我是func2开始')
func()
print('我是func2结束')
return inner
@func2
def func1():
print('我是func1')
func1()
多层装饰器
def func2(func):
def inner():
print('我是func2开始')
func()
print('我是func2结束')
return inner
def func3(func):
def inner():
print('func3开始')
func()
print('func3结束')
return inner
@func3
@func2
def func1():
print('我是func1')
func1()
# func3开始
# 我是func2开始
# 我是func1
# 我是func2结束
# func3结
相当于
func3(func2(func1))
设计模式
单例模式
对于一个这样的类
class tools:
pass
如果我在很多地方需要使用它,一般会这么做
a = tools()
b = tools()
print(a)
print(b)
# <__main__.tools object at 0x00000210AA9D7710>
# <__main__.tools object at 0x00000210AA9DD208>
可以看到不同的实例,内存是不一样的,也就是说这两个实例是独立的。
但是如果这是一个工具类,我们在不同的地方使用同一个实例就可以了
tool = tools()
a=tool
b=tool
print(a)
print(b)
# <__main__.tools object at 0x00000210AA9DD278>
# <__main__.tools object at 0x00000210AA9DD278>
一般开发中,会把下面的代码放在一个单独的文件,然后别的地方要用的时候直接import调用tool就可以了
class tools:
pass
tool = tools()
工厂模式
多线程
基础使用
python多线程的基础使用:
import threading
thread_obj = threading.Thread([group [, target [, name [, args [,kwarngs[,daemon=None]]]]])
#
# group:暂时无用,未来功能的预留参数
# target:执行的目标任务名
# args:以元组或者列表的方式给执行任务传参
# kwargs:以字典方式给执行任务传参
# name:线程名,一般不用设置
# daemon 参数将显式地设置该线程是否为守护模式
#启动线程,让线程开始工作
thread_obj.start()
通过小案例来体会多线程
import threading
import time
def func1():
while 1:
print(111)
time.sleep(1)
def func2():
while 1:
print(222)
time.sleep(1.5)
thread_1 = threading.Thread(target=func1)
thread_2 = threading.Thread(target=func2)
thread_1.start()
thread_2.start()
当函数需要传递参数时,就可以用到args,kwargs了,需要注意的是args必须是元组或者列表(默认元组),kwargs必须是字典
import threading
import time
def func1(msg):
while 1:
print(msg)
time.sleep(1)
def func2(msg1,msg2):
while 1:
print(msg1)
print(msg2)
time.sleep(1.5)
thread_1 = threading.Thread(target=func1,args=(111,))
thread_2 = threading.Thread(target=func2,kwargs={'msg1':222,'msg2':333})
thread_1.start()
thread_2.start()
#
得到当前的线程
import threading
import threading
import time
def func1(msg):
while 1:
time.sleep(1)
print(threading.currentThread())
thread_1 = threading.Thread(target=func1,args=[111],name='lalala')
thread_1.start()
可以使用threading.currentThread()得到当前的进程,如果指定了线程名字,会显示进程名字
守护线程
启动python时,会生成一个主线程,我们可以通过之前的方法生成子线程,当所有子线程都结束了,主线程才会结束
threading提供了一个daemon参数,默认是False,如果是True,则表示主线程不会等待这个线程,等别的子线程结束就直接结束主线程
例如
import threading
import time
def func1():
for i in range(5):
time.sleep(1)
print(threading.currentThread())
def func2():
while 1:
print(222)
time.sleep(1.5)
thread_1 = threading.Thread(target=func1,name='lalala')
thread_2 = threading.Thread(target=func2,daemon=True)
thread_1.start()
thread_2.start()
func1内的代码执行五次后就会结束运行
线程阻塞join方法
join(timeout=None)
- 等待,直到线程终结。这会阻塞调用这个方法的线程,直到被调用 join() 的线程终结 -- 不管是正常终结还是抛出未处理异常 -- 或者直到发生超时,超时选项是可选的。
- 当 timeout 参数存在而且不是
None
时,它应该是一个用于指定操作超时的以秒为单位的浮点数或者分数。因为 join() 总是返回None
,所以你一定要在 join() 后调用 is_alive() 才能判断是否发生超时 -- 如果线程仍然存活,则 join() 超时。- 当 timeout 参数不存在或者是
None
,这个操作会阻塞直到线程终结。- A thread can be joined many times.
- 如果尝试加入当前线程会导致死锁, join() 会引起 RuntimeError 异常。如果尝试 join() 一个尚未开始的线程,也会抛出相同的异常
相当于给进程增加阻塞,需要等待timeout时间
我们分成三种情况分析这个功能
1、等待时间小于执行时间
def target():
time.sleep(7)
print("线程{}已退出".format(current_thread().name))
thread01 = Thread(target=target,name="1")
thread01.start()
thread01.join(timeout=5)
print(11111)
在这里,五秒之后输出11111,再经过2秒后输出 线程1已退出
相当于这里join阻碍了主线程,设置主线程需要等待我的thread01 5秒才能运行
2、等待时间大于执行时间
def target():
time.sleep(5)
print("线程{}已退出".format(current_thread().name))
thread01 = Thread(target=target,name="1")
thread01.start()
thread01.join(timeout=7)
print(11111)
再这里,五秒之后同时输出两个
也就是说join函数在timeout时间结束或者执行时间结束都会返回
3、设置dasman
def target():
time.sleep(5)
print("线程{}已退出".format(current_thread().name))
thread01 = Thread(target=target,daemon=True,name="1")
thread01.start()
thread01.join(timeout=3)
print(11111)
3秒后输入11111,不输出 其他
说明join可以无视守护线程的限制进行阻碍
线程锁 Lock
实现原始锁对象的类。一旦一个线程获得一个锁,会阻塞随后尝试获得锁的线程,直到它被释放;任何线程都可以释放它。
原始锁处于 "锁定" 或者 "非锁定" 两种状态之一。它被创建时为非锁定状态。它有两个基本方法, acquire() 和 release() 。当状态为非锁定时, acquire() 将状态改为 锁定 并立即返回。当状态是锁定时, acquire() 将阻塞至其他线程调用 release() 将其改为非锁定状态,然后 acquire() 调用重置其为锁定状态并返回。 release() 只在锁定状态下调用; 它将状态改为非锁定并立即返回。如果尝试释放一个非锁定的锁,则会引发 RuntimeError 异常。
当多个线程同时处理数据时,可能会发生错误,其实就是通过锁的方式 对数据进行保护
我们通过一个例子查看这个的作用
import time
import threading
lock = threading.Lock()
cash = 1000
def func1(money):
global cash
if cash>money:
time.sleep(1)
cash -= money
print('ok')
print(cash)
else:
print('no')
th1 = threading.Thread(target=func1,args=[600])
th2 = threading.Thread(target=func1,args=[600])
# ok
# ok
# -200
# -200
由于两个线程同时开始,都会通过 cash>money的判断,从而使得同时减去600,输出负数
def func1(money):
global cash
lock.acquire()
if cash>money:
time.sleep(1)
cash -= money
print('ok')
print(cash)
else:
print('no')
lock.release()
th1 = threading.Thread(target=func1,args=[600])
th2 = threading.Thread(target=func1,args=[600])
#
th1.start()
th2.start()
# ok
# 400
# no
增加线程锁之后,在th1调用时,不允许th2调用,起到了保护数据的作用
下面这篇文章说到,锁保证了python语句的 原子性 ,我觉得很有道理,可以参考一下
关于Python中最基本的锁的理解 - 知乎
递归锁对象RLock
重入锁是一个可以被同一个线程多次获取的同步基元组件。在内部,它在基元锁的锁定/非锁定状态上附加了 "所属线程" 和 "递归等级" 的概念。在锁定状态下,某些线程拥有锁 ; 在非锁定状态下, 没有线程拥有它。
若要锁定锁,线程调用其 acquire() 方法;一旦线程拥有了锁,方法将返回。若要解锁,线程调用 release() 方法。 acquire()/release() 对可以嵌套;只有最终 release() (最外面一对的 release() ) 将锁解开,才能让其他线程继续处理 acquire() 阻塞
上面的Lock是不能被重复访问的,否则会陷入死锁状态
例如
import threading
lock = threading.Lock()
print(22222)
lock.acquire()
lock.acquire()
print(1111)
lock.release()
lock.release()
输出22222只会就陷入了死锁状态,但是换成RLock就可以
我们可以看一下Rlock的源码
[threading]
class _RLock:
def __init__(self):
self._block = _allocate_lock() # _thread模块中定义一个锁对象的方法
self._owner = None # 用来标记哪个线程获取了锁
self._count = 0 # 计数器
def acquire(self, blocking=True, timeout=-1):
me = get_ident()
if self._owner == me:
self._count += 1
return 1
rc = self._block.acquire(blocking, timeout)
if rc:
self._owner = me
self._count = 1
return rc
def release(self):
if self._owner != get_ident():
raise RuntimeError("cannot release un-acquired lock")
self._count = count = self._count - 1
if not count:
self._owner = None
self._block.release()
这里的 me代表方法的调用者,如果调用者就是锁的拥有者,则计数器加1,否则进行阻塞自己(也可能是获得锁)
可以看到,在重复得到锁的过程中,实际上只有第一次调用了acquire()方法,后面只进行了加1操作,解开锁的过程类似
但是需要注意的是,acquire()后必须搭配release(),若两者数量不相同,则仍然会陷入锁死
GIL锁
全局解释器锁(英语:Global Interpreter Lock,缩写GIL)
是计算机程序设计语言解释器用于同步线程的一种机制,它使得任何时刻仅有一个线程在执行。
即便在多核心处理器上,使用GIL的解释器也只允许同一时间执行一个线程。
也就是线程在运行的时候获得GIL,当它到达IO时释放GIL
那为什么有GIL锁这个东西呢,下面这个视频讲的很好,可参考
Python速度慢的罪魁祸首,全局解释器锁GIL_哔哩哔哩_bilibili