- *args 和 **kwargs
- 装饰器和闭包
- 垃圾回收机制
- 赋值、深拷贝、浅拷贝
*args和**kwargs
*和**在python中主要有三方面的用途
- 对可迭代对象进行拆分
- 可变变量的赋值
- 函数的可选参数标志
对可迭代对象进行拆分
典型的可迭代对象包括元组(tuple)、列表(list)、集合(set)、字典(dict)等等
print("a", "b", "c")
print(*("a", "b", "c")) #元组
print(*["a", "b", "c"]) #列表
print(*{"a", "b", "c"}) #集合
print(*{"a": 1, "b": 2, "c": 3}) #注意:对字典拆解时只拆解key
print(*"abc")
可变变量的赋值
对于一个可迭代对象列表,如果想要将其中的元素赋值给变量。写法如下:
a, *b, c = {1, 2, 3, 4, 5, 6}
函数的可选参数标志
如果是单星号*标记就是可选的位置参数(positional arguments), 如果是双星号**标记就是可选的关键词参数(keyword arguments), 如:
def func(a, *args, **kwargs):
print(a)
print(args)
print(kwargs)
func(1, 2, 3, c = 4, d = 3)
kwargs.get()方法和kwargs.pop()方法:
kwargs.get("denominator", 1)会从kwargs这个字典中读取"denominator"这个key对应的值,如果没有这个key,就会返回1。这样写可以避免不断地写if else语句。
kwargs.pop()与kwargs.get()不同的地方在于,kwargs.pop()读取某个key对应的值后,kwargs这个字典中的这个键值对就会被删除。
参考:【Python】函数中的*args和**kwargs是什么?
arg是argument的简称, *args用来发送一个非键值对的可变数量的参数列表给一个函数。倘若确定传入一个参数,可以将参数单独写出来。但是不确定需要传入多少个参数,可以使用*args占位代替,即使传入非常多的参数,也可以逐一显示。
**kwargs允许将不定长度的键值对,作为参数传递给函数。如果想要在一个函数里面处理带名字的参数,需要使用**kwargs。
在一个函数中,普通的参数与*args、**kwargs在使用时的顺序。如果想要在函数中同时使用所有的三种参数,顺序如下:(必选参数、默认参数、*args, **kwargs)
func(normal_args, *args, **kwargs)
Python的装饰器与闭包
闭包是Python装饰器的基础。
要理解闭包,需要先了解Python中的变量作用域规则
变量作用域规则
- 函数能访问全局变量
- 在一个嵌套函数中,内层函数能够访问在外层函数中定义的局部变量
闭包
嵌套函数就是闭包。闭包是指延伸了作用域的函数,在其中能够访问未在函数定义体中定义的非全局变量。未在函数定义体中定义的非全局变量一般都是在嵌套函数中出现的。
当Python函数中的全局变量或者自由变量是不可变对象(数字、字符串、元组等)时,是只能读取,无法更新的。
装饰器
一、什么是装饰器?
装饰器是给现有的模块增添新的小功能,可以对原函数进行功能扩展,而且还不需要修改原函数的内容,也不需要修改原函数的调用。装饰器的使用符合面向对象编程的开放封闭原则。
开放封闭原则主要体现在两个方面:
- 对扩展开放,意味着有新的需求或者变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立其工作,而不要对类进行任何修改
二、为什么要用装饰器?
python里万物皆是对象,万物皆可传参。函数也可以作为函数的参数进行传递
装饰器是可调用对象,
def func1():
print("我是子函数1")
def func2(name):
print("子函数2开始")
name()
print("子函数2结束")
if __name__ == "__main__":
func = func1
func()
print("-" * 20)
func2(func)
对于两个函数的执行时间分别是多少,需要分别对两个函数进行改写。
import time
def func1():
print("我是子函数1")
time.sleep(2)
def count_time(func):
def wrapper():
t1 = time.time()
func()
print("执行时间为:" , time.time() - t1)
return wrapper
if __name__ == "__main__":
func1 = count_time(func1)
func1() # 执行func1就相当于执行wrapper()
这里的count_time是一个装饰器,装饰器函数里面定义一个wrapper函数, 把func这个函数当作参数传入,函数实现的功能是把func包裹起来,并且返回wrapper函数。wrapper函数体就是要实现装饰器的内容。
三、装饰器的语法糖@
使用装饰器的是,默认转入的参数就是被装饰的函数。
import time
def count_time(func):
def wrapper():
t1 = time.time()
func()
print("执行时间为:", time.time() - t1)
return wrapper
@count_time
def func1():
print("我是子函数1")
time.sleep(2)
if __name__ == "__main__":
func1() #用语法糖之后,就可以直接调用该函数
四、带参数的装饰器
装饰器函数也是函数,函数可以进行参数传递,写一个带参数的装饰器
import time
def count_time_args(msg = None):
def count_time(func):
def wrapper(*args, **kwargs):
t1 = time.time()
func(*args, **kwargs)
print("{}执行时间为:".format(msg), time.time() - t1)
return wrapper
return count_time
@count_time_args(msg = "jc1")
def fun_one():
time.sleep(1)
@count_time_args(msg = "jc2")
def fun_two():
time.sleep(1)
@count_time_args(msg = "jc3")
def fun_three():
time.sleep(1)
if __name__ == "__main__":
fun_one()
fun_two()
fun_three()
五、类装饰器
在python中,可以用类来实现装饰器的功能,称之为类装饰器。类装饰器的实现调用类里面的__call__函数。类装饰器的写法比装饰器函数的写法更为简单。
当将类作为一个装饰器,工作流程:
- 通过__init__()方法初始化类
- 通过__call__()方法调用真正的装饰方法
import time
class Decorator:
def __init__(self, func):
self.func = func
print("执行类的__init__方法")
def __call__(self, *args, **kwargs):
print("进入__call__函数")
t1 = time.time()
self.func(*args, **kwargs)
print("执行时间为:", time.time() - t1)
@Decorator
def jc():
print("我是jc函数")
time.sleep(2)
def jc2():
print("我是jc2函数")
time.sleep(1)
@Decorator
def func(name):
print("进入func函数")
name()
print("func函数结束")
if __name__ == "__main__":
jc()
print("-" * 20)
func(jc2)
一个函数可以被多个装饰器进行修饰,在装饰器修饰的函数,在执行的时候先执行原函数的功能,然后再由里向外依次执行装饰器的内容。
重点:【Python】一文弄懂python装饰器(附源码例子)
参考:
写一个装饰器计算函数的运行时间代码
参考:
python-装饰器实现函数的运行时间 - 知乎 (zhihu.com)
import time
def time_it(func):
def inner():
start = time.time()
func()
end = time.time()
print(end - start)
return inner
@time_it
def func():
time.sleep(2)
print("func has sleeping 2 s")
if __name__ == '__main__':
func()
Python垃圾回收(GC)
- 引用计数(主要)【缺点:维护性高、循环引用不能解决】
- 标记-清除
- 分代回收【垃圾回收 = 垃圾检测 + 释放】
python中sorted函数对字典按key排序和按value排序
python的sorted函数对字典按key排序和按value排序
sorted函数, sorted(iterable, key, reverse), sorted一共有iterable, key, reverse三个参数。
赋值、浅拷贝、深拷贝
Python中的赋值(复制)、浅拷贝与深拷贝 - 知乎 (zhihu.com)
- 变量:是一个系统表的元素,拥有指向对象的连接空间
- 对象:被分配的一块内存,存储其所代表的值
- 引用:是自动形成的从变量到对象的指针
- 类型:属于对象,而非变量
- 不可变对象:一旦创建就不可修改的对象,包括字符串、元组、数值类型
- 可变对象:可以修改的对象,包括列表、字典、集合
(不可变对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。)
(可变对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的地址,通俗点说就是原地改变。)
赋值:只是复制了新对象的引用,不会开辟新的内存空间
并不会产生一个独立的对象单独存在,只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。
浅拷贝: 切片操作,工厂函数,copy模块中的copy函数。
如: lst = [1,2,[3,4]]
切片操作:lst1 = lst[:] 或者 lst1 = [each for each in lst]
工厂函数:lst1 = list(lst)
copy函数:lst1 = copy.copy(lst)
浅拷贝之所以称为浅拷贝,是它仅仅只拷贝了一层,拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已,在lst中有一个嵌套的list[3,4],如果我们修改了它,情况就不一样了。
浅复制要分两种情况进行讨论:
1)当浅复制的值是不可变对象(字符串、元组、数值类型)时和“赋值”的情况一样,对象的id值(id()函数用于获取对象的内存地址)与浅复制原来的值相同。
2)当浅复制的值是可变对象(列表、字典、集合)时会产生一个“不是那么独立的对象”存在。有两种情况:
第一种情况:复制的对象中无复杂子对象,原来值的改变并不会影响浅复制的值,同时浅复制的值改变也并不会影响原来的值。原来值的id值与浅复制原来的值不同。
第二种情况:复制的对象中有复杂子对象(例如列表中的一个子元素是一个列表),如果不改变其中复杂子对象,浅复制的值改变并不会影响原来的值。 但是改变原来的值中的复杂子对象的值会影响浅复制的值。
深拷贝:和浅拷贝对应,深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。深拷贝出来的对象是一个全新的对象,不再与原来的对象有任何关联。
所以改变原有被复制对象不会对已经复制出来的新对象产生影响。
只有一种形式,copy模块中的deepcopy函数
深浅拷贝对比
不可变对象类型,没有被拷贝的说法,即便是用深拷贝,查看id的话也是一样的,如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。
一句话就是,不可变类型,不管是深拷贝还是浅拷贝,地址值和拷贝后的值都是一样的。
- 赋值: 值相等,地址相等
- copy浅拷贝:值相等,地址不相等
- deepcopy深拷贝:值相等,地址不相等
python集合元素可以是列表吗
python中的不可变类型:Numble(数字),Tuple(元组), String(字符串)
python中的可变类型:List 列表, Dict 字典, Set 集合
集合的元素不可以是列表,只能固定数据类型, 数值、字符串、元组。
Pytorch并行计算
Pytorch多进程并行计算
DistributedDataParallel(DDP):多进程并行计算,支持多节点分布式计算
DataParallel(DP):单进程多线程计算
DDP通常性能也更好,因为它不仅规避了Python多线程的全局解释器锁争用(GIL contention)造成的性能开销,而且还不需要在多GPU训练中频繁复制同步模型、分发输入数据和收集模型输出。
分布式训练:同步训练、异步训练。同步训练利用AllReduce来整合不同worker计算gradient, 异步训练则是基于参数服务器架构。
AllReduce是一类算法,将不同机器中的数据整合之后再将结果分发给各个机器。优化网络带宽的占用或者延迟。AllReduce分为主从式架构、环形架构、全连接架构。
功能上:
- DDP的原理是多进程,因此DDP支持多机多卡的分布式计算,而DP是但经常多线程,因此最高只支持单机多卡;
- DDP支持模型并行(model parallel),可以把一个模型拆成几个阶段来跑,而DP还不支持。
性能上:
- 正是因为DDP基于多进程(通常推荐1个GPU匹配一个工作进程),所以不像DP那样基于单进程多线程的并行性能受到GIL争用开销的阻碍。
- 在单机多卡的情况下,DP需要在训练中频繁在多卡之间复制模型以完成同步,需要分发(scatter)输入和收集(gather)输出,而DDP采用的All-Reduce算法采取聚合通信(collective communication)的方式收集梯度,其性能更好。
launch 使用 multiprocessing.spawn 创建 nprocs个新进程, 每个进程都执行worker函数。
初始化进程组 - DDP包装模型 - 准备数据 - 进行训练 - 销毁进程组
进程组后端负责进程组聚合通信 Pytorch 分布式训练GPU使用NCCL, CPU使用Cloo