在学习《流畅的Python》这本书籍时,有一些理解不够透彻的知识点,包括eval函数、装饰器、闭包、可变与不可变数据类型、浅拷贝和深拷贝等。
eval函数
eval函数是python的一个内置函数,它的功能是返回传入字符串的表达式的结果。即:将字符串当成有效的表达式来求值并返回计算结果。
eval函数就是实现list、dict、tuple与str之间的转化,同样str函数把list、dict、tuple转为为字符串。
eval的语法
eval(expression[, globals[, locals]])
expression : 表达式。
globals : (可选参数)变量作用域,全局命名空间,如果被提供,则必须是一个字典对象。
locals : (可选参数)变量作用域,局部命名空间,如果被提供,可以是任何映射对象。
何为命名空间?
定义
名称到对象的映射。python是用命名空间来记录变量的轨迹的,命名空间是一个dictionary,键是变量名,值是变量值。各个命名空间是独立没有关系的,一个命名空间中不能有重名,但是不同的命名空间可以重名而没有任何影响。
分类
python程序执行期间会有2个或3个活动的命名空间(函数调用时有3个,函数调用结束后2个)。按照变量定义的位置,可以划分为以下3类:
Local,局部命名空间,每个函数所拥有的命名空间,记录了函数中定义的所有变量,包括函数的入参、内部定义的局部变量。
Global,全局命名空间,每个模块加载执行时创建的,记录了模块中定义的变量,包括模块中定义的函数、类、其他导入的模块、模块级的变量与常量。
Built-in,python自带的内建命名空间,任何模块均可以访问,放着内置的函数和异常。
生命周期
Local(局部命名空间)在函数被调用时才被创建,但函数返回结果或抛出异常时被删除。(每一个递归函数都拥有自己的命名空间)。
Global(全局命名空间)在模块被加载时创建,通常一直保留直到python解释器退出。
Built-in(内建命名空间)在python解释器启动时创建,一直保留直到解释器退出。
各命名空间创建顺序:python解释器启动 ->创建内建命名空间 -> 加载模块 -> 创建全局命名空间 ->函数被调用 ->创建局部命名空间
各命名空间销毁顺序:函数调用结束 -> 销毁函数对应的局部命名空间 -> python虚拟机(解释器)退出 ->销毁全局命名空间 ->销毁内建命名空间
python解释器加载阶段会创建出内建命名空间、模块的全局命名空间,局部命名空间是在运行阶段函数被调用时动态创建出来的,函数调用结束动态的销毁的。
python的全局命名空间存储在一个叫globals()的dict对象中;局部命名空间存储在一个叫locals()的dict对象中。可以用print (locals())来查看该函数体内的所有变量名和变量值。
1 | print(locals()) #打印显示所有的局部变量 |
参数查找
当后两个参数都为空时,很好理解,就是一个string类型的算术表达式,计算出结果即可。等价于eval(expression)。
当locals参数为空,globals参数不为空时,先查找globals参数中是否存在变量,并计算。
当两个参数都不为空时,先查找locals参数,再查找globals参数。
eval的使用演示
1 | #1.eval无参实现字符串转化 |
eval的使用与风险
eval虽然方便,但是要注意安全性,可以将字符串转成表达式并执行,就可以利用执行系统命令,删除文件等操作。比如用户恶意输入就会获得当前目录文件:
1 | >>>eval("__import__('os').system('dir')") |
全局变量和局部变量
全局变量:在模块内、在所有函数的外面、在class外面。
局部变量:在函数内、在class的方法内。
函数内部使用与全局变量同名的局部变量
1 | a="hello" #全局变量a |
函数内部调用全局变量
1 | a = "hello" #全局变量a |
下面便能修改全局变量的值了:
1 | a = "hello" #全局变量a |
注:在方法内部的变量是在=号前面的,那肯定是局部变量。如果是第一次出现在=号后面的,那肯定是调用的全局变量;全局变量可以在函数里面调用,局部变量只能在对应的函;数里面调用,在该函数外面任何地方都无法被调用。
装饰器
装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。
更易懂版本:假设我们要增强一个函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改该函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
实现函数运行计时器:
1 | import time |
对于加入装饰器的fast()
函数等价于fast=metric(fast)=wrapper
,然后fast(11,22)=metric(fast)(11,22)=wrapper(11,22)
。由于metric()
是一个decorator,返回一个函数,所以,原来的fast()
函数仍然存在,只是现在同名的fast
变量指向了新的函数,于是调用fast()
将执行新函数,即在metric()
函数中返回的wrapper()
函数。
wrapper()
函数的参数定义是(*args, **kw)
,因此,wrapper()
函数可以接受任意参数的调用。在wrapper(11, 22)
函数内,首先用t0
记录当前时间,然后调用原始函数fast(11, 22)
,然后将原始函数返回值赋值给result,计算执行时间并打印,最后返回结果。
global和nonlocal
在python中,global和nonlocal的作用都是可以实现代码块内变量使用外部的同名变量,但其中有很明显的区别。
global
global是声明代码块中的变量使用外部全局的同名变量。
1 | a = 1 |
其中,b
未被改变。这里涉及到python变量定义域规则:change(b)
把传入的参数b
当做了局部变量,相当于change(b=1)
,所以函数定义里面改变b
的值不会改变函数外部的b
的值,即外面的b
和里面的b
不是同一个;而全局变量a
不一样,要在函数里修改a
的值,必须要先声明,否则函数会把a
当成局部变量并会报局部变量a
为赋值的错误。
nonlocal
nolocal 的使用场景就比较单一,它是使用在闭包中的,让变量使用外层的同名变量。
1 | def foo(func): |
在刷题中,深有体会,尤其是dfs()
中,要修改计数器的值,需要声明为nonlocal
,而list
则不用,因为它是可变的。
总结
global的作用对象是全局变量,nonlocal的作用对象是外层变量(很显然就是闭包这种情况)。
可变数据类型与不可变数据类型
可变数据类型:列表、字典dict()
、集合set(),.add(),.pop(),pop()是先进先pop()
。
不可变数据类型:整数、字符串、元组。
表示形式和操作:
表示 | ||
---|---|---|
列表 | [] | .append(object);.pop(indx)默认pop出最后append的即最后一个,等同于.pop(-1),.pop(i)即pop第i个;其他的切片等操作,+也是拼接两个列表为一个列表的操作,没有-操作。 |
字典 | dict()或者{} | .keys(); .values(); .items(); .get(key); .popitem,pop最后一组键值对;.pop(key); |
集合 | set() | .add(element);.pop(),无参数,默认pop出第一个;.remove(element),移除某个元素。 |
整数 | 直接a=1 | |
字符串 | 直接a=’123’ | |
元组 | tup=(1,),不能tup=(1);前者是元组,后者是int。空元组tup=()或者tup=tuple() | 不可删除元素,不可增加元素,不可赋值,可以切片获取元素tup[i:j];len(tuple) 计算元组元素个数;max(tuple)返回元组中元素最大值;min(tuple)返回元组中元素最小值;tuple(iterable) 将可迭代系列转换为元组; |
集合set()的元素相当于字典里key,必须是不可变数据类型。列表则可变不可变都可以。
浅拷贝和深拷贝
浅拷贝:
- 对列表浅拷贝:
list[:]
。 - 调用对象的拷贝方法:
list.copy()
。 - 调用:
copy.copy()
。
深拷贝:
- 调用
copy.deepcopy()
。
两种拷贝的异同:
可变对象 | 不可变对象 | |
---|---|---|
浅拷贝 | 拷贝外面的壳子,里面的元素不拷贝。 | 拷贝新对象 |
深拷贝 | 壳子和里面的元素都拷贝。 | 拷贝新对象 |
当 copy() 的时候(浅拷贝),列表、字典、集合等这类可变数据类型复合而成的对象仍然只是拷贝了引用,也就是贴标签,并没有建立一个新的对象,我们把这种拷贝方式叫做浅拷贝;若深拷贝相当于赋值了新对象,然后拷贝新对象的引用或者说是贴标签。
下面的例子:
1 | first = {'name':'rocky','language':['python','c++','java']} |
Python中的传参
传值和传引用是C/C++中的概念,Python中的一切皆为对象,实参像形参传递的是对象的引用值,和Python赋值意思一直。
因此,Python函数传递的是对象的引用值,非传值或传引用。但是如果对象是不可变的,和C/C++语言中传值类似。如果对象是可变的,感觉和C/C++语言中传引用类似。
不可变数据类型:整数、字符串和元组。
可变数据类型:列表、字典dict()
、集合set()
。