在Python中类型属于对象,变量没有类型,仅仅是对一个对象的引用。而赋值语句改变的是变量所执的对对象的引用,故一个变量可指向各种数据类型的对象。
对于不可变对象,无论深、浅拷贝,内存地址都是一成不变,对于可变对象,需要分情况讨论:
直接赋值:仅拷贝了对可变对象的引用,故前后变量均未隔离,任一变量 / 对象改变,则所有引用了同一可变对象的变量都作相同改变。
浅拷贝:使用 copy(x) 函数,拷贝可变对象最外层对象并实现隔离,但内部的嵌套对象仍是未被隔离的引用关系。下面这段代码说明这个问题:
>>> import copy
>>> x = [555, 666, [555, 666]]
>>> z = copy.copy(x) # 浅拷贝
>>> zz = x[:] # 也是浅拷贝, 等同于使用 copy() 函数的 z
>>> z
[555, 666, [555, 666]]
>>> zz
[555, 666, [555, 666]]
# 改变变量 x 的外围元素, 不会改变浅拷贝变量
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777] # 只有自身改变, 增加了外围元素 777
>>> z
[555, 666, [555, 666]] # 未改变
>>> zz
[555, 666, [555, 666]] # 未改变
# 改变变量 x 的内层元素, 则会改变浅拷贝变量
>>> x[2].append(888)
>>> x
[555, 666, [555, 666, 888], 777] # 同时发生改变, 增加了内层元素 888
>>> z
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888
>>> zz
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888
# 浅拷贝变量的外围元素改变不会相互影响
>>> z.pop(0)
555
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> z
[666, [555, 666, 888]] # 只有自身改变, 弹出了外围元素 555
>>> zz
[555, 666, [555, 666, 888]] # 未改变
# 浅拷贝变量的内层元素改变会相互影响
>>> z[1].pop()
888
>>> x
[555, 666, [555, 666], 777] # 同时发生改变, 弹出了内层元素 888
>>> z
[666, [555, 666]] # 同时发生改变, 弹出了内层元素 888
>>> zz
[555, 666, [555, 666]] # 同时发生改变, 弹出了内层元素 88
深拷贝:使用 deepcopy(x) 函数,拷贝可变对象的“外围+内层”而非引用,实现对前后变量不论深浅层的完全隔离。此外需要注意的是深拷贝递归对象 (直接或间接包含对自身引用的复合对象) 可能会导致递归循环。
Python参数传递采用的是“传对象引用”的方式,结合上面我们对可变对象和不可变对象、深拷贝以及浅拷贝的解析,我们应该可以得到如下结论:
3. 关于释放内存方面,当一个对象的引用计数变为0时,Python就会调用它的析构函数。调用析构函数并不意味着最终一定会调用free来释放内存空间,频繁地申请、释放内存空间会使Python的执行效率大打折扣。在析构时也采用了内存池机制,从内存池申请到的内存会被归还到内存池中,以避免频繁地申请和释放动作
分代回收建立标记清除的基础之上,是一种以空间换时间的操作方式,即控制内存回收频次。
import gc
gc.set_threshold(600, 10, 5)
print(gc.get_threshold())
Python内存泄漏通常有如下几种情况:
在学习闭包概念之前,有必要先了解下Python的作用域相关概念
flist = []
for i in xrange(3):
def func(x):
return x*i
flist.append(func)
for f in flist:
print(f(2))
上述打印结果为4,4,4,而不是0,2,4,原因是在往flist中添加func的时候并没有形成作用域,也就没有保存i的值,而是在执行f(2)的时候去取,此时循环已经结束,i的值固定为2,输出结果为4,4,4,如果需要输出0,2,4的话,可以进行如下修改形成闭包即可:flist = []
for i in xrange(3):
def makefunc(i)
def func(x):
return x*i
return func
flist.append(makefunc(i))
for f in flist:
print(f(2))
下面我们来看闭包的定义和使用方法。闭包并不是Python的独有概念,闭包在维基百科上的定义如下:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
闭包的两个作用主要是:
局部变量无法共享和长久的保存,而全局变量可能造成变量污染,其中上述第二种作用可以使得我们既可以长久保存变量又不会造成全局污染,但是使用闭包有两点需要注意几点:
闭包无法改变外部函数局部变量指向的内存地址,如下所示:
def outfun():
x = 0
def infun():
x = 1
print(x) # 打印1
print(x) # 打印0
infun()
print(x) # 打印0
outfun()
上述函数打印结果为010,很多博客将这一特性描述为闭包无法改变外部函数的局部变量,但这一描述是不准确的,如果x为可变对象,上述代码就会表现为闭包改变了外部函数的局部变量的值。
返回闭包时,返回函数不要引用任何循环变量,或者后续会发生变化的变量,这条规则可以参考上述3.1节第3点展示的例子,具体原因是因为循环无法形成作用域,对应的循环变量无法保存,因此会造成与预期不符的结果。
可以通过closure属性判断一个函数是否是闭包。
装饰器的定义和作用:装饰器本质上是一个Python函数,装饰器的返回值也是一个函数对象。它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,基于此可以抽离出大量与函数功能本身无关的雷同代码并继续重用,例如插入日志、性能测试、事务处理、缓存、权限校验等。
装饰器的实现:简单装饰器实现如下:
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper
def foo():
print('i am foo')
foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo()
基于@语法糖的实现如下:
def use_logging(func):
def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo")
foo()
带参数的装饰器实现如下:
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper
return decorator
@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)
foo()
还有一个与装饰器相关的库函数functools.wraps,其作用主要是将原函数f(x)的元信息拷贝到装饰器的func函数中,使得装饰器中的func函数和原函数f(x)一样的元信息:
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'f'
print func.__doc__ # 输出 'does some math'
return func(*args, **kwargs)
return with_logging
@logged
def f(x):
"""does some math"""
return x + x * x
当我们理解闭包后,会发现装饰器的实现并不难理解。
迭代器的作用:迭代器可以像列别一样迭代获取其中每一个元素,但是它不像列表将所有元素一次性加载到内存,而是以一种延迟计算方式返回元素。当我们获取的元素数据量特别大时,列表会占用几百兆的内存,而迭代器只需要几十个字节的空间,这就是迭代器的作用。
将一个类作为迭代器需要实现两个方法 __ iter__() 与 __ next__():
__ iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __ next__() 方法并通过 StopIteration 异常标识迭代的完成。可迭代对象需要提供 iter()方法,否则不能被 for 语句处理。
__ next__() 方法会返回下一个迭代器对象。
如下是一个斐波拉契数列的迭代器:
class Fibonacci(object):
def __init__(self, all_num):
self.all_num = all_num
self.current_num = 0
a = 0
b = 1
def __iter__(self):
return self
def __next__(self):
if self.current_num < self.all_num:
result = self.a
self.a, self.b = self.b, self.a + self.b
self.current_num += 1
return result
else:
raise StopIteration
fibo = Fibonacci(10)
for i in fibo:
print(i)
生成器通常两种方式:生成器表达式(generator expression)和生成器函数(generator function)。
生成器表达式:在生成列表和字典时,可以通过推导表达式完成。只要把推导表达式中的中括号换成小括号就成了生成器表达式,如下
# 列表:
a = [x * x for x in range(3)]
print(a) # 0,1,4
# 生成器表达式:
b = (x * x for x in range(3))
print(next(b)) # 0
print(next(b)) # 1
print(next(b)) # 4
print(next(b)) # 触发 StopIteration 异常
# 通常我们使用时不会调用next()方法,而是使用for循环
c = (x * x for x in range(3))
for i in c:
print(i)
生成器函数:如果一个函数定义中包含 yield 表达式,那么这个函数就不再是一个普通函数,而是一个生成器函数。yield 语句类似 return 会返回一个值,但它会记住这个返回的位置,下次 next() 迭代就从这个位置下一行继续执行。通过生成器表达式来进行表达式推到是有局限的,复杂的处理需要生成器函数完成。如下斐波拉契数列的生成器:
def Fibonacci(n):
a, b = 0, 1
while(i < n):
yield a
a, b = b, a + b
fibo = Fibonacci(5)
for i in fibo :
print(i, end=' ') # 0 1 1 2 3
print(type(fibo)) # 输出
print(type(Fibonacci(5))) # 输出
从这里我们可以看出:
生成器的本质:生成器表达式和生成器函数产生生成器时,会自动生成名为 __ iter__ 和 __ next__ 的方法。也就是说生成器是一种迭代器。对于迭代器和生成器的区别,可以从下图进行理解:
正则表达式并不是Python的一部分。正则表达式是用于处理字符串的强大工具,拥有自己独特的语法以及一个独立的处理引擎,效率上可能不如str自带的方法,但功能十分强大。得益于这一点,在提供了正则表达式的语言里,正则表达式的语法都是一样的,区别只在于不同的编程语言实现支持的语法数量不同。这里我们对其实现原理我们不做深究,主要以思维导图的方式整理了下正则表达式的各种语法和函数。
Python通过re模块提供对正则表达式的支持,如下列出来re模块函数:
re模块中还提供了9个模块常量用于标记不同的功能细节,如下:
>>> dir("hello")
['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
'__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mo
d__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center',
'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'isl
ower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', '
rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate'
, 'upper', 'zfill']
其中有一些我们比较熟悉的例如:__ getattribute__定义当该类的属性被访问时的行为,__ getitem__定义获取容器中指定元素的行为等,具体用到的时候我们在去了解其作用。在所有魔法方法中,__ init__和__ new__可能是最重要的一对魔法方法了,其区别如下:
class Mycls:
_instance = None
def __new__(cls):
# 判断该类的属性是否为空;对第一个对象没有被创建,我们应该调用父类的方法,为第一个对象分配空间
if cls._instance == None:
# 把类属性中保存的对象引用返回给python的解释器
cls._instance = object.__new__(cls)
return cls._instance
# 如果cls._instance不为None,直接返回已经实例化了的实例对象
else:
return cls._instance
def __init__(self):
print('init')
my1=Mycls()
print(my1)
my2=Mycls()
print(my2)
>>>
init
<__main__.Mycls object at 0x000000406E471148>
Init
<__main__.Mycls object at 0x000000406E471148>
多继承中最经典的问题就是菱形继承会带来的子类重复调用问题,如下代码
class A:
def fun(self):
print('A.fun')
class B(A):
def fun(self):
A.fun(self)
print('B.fun')
class C(A):
def fun(self):
A.fun(self)
print('C.fun')
class D(B , C):
def fun(self):
B.fun(self)
C.fun(self)
print('D.fun')
D().fun()
>>>
A.fun
B.fun
A.fun
C.fun
D.fun
可以看到,A类被初始化了两次,会造成资源浪费。
我们可以通过super()函数解决上述问题,如下代码:
class A:
def fun(self):
print('A.fun')
class B(A):
def fun(self):
super(B , self).fun()
print('B.fun')
class C(A):
def fun(self):
super(C , self).fun()
print('C.fun')
class D(B , C):
def fun(self):
super(D , self).fun()
print('D.fun')
D().fun()
>>>
A.fun
C.fun
B.fun
D.fun
从上述结果看到,使用super()函数后A类仅初始化了一次,那么为什么输出A->C->B->D以及为什么super()可以避免菱形继承问题呢?下面进一步解释
class A(object):
def fun(self):
print('A.fun')
class B(object):
def fun(self):
print('B.fun')
class C(object):
def fun(self):
print('C.fun')
class D(A,B):
def fun(self):
print('D.fun')
class E(B, C):
def fun(self):
print('E.fun')
class F(D, E):
def fun(self):
print('F.fun')
# 保持obj实例不变,尝试不同的type
super(E , F()).fun() # 输出结果:B.fun
super(D , F()).fun() # 输出结果:A.fun
super(F , F()).fun() # 输出结果:D.fun
# 保持type不变,obj尝试不同的实例
super(B , F()).fun() # 输出结果:C.fun
super(B , E()).fun() # 输出结果:C.fun
super(B , B()).fun() # 这是错误的,会报错
上述代码__mro__的顺序:F->D->A->E->B->C->object,我们可以发现调用的都是type对应的类在__mro__顺序中的下一个类的fun方法。所以,我们可以通过type参数来指定调用父类的范围,通过obj参数指定的是用那个类的__mro__属性。我们经常会在代码中看到*args、**kwargs,他们都被称为可变参数(任意参数):
作为函数定义时:
(1)*参数收集所有未匹配的位置参数组成一个tuple对象,局部变量args指向此tuple对象;
(2)**参数收集所有未匹配的关键字参数组成一个dict对象,局部变量kwargs指向此dict对象;
例如
def temp(*args,**kwargs):
pass
作为函数调用时:
(1)*参数用于解包tuple对象的每个元素,作为一个一个的位置参数传入到函数中;
(2)**参数用于解包dict对象的每个元素,作为一个一个的关键字参数传入到函数中;
例如:
my_tuple = ("wang","yuan","wai")
temp(*my_tuple)
#---等同于---#
temp("wangyuan","yuan","wai")
my_dict = {"name":"wangyuanwai","age":32}
temp(**my_dict)
#----等同于----#
temp(name="wangyuanwai",age=32)
Python提供了非常完善的基础代码库,覆盖了网络、文件、GUI、数据库、文本等大量内容,被形象地称作Batteries Included。用Python开发,许多功能不必从零编写,直接使用现成的即可,因此作为开发者熟练掌握这些内建模块对于开发效率肯定是有帮助的。这里总结几种比较常用的模块:
collections是集成专用容器的模块,作为对通用容器 dict、list、set 和 tuple 的补充。下面仅对常用方法进行整理。
functools是集成特殊装饰器的模块,基于这些装饰器通常可以用来节省内存或者简化函数。下面仅对常用方法进行整理:
itertools是集成操作迭代器方法的模块,通过这些方法可以使得迭代更加高效,下面仅对常用方法进行整理:
在 Python 中,协程(Coroutine)是一种轻量级的并发编程方式,可以通过协作式多任务来实现高效的并发执行,协程相比于线程的优势如下:
在 Python 3.4 之前,协程通常使用 yield 关键字来实现,称为“生成器协程”。在 Python 3.4 引入了 asyncio 模块后,可以使用 async/await 关键字来定义协程函数,称为“原生协程”。这里我们仅介绍下原生协程的用法:
下面给出一个简单的原生协程示例,其中包含一个 async 关键字修饰的协程函数 coroutine 和一个简单的异步 I/O 操作:
import asyncio
async def func():
print('Coroutine started')
await asyncio.sleep(1)
print('Coroutine finished')
async def main():
print('Main started')
await func()
print('Main finished')
asyncio.run(main())
>>>
Main started
Coroutine started
Coroutine finished
Main finished
在上面的代码中,使用 async 关键字定义了一个原生协程函数 ,并在其中使用 await 关键字来暂停函数的执行,等待异步 I/O 操作的完成。通过这种方式,可以在原生协程中编写异步并发代码,从而提高代码的性能和效率。(为什么要用asyncio.sleep, 而不用time.sleep呢? 因为await后面一个要跟一个异步函数的实例化对象,可是time.sleep并不是异步函数,也就不支持协程间切换,就没法实现并发,只能串行。)
以上是在两个异步函数中实现了切换,而如果一个普通的线程要能同时处理多个异步函数, 就要创建一个事件循环:
import asyncio
def main():
loop = asyncio.new_event_loop()
下面通过一个简单的例子来说明事件循环的用法,平常我们上课时通常是一边听一边记笔记,
import asyncio
import time
async def listening():
"""听课"""
print('start listening')
await asyncio.sleep(1)
print("listening...")
await asyncio.sleep(1)
print("listening...")
await asyncio.sleep(1)
print('end listening')
return "finish listening"
async def taking_notes():
"""记笔记"""
print("start taking notes")
await asyncio.sleep(1)
print("taking notes...")
await asyncio.sleep(1)
print("taking notes...")
await asyncio.sleep(1)
print("end taking notes")
return "finish taking notes"
async def main():
print("start main")
future1 = listening()
future2 = taking_notes()
ret1 = await future1
ret2 = await future2
print(ret1, ret2)
print("end main")
if __name__ == '__main__':
t1 = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
t2 = time.time()
print('cost:', t2-t1)
>>>
start main
start listening
listening...
listening...
end listening
start taking notes
taking notes...
taking notes...
end taking notes
finish listening finish taking notes
end main
cost: 6.007081508636475
上述执行并不符合预期,原因是用await确实会切换协程, 但你事先没有告诉事件循环有哪些协程, 它不知道切换到哪个协程, 所以事件循环就会按顺序坚持执行完,但是当我们使用用asyncio.gather()补充改信息后结果就符合预期了:
async def main():
print("start main")
future1 = listening()
future2 = taking_notes()
ret1, ret2 = await asyncio.gather(future1, future2)
print(ret1, ret2)
print("end main")
>>>
start main
start listening
start taking notes
listening...
taking notes...
listening...
taking notes...
end listening
end taking notes
finish listening finish taking notes
end main
cost: 3.003592014312744
通过上述例子我们应该了解了协程的大致使用方法,协程的更细节的用法后续有机会实际使用后再进行补充