0%

《流畅的Python》的一些笔记

在学习《流畅的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
2
3
4
5
6
7
8
print(locals())  #打印显示所有的局部变量
'''

{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001B22E13B128>,
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:/pythoyworkspace/file_demo/Class_Demo/pachong/urllib_Request1.py',
'__cached__': None, 's': '1+2+3*5-2', 'x': 1, 'age': 18}

Process finished with exit code 0

参数查找

当后两个参数都为空时,很好理解,就是一个string类型的算术表达式,计算出结果即可。等价于eval(expression)。

当locals参数为空,globals参数不为空时,先查找globals参数中是否存在变量,并计算。

当两个参数都不为空时,先查找locals参数,再查找globals参数。

eval的使用演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#1.eval无参实现字符串转化
s = '1+2+3*5-2'
print(eval(s))
# 16

#2.字符串中有变量也可以
x = 1
print(eval('x+2'))
# 3

#3.字符串转字典
print(eval("{'name':'linux', 'age':'18'}"))
# {'name': 'linux', 'age': '18'}

#4.eval传递全局变量参数,注意字典里的:age中的age没有带引号,说明它是个变量,而不是字符串。
#这里两个参数都是全局的
print(eval("{'name':'linux', 'age':age}", {"age":1822}))
# {'name': 'linux', 'age': 1822}
print(eval("{'name':'linux', 'age':age}", {"age":1822},{"age":1823}))
# {'name': 'linux', 'age': 1823}

#eval传递本地变量,既有global和local时,变量值先从local中查找。
age = 18
print(eval("{'name':'linux', 'age':age}", {"age":1822}, locals()))
# {'name': 'linux', 'age': 18}
print("-----------------")

print(eval("{'name':'linux','age':age}"))
# {'name': 'linux', 'age': 18}

eval的使用与风险

eval虽然方便,但是要注意安全性,可以将字符串转成表达式并执行,就可以利用执行系统命令,删除文件等操作。比如用户恶意输入就会获得当前目录文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
>>>eval("__import__('os').system('dir')")
驱动器 C 中的卷是 OS
卷的序列号是 B234-8A38

C:\Users\Robot_TENG 的目录

2019-07-01 09:11 <DIR> .
2019-07-01 09:11 <DIR> ..
2017-11-23 16:15 <DIR> .android
2018-12-23 00:02 <DIR> .conda
2018-12-06 19:08 20 .dbshell
2017-12-01 19:28 <DIR> .eclipse
2018-01-22 22:46 <DIR> .idea-build
2017-12-31 14:49 <DIR> .IdeaIC2017.1
2018-01-22 21:21 <DIR> .IdeaIC2017.2
2019-07-01 09:11 <DIR> .ipynb_checkpoints
2018-12-19 20:04 <DIR> .ipython
2019-07-01 09:30 <DIR> .jupyter
2017-12-01 16:11 <DIR> .m2
2017-12-31 23:14 0 .mongorc.js
2019-02-03 22:52 <DIR> .p2
2018-07-16 22:04 <DIR> .PyCharm2016.1
2018-12-06 19:49 <DIR> .rdm
2018-01-22 22:09 580 .scala_history
2018-12-06 19:19 <DIR> .vscode
2019-06-21 16:37 <DIR> 3D Objects
2019-06-21 16:37 <DIR> Contacts
2019-07-01 16:21 <DIR> Desktop
2019-06-28 16:34 <DIR> Documents
2019-06-28 10:26 <DIR> Downloads
2018-09-11 22:24 <DIR> Evernote
2019-06-21 16:37 <DIR> Favorites
2018-08-02 23:58 <DIR> HBuilder
2018-08-03 00:00 <DIR> HBuilder settings
2018-08-03 00:02 <DIR> HBuilderProjects
2019-06-21 16:37 <DIR> Links
2019-06-21 16:37 <DIR> Music
2018-03-18 00:22 <DIR> Oracle
2019-06-21 16:37 <DIR> Pictures
2019-06-21 16:37 <DIR> Saved Games
2019-06-21 16:37 <DIR> Searches
2018-12-23 00:47 690 Untitled.ipynb
2019-07-01 09:11 72 Untitled1.ipynb
2019-06-30 18:43 <DIR> Videos
2019-01-13 18:20 <DIR> Yinxiang Biji
5 个文件 1,362 字节
34 个目录 72,365,862,912 可用字节

全局变量和局部变量

全局变量:在模块内、在所有函数的外面、在class外面。

局部变量:在函数内、在class的方法内。

函数内部使用与全局变量同名的局部变量

1
2
3
4
5
6
7
a="hello"  #全局变量a
def test():
a = "hell0 local" #定义了一个局部变量a
b = a #test方法里之后再调用a时,都是局部的a
print(b + ",",a)
test()
# hell0 local, hell0 local

函数内部调用全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = "hello"  #全局变量a
c = '123'
ls = [] # 可变数据类型
def test():
c += ' local'
# 想改变全局变量的值,但因为没声明,函数会把c当成局部变量去找,
# 发现函数内未定义局部变量c,故会报错。
ls.append(1)
# ls不会遇到这个问题,因为我们没有给ls赋值,我们只是调用ls.append,
# 也就是说,我们利用了列表是可变的对象这一事实。
a = "hell0 local" # 定义了一个局部变量a
b = a #test方法里之后再调用a时,都是局部的a
print(b + ",",a)
test()

下面便能修改全局变量的值了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
a = "hello"  #全局变量a
c = '123'
ls = [] # 可变数据类型
def test():
global c
c += ' local'
print(c)
# 想改变全局变量的值,但因为没声明,函数会把c当成局部变量去找,
# 发现函数内未定义局部变量c,故会报错。
ls.append(1)
# ls不会遇到这个问题,因为我们没有给ls赋值,我们只是调用ls.append,
# 也就是说,我们利用了列表是可变的对象这一事实。
a = "hell0 local" # 定义了一个局部变量a
b = a #test方法里之后再调用a时,都是局部的a
print(b + ",",a)
test()

# 123 local
# hell0 local, hell0 local

注:在方法内部的变量是在=号前面的,那肯定是局部变量。如果是第一次出现在=号后面的,那肯定是调用的全局变量;全局变量可以在函数里面调用,局部变量只能在对应的函;数里面调用,在该函数外面任何地方都无法被调用。

装饰器

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

更易懂版本:假设我们要增强一个函数的功能,比如,在函数调用前后自动打印日志,但又不希望修改该函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

实现函数运行计时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import time
import functools
def metric(fn):
@functools.wraps(fn)
def wrapper(*args, **kws):
t0 = time.time()
result = fn(*args, **kws)
elapse = time.time() - t0
print('%s executed in %s ms' % (fn.__name__, elapse))
return result
return wrapper

# 测试
@metric
def fast(x, y):
time.sleep(0.0012)
return x + y

@metric
def slow(x, y, z):
time.sleep(0.1234)
return x * y * z

f = fast(11, 22)
s = slow(11, 22, 33)

if f != 33:
print('测试失败!')
elif s != 7986:
print('测试失败!')

对于加入装饰器的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
2
3
4
5
6
7
8
9
10
a = 1
b = 1
def change(b):
global a
a += 1
b += 1
print("函数内部的a的值:", a) # 2
change(b)
print("调用change函数后, 函数外部的a的值:", a) # 2
print("调用change函数后, 函数外部的b的值:", b) # 1

其中,b未被改变。这里涉及到python变量定义域规则:change(b)把传入的参数b当做了局部变量,相当于change(b=1),所以函数定义里面改变b的值不会改变函数外部的b的值,即外面的b和里面的b不是同一个;而全局变量a不一样,要在函数里修改a的值,必须要先声明,否则函数会把a当成局部变量并会报局部变量a为赋值的错误。

nonlocal

nolocal 的使用场景就比较单一,它是使用在闭包中的,让变量使用外层的同名变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def foo(func):
a = 1
print("外层函数a的值", a)

def wrapper():
func()
nonlocal a
a += 1
print("经过改变后,里外层函数a的值:", a)

return wrapper

@foo
def change():
print("nolocal的使用")

change()

在刷题中,深有体会,尤其是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
first = {'name':'rocky','language':['python','c++','java']}
second = first.copy()
print(second)
print(id(first))
print(id(second))

print('-'*50)
second['language'].remove('java')
print(first)
print(second)

print(id(first['language']))
print(id(second['language']))

print('-'*50)
first = {'name':'rocky','language':['python','c++','java']}
import copy
second = copy.deepcopy(first)
print(id(first))
print(id(second))
print(id(first['language']))
print(id(second['language']))
second['language'].remove('java')
print(first)
print(second)
# {'name': 'rocky', 'language': ['python', 'c++', 'java']}
# 140619041889728
# 140619045528032
# --------------------------------------------------
# {'name': 'rocky', 'language': ['python', 'c++']}
# {'name': 'rocky', 'language': ['python', 'c++']}
# 140619021892576
# 140619021892576
# --------------------------------------------------
# 140619041911184
# 140619023111744
# 140619045611552
# 140619045610112
# {'name': 'rocky', 'language': ['python', 'c++', 'java']}
# {'name': 'rocky', 'language': ['python', 'c++']}

Python中的传参

传值和传引用是C/C++中的概念,Python中的一切皆为对象,实参像形参传递的是对象的引用值,和Python赋值意思一直。

因此,Python函数传递的是对象的引用值,非传值或传引用。但是如果对象是不可变的,和C/C++语言中传值类似。如果对象是可变的,感觉和C/C++语言中传引用类似。

不可变数据类型:整数、字符串和元组。

可变数据类型:列表、字典dict()、集合set()

参考

python中的eval函数的使用详解

python中的eval函数的使用详解

Python深拷贝和浅拷贝解析

Python直接赋值、浅拷贝和深度拷贝解析