Python进阶细节
根据慕课网七月老师视频整理
一切皆对象
对与Python来说,一切皆对象,包括函数。在其他语言比如c++中,函数只是一段可执行的代码,只要你获得入口地址就可以调用这段代码。但是Python中不一样,Python中一切皆对象。Python中的函数,可以作为另一个函数的参数传入到另外一个函数里,也可以当作另外一个函数的返回值,甚至可以赋值给一个变量。
def a():
pass
print(type(a))
<class 'function'> # 可见是一个class,是一个类。
闭包
闭包指的是:函数+环境变量(环境变量不能是全局变量)。
python在函数内部还可以定义函数,但该函数作用域只在外部函数内部有效,除非作为外部函数的返回值被返回,在外部函数的外部用一个变量接收后就可以调用它了。
def curve_pre():
a = 25 # 这里定义了环境变量
def curve(x):
return a*x*x
return curve
a = 10 # 在外部定义了a为10
f = curve_pre()
print(f(2))
print(f.__closure__) # 可以通过这个内置变量来查看闭包里的内容
print(f.__closure__[0].cell_contents)
100
(<cell at 0x0000016832868708: int object at 0x0000000065DCECD0>,)
25
可见返回了一个对象。你再在外面定义了变量,也不会改变闭包内的环境变量。
闭包的意义在于返回了一个现场,如果没有闭包,很容易被外部的变量所影响。
1. 闭包的经典误区
闭包返回现场的环境变量,不能在闭包里定义的函数里面再被定义了,而且函数里必须要有调用环境变量的地方,否则就不叫做闭包了。
def f1():
a = 20
def f2():
a = 10 # 重复定义
return a
return f2
f = f1()
print(f.__closure__)
None # 可见此时返回了None,不再是闭包了。本质上是认为此时a被认为是一个局部变量,不再是环境变量了!
--------------------------------------------------------------------------
# 如果想要环境变量在函数里被改变,可以这样:
def f1():
a = 25
def f2():
nonlocal a # nonlocal关键字强制让a不再为局部变量,跳到上一级作为了环境变量。
a = a + 10
return a
return f2
f = f1()
print(f.__closure__)
print(f())
print(f())
print(f())
(<cell at 0x000002A5CF348708: int object at 0x0000000065DCECD0>,)
35
45
55
# 可以看到a的值是可以保存的,这是因为闭包的环境变量具有保存现场的功能,记忆住上次调用的状态,所以可以这样做。
---------------------------------------------------------------------------
def f1():
a = 20
def f2():
return 2 # 里面不再调用a了
return f2
f = f1()
print(f.__closure__)
None # 可见此时仍然不是闭包
---------------------------------------------------------------------------
def f1():
a = 20
def f2():
s = a+20
return 2
return f2
f = f1()
print(f.__closure__)
(<cell at 0x00000294E8568708: int object at 0x0000000065DCEC30>,)
# 可见就算返回值里不包括a,但是只要调用了,就可以是一个闭包。
2. 闭包的优点
从闭包可以看出函数式编程的优点。如果出现需要保存值进行迭代的情况,就不得不定义一个全局变量来保存上一次的值。但是在闭包里不需要使用到全局变量,只需要闭包里定义的环境变量即可记忆上一次的状态,这样就具有了封闭性,否则过多的使用全局变量会使代码变得混乱。这里再注意一个问题:
a = 10
def f1(x):
a_new = a + x
a = a_new
print(f1(5))
Traceback (most recent call last):
File "c4.py", line 7, in <module>
print(f1(5))
File "c4.py", line 4, in f1
a_new = a + x
UnboundLocalError: local variable 'a' referenced before assignment
# 看起来美滋滋其实报错了。再Python里,如果再定义了a的话,无论是在哪里,在定义的时候系统会默认a是局部变量不再是全局变量了。所以在执行代码的时候,就会出现了找不到局部变量a的情况,因为f1中第一段代码中用到了局部变量a,但是此时还没有定义啊。
----------------------------------------------------------------------
# 可以这么解决:
a = 10
def f1(x):
global a # 定义global关键字强制认为是全局变量
a_new = a + x
a = a_new
return a
print(f1(5))
print(f1(10))
15
25
# 可见这时候全局变量起到了保存的功能,但相对闭包,就显得很Low了。
3. 闭包的一个经典例子
def testFun():
temp = [lambda x : i*x for i in range(4)]
return temp
for everyLambda in testFun():
print (everyLambda(2))
# 运行后结果竟然是:
6
6
6
6
这里testfun()返回一个temp,temp是一个列表,everyLambda每次返回的都是temp里列表的值,参数2赋给x。
三元表达式
python中的三元表达式和其他语言中的不太一样。条件为真时返回的结果 if 判断条件 else 条件为假时返回的结果
x if x > y else y
其他很多语言中是这么定义的:x > y ? x:y
map类
map不是一个函数而是一个类。map和lambda表示结合起来一起用会非常好。map(func, *iterables) --> map object
iterables是可迭代的类型,序列和元组都可以。*号表示是可变参数,可以传入多个不同值的取值。
a = [1,2,3,4,5]
def square(x):
return x*x
r = map(square, a)
print(r)
print(list(r))
<map object at 0x000001DC6AD0B0F0>
[1, 4, 9, 16, 25]
匿名函数(lambda表达式)
lambda表达式也叫做匿名函数。
lambda的定义:lambda parameter_list: expression
expression意思是表达式,所以后面只能是表达式,不能是语句,比如赋值语句等是不可以的。
lambda表达式最后返回的是表达式计算出的值,和return后是一样的。
def add(x, y):
return x+y
lambda x, y : x + y
lambda表达式一般和三元表达式和map连接在一起用会更加整洁。
x = 1,2,3,4,5,6
y = 1,2,3,4,5,6
r = map(lambda x,y:x+y, x,y)
print(tuple(r))
(2, 4, 6, 8, 10, 12)
reduce函数
reduce 是一个函数。def reduce(function, sequence, initial=None)
- function : 这里需要注意函数必须且只能有两个参数
- sequence: 序列
- initial: 初始值
- reduce的含义是连续调用函数进行连续计算
from functools import reduce # 需要引入这个函数才可以使用
list_x = ['1','2','3']
r = reduce(lambda x,y:x+y, list_x, 'a')
print(r)
a123
# 执行情况 :(('a'+'1')+'2')+'3'
# 每次执行完一次函数的结果在下一次调用函数的时候会传入到函数的参数中进行计算。初始值是给出的开始的参数之一。
filter类
class filter(function or None, iterable)
表示过滤不符合条件的值。当函数返回为True时保留,False时剔除。当函数为None时,剔除调iterable中本来就为False的值
# ord()返回ascII码值
list_x = ['a', 'B', 'c', 'D']
r = filter(lambda x: False if ord(x)>64 and ord(x)<91 else True, list_x)
print(list(r))
['a', 'c']
装饰器
编写代码一个原则是:对修改是封闭的,对拓展是开放的。
如果想在很多个函数里,每个函数都实现相同的功能,用装饰器是最方便的,不用在每个函数里重复定义,而且调用起来很方便,和“装饰”的意思很像。
import time
def decorator(func):
def wrapper(*args, **kw): # *args是可变参数,**kw是可变关键字参数,这样可以接受除了有默认参数类型以外所有类型的函数参数
print(time.time())
func(*args, **kw)
return wrapper
# 就是一个闭包,传入的函数func是环境变量
@decorator # @ 是一个语法糖
def f1(func_name):
print("this is f1" + func_name)
@decorator
def f2(func_name1, func_name2):
print("this is f2" + func_name1 + func_name2)
@decorator
def f3(func_name1, func_name2='f3',*args, **kw):
print("this is f3"+func_name1+func_name2)
print(kw)
f1('f1') # 可见虽然在定义的时候麻烦了一些,但是调用的时候很方便。
f2('f2','f2')
f3('f3','f3',a='1',b='2',c='3') # 可变关键字参数
1519276076.973657 #时间戳
this is f1f1
1519276076.9746575
this is f2f2f2
1519276076.9746575
this is f3f3f3
{'a': '1', 'b': '2', 'c': '3'}
生成器
通过列表生成式可以直接创建一个列表,但是我们如果只想访问前面几个元素,不想利用后面的元素,我们可以定义一个生成器(generator),一边循环一边计算,有一种方法很简单,只要把列表生成式的[]
改为()
就可以创建一个generator.
L = [x * x for x in range(10)]
L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
g = (x * x for x in range(10))
g
<generator object <genexpr> at 0x104feab40>
如果需要打印出来元素,可以通过生成器的next()
方法。
g.next()
0
g.next()
1
g.next()
4
generator是可以迭代的:
g = (x * x for x in range(10))
for n in g:
... print n
...
0
1
4
9
可以把一个函数写成生成器,把return
改为yield
:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
fib(6)
<generator object fib at 0x104feaaa0>
for n in fib(6):
... print n
...
1
1
2
3
5
8
定义成生成器后,generator的执行流程和函数并不一样,函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。
另外一个例子:
def odd():
... print 'step 1'
... yield 1
... print 'step 2'
... yield 3
... print 'step 3'
... yield 5
...
o = odd()
o.next()
step 1
1
o.next()
step 2
3
o.next()
step 3
5
o.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
字典来代替swich
Python中没有swich这种结构,可以用字典来实现。
day = 0
def get_monday():
return 'Monday'
def get_Tuseday():
return 'Tuseday'
def get_Wednesday():
return 'Wednesday'
def default():
return 'Unknow'
#返回函数可以定义跟多操作,也可以直接返回值
swicher = {
0: get_monday,
1: get_Tuseday,
2: get_Wednesday
}
# 字典的get方法可以获取键对应的值,如果键不存在则返回指定的默认值
day_time = swicher.get(day, default())() # 后面再加一个括号来调用函数
print(day_time)
monday
列表推导式
对于列表推导式,其实不只是列表,也可以是元组,集合,字典进行推倒
a = [1,2,3,4,5]
b = [i**2 for i in a if i>=3]
print(b)
[9,16,25]
---------------------------------------------------
# 对于字典可以这样:
sdict = {
'q':'烈焰冲击',
'w':'天女散花',
'e':'致命一击'
}
dic = {value:key for key,value in sdict.items()} # 最外面加花括号就是字典或者集合
dic1 = [key for key,value in sdict.items()]
dic2 = (key for key,value in sdict.items())
print(dic)
print(dic1)
print(dic2)
# 如果是元组,元组是不可遍历的对象,所以需要下面这样取出对象
for i in dic2:
print(i,end='\\ ')
{'烈焰冲击': 'q', '天女散花': 'w', '致命一击': 'e'}
['q', 'w', 'e']
<generator object <genexpr> at 0x0000021A3430E150>
q\ w\ e\
None
None代表空,并不是False,也不是[],”
print(type(None))
print(type(False))
print(type([]))
print(type(''))
<class 'NoneType'>
<class 'bool'>
<class 'list'>
<class 'str'>
我们可以看见,这些在本质上都是不一样的,类型都不一样,只不过我们在进行逻辑判断的时候,有时会像下面这样做:
a = None/false/[]/''
if not a:
.....
# 判断的时候会这样做
不建议使用if a is None:
这种语句,我们可以看到类型是不一样的,有时会出错。
对象存在不一定是True
在上面对None的分析中,在逻辑判断的时候之所以可以判断None为False,是因为每个对象和bool类型之间都是有联系的,所以可以进行逻辑判断。但是我们自定义的对象却不一定了,返回True和False因不同的对象而不同。我们自定义的对象,如类,和我们的内置方法有关系。
类中有两个内置方法会影响对类的布尔类型的取值:
-
__len__
:这个内置方法返回的是类的长度,外部调用len()时会返回该方法的返回值,返回值只有布尔类型或者int类型。 -
__bool__
:这个内置方法返回的类的bool类型的取值,当这个方法在类里面定义以后,返回值只看其返回值,__len__的返回值不再起作用。注意该方法的返回值只能是布尔类型,即True或False。
class Rest():
def __len__(self):
return 5
def __bool__(self):
return False
print(len(Rest()))
print(bool(Rest()))
5
False
装饰器的副作用
加上装饰器后会改变函数的名字。
def decorator(func):
def wrapper():
print('this is decorator')
func()
return wrapper
@decorator
def f1():
print(f1.__name__) # 打印函数名字,不加装饰器是f1
f1()
this is decorator
wrapper
可见会出错,加上装饰器后,如果不想改变名字,可以这样做:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper():
print('this is decorator')
func()
return wrapper
@decorator
def f1():
print(f1.__name__) # 打印函数名字,不加装饰器是f1
f1()
this is decorator
f1
可哈希(hashable)对象和不可变性(immutable)
- 可哈希(hashable)和不可改变性(immutable)
如果一个对象在自己的生命周期中有一哈希值(hash value)是不可改变的,那么它就是可哈希的(hashable)的,因为这些数据结构内置了哈希值,每个可哈希的对象都内置了__hash__方法,所以可哈希的对象可以通过哈希值进行对比,也可以作为字典的键值和作为set函数的参数,但是list是unhashable,所以不可以作为字典的键值。所有python中所有不可改变的的对象(imutable objects)都是可哈希的,比如字符串,元组,也就是说可改变的容器如字典,列表不可哈希(unhashable)。我们用户所定义的类的实例对象默认是可哈希的(hashable),它们都是唯一的,而hash值也就是它们的id()。
- 哈希
它是一个将大体量数据转化为很小数据的过程,甚至可以仅仅是一个数字,以便我们可以用在固定的时间复杂度下查询它,所以,哈希对高效的算法和数据结构很重要。
- 不可改变性
它指一些对象在被创建之后不会因为某些方式改变,特别是针对任何可以改变哈希对象的哈希值的方式
- 联系
因为哈希键一定是不可改变的,所以它们对应的哈希值也不改变。如果允许它们改变,,那么它们在数据结构如哈希表中的存储位置也会改变,因此会与哈希的概念违背,效率会大打折扣
具体的我们可以参考官方文档和以下博客:
关于python内置__eq__函数的探索
关于可哈希对象的理解
python的深拷贝和浅拷贝
1.赋值方法:
list1 = [1,2,3]
list2 = list1
print(id(list1),id(list2))
2577180416904 2577180416904
可见这和在c语言中不一样,二者的id是一样的,换句话说,你改变 list2,同时也会改变 list1
2.浅拷贝:
import copy
list1 = [1,2,3]
list2 = copy.copy(list1)
print(id(list1),id(list2))
list2.append(4)
print(list1,list2)
2522465131400 2522465130824
[1, 2, 3] [1, 2, 3, 4]
但是浅拷贝,对于里面的元素,如果是不可变类型,会直接拷贝,但是对于可变类型只是指向它而已,例如看下面的代码:
import copy
list1 = [1,2,[1,2,3]]
list2 = copy.copy(list1)
print(id(list1),id(list2))
list2[2].append(4)
print(list1,list2)
2710366738760 2710368236680
[1, 2, [1, 2, 3, 4]] [1, 2, [1, 2, 3, 4]] # 都变化了
3.深拷贝
import copy
list1 = [1,2,[1,2,3]]
list2 = copy.deepcopy(list1)
print(id(list1),id(list2))
list2[2].append(4)
print(list1,list2)
1660389185864 1660390683784
[1, 2, [1, 2, 3]] [1, 2, [1, 2, 3, 4]]
可见深拷贝才是真正的拷贝。