第3章_Python进阶(一)
1.lambda
函数
该函数有一个有趣的例子,代码如下:
def multipliers():
return [lambda x:i*x for i in range(4)]
print([m(2) for m in multipliers()])
# [6, 6, 6, 6]
一般的解释来说,这是闭包延迟绑定的效果。我们可以更改如下:
lst = [lambda x, i=i: x*i for i in range(4)]
res = [m(2) for m in lst]
相关的参考文献有:
关于 [lambda x: xi for i in range(4)] 理解
说说我对[lambda x: xi for i in range(4)]的理解
fun = [lambda x: x*i for i in range(4)] 本质解析/原理,LEGB规则 闭包原理
python 函数及变量作用域及装饰器decorator @详解
2.单下划线
-
在交互解释器当中,
_
代表上一条命令的执行结果。在代码当中,它代表被丢弃的临时变量。 -
在属性和方法之前加上
_
,代表该属性、方法和类只能在内部使用,是API
的非公开部分。如果用from <module> import *
和from <package> import *
时,这些属性、方法、类将不被导入。但是,如果在代码当中定义了__all__
这个字符串列表,则列表当中所有的变量不受_
的影响。__all__
也可以定义在包的__init__.py
文件当中。
3.双下划线
-
父类在属性和方法之前加上
__
,代表该属性或方法不会被子类轻易覆盖,即父类的私有属性和方法。 -
如果名称前后都有双下划线,则说明这是
Python
内部定义的方法,可以进行重写,例如:__init__
4.函数参数传递方式
-
对于不可变类型(数值型、字符串、元组),函数参数传递的是值传递;对于可变数据类型(列表、字典)来说。,函数的参数传递是引用传递。
-
参数列表当中定义的缺省参数会随着被调用而改变1,例如:
def extendList(val, list=[]): list.append(val) return list list1 = extendList(10) list2 = extendList(123, []) list3 = extendList('a') # list1 = [10, 'a'] # list2 = [123] # list3 = [10, 'a']
首先,我们解释
list3
的情况:list
是可变类型,所以其属于引用传递。也就是,这个缺省的参数会随着被调用而改变。在list1
执行的时候,缺省的参数list
就已经添加了10
,所以在执行list3
的时候,就是在10
后面加入a
。然后我们来看一看list1
的情况:我们知道List
是可变类型,所以此处给list1
的赋值应该属于引用赋值2。那么,当参数list
发生改变时,其引用list1
也应该发生了改变。
5.闭包3
一般来说,闭包的定义如下:
-
在一个外函数中定义了一个内函数。
-
内函数里运用了外函数的临时变量。
-
并且外函数的返回值是内函数的引用。
闭包也有一些陷阱,例如上文lambda
函数的例子
闭包的应用有装饰器和单例模式4等
6.*args
和**kwarg
*arg
和**kwarg
是函数形式参数的两个格式,分别代表以下含义5:
*args:可以理解为长度不固定的列表。
**kwarg:可以理解为长度不固定的字典
特别地:6
如果我们想把元组作为星号参数的参数值,在元组值前加上" * " 即可。
7.__getattr__
、__setattr__
、__delattr__
__getattr__
函数的作用: 如果属性查找(attribute lookup
)在实例以及对应的类中(通过__dict__)
失败, 那么会调用到类的__getattr__
函数, 如果没有定义这个函数,那么抛出AttributeError
异常。由此可见,__getattr__
一定是作用于属性查找的最后一步,兜底。7、8
在对一个属性设置值的时候,会调用到这个函数,每个设置值的方式都会进入
__setattr__
这个方法。9
__delattr__
是个删除属性的方法。__delattr__
也要避免死循环的问题,就如__setattr__
一样,在重写__delattr__
,避免重复调用。10
8.@property
使方法可以像属性一样访问,防止属性被修改11
9.r
、b
、u
、f
12
-
r
:去掉反斜杠的转义机制,常用于正则表达式,对应着re模块。 -
b
:表明后面是bytes 类型。网络编程中,服务器和浏览器只认bytes 类型数据。 -
u
:后面字符串以 Unicode 格式进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码。 -
f
:表示在字符串内支持大括号内的python
表达式,类似str.format()
的格式化函数,详细用法见13。
10.大文件读取
如果要处理一个大小为10G的文件,但是内存只有4G,需要怎么做?既然无法一次性读入10G文件,那就需要分批读入。分批读入数据,要记录好每次读入数据的位置。同时要控制好每次读取数据的大小,太小会在读取操作花费过多时间。14、15
# 方法1-------------------------------------------
def get_lines():
with open('file.txt','rb') as f:
for i in f:
yield i
# 使用生成器,每次只返回一部分,下一次再从上一次打断的地方开始。
# 方法2-------------------------------------------
from mmap import mmap
def get_lines(fp):
with open(fp,"r+") as f:
m = mmap(f.fileno(), 0)
tmp = 0
for i, char in enumerate(m):
if char==b"\n":
yield m[tmp:i+1].decode()
tmp = i+1
if __name__=="__main__":
for i in get_lines("fp_some_huge_file"):
print(i)
# 使用mmap,将文件映射到内存。再使用生成器,每次返回一行
# 方法3-------------------------------------------
def pandas_read(filename,sep=',',chunksize=5):
reader = pd.read_csv(filename,sep,chunksize=chunksize)
while True:
try:
yield reader.get_chunk()
except StopIteration:
print('---Done---')
break
if __name__ == '__main__':
g = pandas_read('./data/movies.dat',sep="::")
for c in g:
print(c)
# process c
# 使用pandas,每次读取5行
# 方法一有缺点,逐行读入,频繁的 IO 操作拖累处理效率。是否有一次 IO ,读取多行的方法?
# pandas 包 read_csv 函数,参数有 38 个之多,功能非常强大。
# 关于单机处理大文件,read_csv 的 chunksize 参数能做到,设置为 5, 意味着一次读取 5 行。
上文当中还有一些疑问没有得到解决,我们先说一下生成器的问题:
for i in get_lines("fp_some_huge_file"):
函数get_lines
当中使用了yield
,属于一个生成器。这个生成器在这里是以类似列表的方式被使用的,下面举一个其他例子16:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b # 使用 yield
# print b
a, b = b, a + b
n = n + 1
for n in fab(5):
print n
然后来说一下mmap
的问题,其解释如下17:
为了随机访问文件的内容,使用mmap将文件映射到内存中是一个高效和优雅的方法。例如,无需打开一个文件并执行大量的
seek()
,read()
,write()
调用,只需要简单的映射文件并使用切片操作访问数据即可。内存映射一个文件并不会导致这个文件被读取到内存中。也就是说,文件并没有被复制到内存缓存或数组中。相反,操作系统仅仅为文件内容保留了一段虚拟内存。当访问文件的不同区域时,这些区域的内容才根据需要被读取并映射到内存区域中。而那些从没被访问到的部分还是留在磁盘上。所有这些过程都是透明的,在幕后完成。
其使用方法等举例如下18:
>>> with open("myfile.txt", "wb") as f:
... f.write(b"Hello Python!\n")
>>> import mmap
>>> with open("myfile.txt", "r+b") as f:
... mapf = mmap.mmap(f.fileno(), 0)
... print(mapf.readline()) # prints b"Hello Python!\n"
... print(mapf[:5]) # prints b"Hello"
... mapf.tell()
... mapf[6:] = b" world!\n"
... mapf.seek(0)
... print(mapf.readline()) # prints b"Hello world!\n"
... mapf.close()
...
b'Hello Python!\n'
b'Hello'
14
b'Hello world!\n'
还有一些其他参考文献:
Python之mmap内存映射模块(大文本处理)说明
认真分析mmap:是什么 为什么 怎么用
11.装饰器
装饰器本质上是一个函数,该函数用来处理其他函数,它可以让其他函数在不需要修改代码的 前提下增加额外的功能,装饰器的返回值也是一个函数对象。19
其主要有以下功能19
- 引入日志
- 函数执行时间统计
- 执行函数前预备处理
- 执行函数后清理功能
- 权限校验等场景
- 缓存
其中,functools
模块下的wraps
装饰器还可以解决自定义装饰器的一些问题。20用法如:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""doc of wrapper"""
print('123')
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""doc of say hello"""
print('同学你好')
print(say_hello.__name__)
print(say_hello.__doc__)
# say_hello
# doc of say hello
如果不加@wraps(func)
,则:
def decorator(func):
def wrapper(*args, **kwargs):
"""doc of wrapper"""
print('123')
return func(*args, **kwargs)
return wrapper
@decorator
def say_hello():
"""doc of say hello"""
print('同学你好')
print(say_hello.__name__)
print(say_hello.__doc__)
# wrapper
# doc of wrapper
还有一些内置装饰器,如:
-
property
装饰器:用于类中的方法,使得我们可以像访问属性一样来获取一个方法的返回值。其内部具体运行机制详见21class XiaoMing: first_name = '明' last_name = '小' @property def full_name(self): return self.last_name + self.first_name xiaoming = XiaoMing() print(xiaoming.full_name) # 小明
-
setter装饰器
:用于实现对私有属性的修改。一般来说类的私有属性不能够在类的外部被访问。而经过property
装饰器,私有属性可以在类外被访问。同样,经过setter装饰器
,私有属性可以在类外被修改。22class Person: def __init__(self,name): self._name = name # 利用property装饰器将获取name方法转换为获取对象的属性 @property def get_name(self): return self._name # 利用setter装饰器将设置name方法转换为设置对象的属性 @get_name.setter def set_name(self,name): self._name = name p = Person('小黑') print(p.get_name) # 原 p.get_name() , 现 p.get_name p.set_name = '小灰' # 原 p.set_name('小灰') ,现 p.set_name = '小灰' print(p.get_name)
-
staticmethod 装饰器
:用于类中的方法,这表示这个方法将会是一个静态方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有self
参数,也无法访问实例化后的对象。应用场景等详见23class XiaoMing: @staticmethod def say_hello(): print('同学你好') XiaoMing.say_hello() # 实例化调用也是同样的效果 # 有点多此一举 xiaoming = XiaoMing() xiaoming.say_hello() # 同学你好 # 同学你好
-
classmethod 装饰器
:用于类中的方法,这表示这个方法将会是一个类方法,意味着该方法可以直接被调用无需实例化,但同样意味着它没有self
参数,也无法访问实例化后的对象。相对于staticmethod
的区别在于它会接收一个指向类本身的cls
参数,即可以访问类中的某些变量。应用场景等详见23class XiaoMing: name = '小明' @classmethod def say_hello(cls): print('同学你好, 我是' + cls.name) print(cls) XiaoMing.say_hello() # 同学你好, 我是小明 # <class '__main__.XiaoMing'>
同时装饰器也可以实现一些缓存功能,例如性能优化当中的函数返回值缓存:
import time
class Cache:
__cache = {}
def __init__(self, func):
self.func = func
def __call__(self):
# 如果缓存字典中有这个方法的执行结果
# 直接返回缓存的值
if self.func.__name__ in Cache.__cache:
return Cache.__cache[self.func.__name__]
# 计算方法的执行结果
value = self.func()
# 将其添加到缓存
Cache.__cache[self.func.__name__] = value
# 返回计算结果
return value
@Cache
def long_time_func():
time.sleep(5)
return '我是计算结果'
start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')
start = time.time()
print(long_time_func())
end = time.time()
print(f'计算耗时{end-start}秒')
# 我是计算结果
# 计算耗时5.001157283782959秒
# 我是计算结果
# 计算耗时0.0秒
关于__call__
:24
call():可以让类的实例具有类似于函数的行为,
进一步模糊了函数和对象之间的概念。
使用方式:对象后面加括号,触发执行。即:对象() 或者 类()()
class Person(object):
def __call__(self, *args, **kwargs):
print('call...')
person = Person() # 将Person()的内存地址赋值给person
person() # 内存地址()
#或者是Person()()
# call...
如上代码所示,调用
__call__
时,并不需要person.__call__
。因为在person=Person()
时,就已经将Person()
的内存地址赋值给了person
,所以后面调用时person
本身就代表了那个内存地址,当成函数使用person()
12.生成器
创建方式:
-
把一个列表生成式的
[]
改成()
-
函数中包含
yield
关键字
yield
的作用:
-
yield
可以保存当前运行状态(断点),然后暂停执行,即将函数挂起。将yield
关键字后面表达式的值作为返回值返回,此时可以理解为起到了return
的作用,如果使用next()
、send()
让函数从断点处继续执行,就会唤醒函数。 -
在用
for
循环的时候,每次取一个元素就会计算一次。使用yield
的函数叫做generator
,与iterator
一样,它的好处是不用一次性计算所有元素,而是用一次计算一次,这样可以节省很多空间。generator
每次计算都需要上一次的计算结果,所以要使用yield
,否则在使用return
之后,上次计算结果就丢失了。
14.迭代器
创建迭代器:
-
使用
iter()
25>>>lst = [1, 2, 3] >>> for i in iter(lst): ... print(i) ... 1 2 3
-
自定义迭代器:26
class MyRange(object): def __init__(self, end): self.start = 0 self.end = end def __iter__(self): return self def __next__(self): if self.start < self.end: ret = self.start self.start += 1 return ret else: raise StopIteration from collections.abc import * a = MyRange(5) print(isinstance(a, Iterable)) print(isinstance(a, Iterator)) for i in a: print(i) # True # True # 0 # 1 # 2 # 3 # 4
说明:27
- 能被for循环的对象,里面一定有__iter__和 __next__两种方法,该对象被for循环时,会优先执行__iter__里面的内容,再循环执行__next__里面的内容
- 能被next调用的对象,里面一定有__next__方法,__iter__方法是否具有不能确定
补充:
StopIteration
异常用于标识迭代的完成28li=[1,2,3,4] it=iter(li) print(next(it)) print(next(it)) print(next(it)) print(next(it)) print(next(it)) # next()完成后引发StopIteration异常 --------------------------------------------------------- for l in it: # for循环自带异常处理 print(l) --------------------------------------------------------- import sys # while循环需要带异常处理 while True: try: print(next(it)) except StopIteration: sys.exit()
补充:
__getitem__
也是模拟的返回一个迭代器29class Person: def __init__(self,persion_list): self.persion_list=persion_list def __getitem__(self, item): return self.persion_list[item] body=Person(["Xiuwu","Adny","Maggie"]) for i in body: print (i)
区别:30
生成器能做到迭代器能做的所有事,而且因为自动创建了
iter()
和next()
方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出StopIteration
异常。
15.isinstance()
与type()
区别如下:31
type()
不会认为子类是一种父类类型,不考虑继承关系。isinstance()
会认为子类是一种父类类型,考虑继承关系。
16.赋值、浅拷贝、深拷贝
解释如下:32
- 直接赋值:其实就是对象的引用(别名)。
- 浅拷贝
(copy)
:拷贝父对象,不会拷贝对象的内部的子对象。- 深拷贝
(deepcopy)
:copy
模块的deepcopy
方法,完全拷贝了父对象及其子对象。
举例如下:
#!/usr/bin/python
# -*-coding:utf-8 -*-
import copy
a = [1, 2, 3, 4, ['a', 'b']]
#原始对象
b = a
#赋值,传对象的引用
c = copy.copy(a)
#对象拷贝,浅拷贝
d = copy.deepcopy(a)
#对象拷贝,深拷贝
a.append(5)
#修改对象a
a[4].append('c')
#修改对象a中的['a', 'b']数组对象
print( 'a = ', a )
print( 'b = ', b )
print( 'c = ', c )
print( 'd = ', d )
# ('a = ', [1, 2, 3, 4, ['a', 'b', 'c'], 5])
# ('b = ', [1, 2, 3, 4, ['a', 'b', 'c'], 5])
# ('c = ', [1, 2, 3, 4, ['a', 'b', 'c']])
# ('d = ', [1, 2, 3, 4, ['a', 'b']])
17.内存管理33、34
-
引用计数机制
Python
内部使用引用计数,来保持追踪内存中的对象,所有对象都有引用计数。创建对象、作为参数传递给函数等会造成引用计数的增加,显式del
对象、本地引用离开其作用范围(引用该对象的函数结束)等会造成引用计数的减少。sys.getrefcount()
函数可以获得对象的当前引用计数。引用计数器的底层机制参见博文35
-
垃圾回收机制
当一个对象的引用计数归零时,它将被垃圾收集机制处理掉。
相互引用:
当两个对象a和b相互引用时,del语句可以减少a和b的引用计数,并销毁用于引用底层对象的名称。然而由于每个对象都包含一个对其他对象的应用,因此引用计数不会归零,对象也不会销毁。(从而导致内存泄露)。为解决这一问题,解释器会定期执行一个循环检测器,搜索不可访问对象的循环并删除它们。
循环引用:
如果显式重写了
__del__
方法,就有可能无法自动回收循环引用的对象,造成内存泄漏,具体解释见36。当然这是python2.x
的环境,我在python3.x
的环境当中测试了一下,似乎没有影响,可以回收内存,即文中注释掉重写__del__
方法的那种情况。 -
内存池机制
Python
中所有小于256
个字节的对象都使用pymalloc
实现的分配器,即管理对小块内存申请和释放的内存池机制,而大的对象则使用系统的malloc
。补充:
对于Python对象,如整数,浮点数和List,都有其独立的私有内存池,对象间不共享他们的内存池。也就是说如果你分配又释放了大量的整数,用于缓存这些整数的内存就不能再分配给浮点数。
这里的意思应该是指整数释放的内存归入整数的私有内存池,用于下次新建整数对象时分配内存。这个情况貌似叫
free_list
机制37
其他:
-
当退出
python
时,是否释放全部内存?38并没有释放全部内存。循环引用其他对象或引用自全局命名空间的对象的模块,在退出时并非完全释放。另外,也不会释放
C
库保留的内存部分 -
引用计数的优缺点39
优点:
- 高效
- 运行期没有停顿:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。
- 对象有确定的生命周期
- 易于实现
缺点:
- 维护引用计数消耗资源,维护引用计数的次数和引用赋值成正比,而不像
mark and sweep
等基本与回收的内存数量有关。 - 无法解决循环引用的问题。
A
和B
相互引用而再没有外部引用A
与B
中的任何一个,它们的引用计数都为1,但显然应该被回收。
# 循环引用示例 list1 = [] list2 = [] list1.append(list2) list2.append(list1)
为了解决这两个缺点
Python
还引入了另外的机制:标记清除和分代回收 -
标记清除(Mark—Sweep)40
- 标记删除第一步:对执行删除(-1)后的每个引用-1,那么a的引用就是0,b的引用为1,将a放到死亡容器,将b放到存活容器。
- 标记删除第二步:循环存活容器,发现b引用a,复活a:将a放到存活容器内。
- 标记删除第三步:删除死亡容器内的所有对象。
-
分代收集40
1、新创建的对象做为0代
2、每执行一个【标记-删除】,存活的对象代数就+1
3、代数越高的对象(存活越持久的对象),进行【标记-删除】的时间间隔就越长。这个间隔,江湖人称阀值。其相关的底层源码参见43
其他参考文献:
python学习分享之垃圾回收-引用计数器35
python学习分享之垃圾回收-缓存机制37
18.zip()
函数
该函数以一个或多个序列(可迭代对象)作为参数,返回一个元组的列表。
a = [1,2,3]
b = [4,5,6]
c = [7,8,9]
print(list(zip(a,b,c)))
# [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
print(*zip(a,b,c))
# (1, 4, 7) (2, 5, 8) (3, 6, 9)
*zip()
函数是zip()
函数的逆过程,将zip
对象变成原先组合前的数据。44
*zip()
似乎是把迭代器zip()
转化回了元组,但依旧对参数进行了逐个配对
19.hasattr
、getattr
、setattr
45
-
hasattr(object, name)
:判断object
对象中是否存在name
属性,返回布尔值 -
getattr(object, name[, default])
:获取object对象的属性的值- 存在则返回
- 不存在则:
- 没有default参数时,会直接报错
- 给定了default参数
- 若对象本身没有name属性,则会返回给定的default值
- 如果给定的属性name是对象的方法,则返回的是函数对象,需要调用函数对象来获得函数的返回值;调用的话就是函数对象后面加括号,如func之于func();
-
setattr(object, name, value)
:给object
对象的name
属性赋值value
- 如果对象原本存在给定的属性
name
,则setattr
会更改属性的值为给定的value
; - 如果对象原本不存在属性
name
,setattr
会在对象中创建属性,并赋值为给定的value
;
- 如果对象原本存在给定的属性
20.方法遮蔽
子类所继承的父类中的方法,如果存在同名,前面的会“遮蔽”后面的。即前面的直接父类优先级高于后面的。46
#coding=utf-8
class Item:
def info(self):
print("Item中的方法",'这是一个商品')
class Product:
def info(self):
print('Product中的方法','这是一个能赚钱的商品')
class Computer(Item,Product):
pass
c = Computer()
c.info()
# Item中的方法",'这是一个商品