Python03深入

Python深入

特殊方法与多范式

特殊方法,或者说魔术方法, 方法名的前后各有两个下划线, 可以通过dir()查看对象拥有的特殊方法

1
print(dir(1))

当对象中定义了特殊方法的时候,Python也会对它们有“特殊优待”。比如定义了__init__()方法的类,会在创建对象的时候自动执行__init__()方法中的操作。

运算符

Python的运算符是通过调用对象的特殊方法实现的

1
2
3
4
result = 'abc' + 'xyz'
result = 'abc'.add('xyz')
print(('abc' + 'xyz') == ('abc'.add('xyz'))) # True
print(result) # abcxyz

所以,在Python中,两个对象是否能进行加法运算,首先就要看相应的对象是否有__add__()方法。一旦相应的对象有__add__()方法,即使这个对象从数学上不可加,我们都可以用加法的形式,来表达obj.__add__()所定义的操作。在Python中,运算符起到简化书写的功能,但它依靠特殊方法实现。

Python不强制用户使用面向对象的编程方法。用户可以选择自己喜欢的使用方式 (比如选择使用+符号,还是使用更加面向对象的__add__()方法)。

内置函数

与运算符类似,许多内置函数也都是调用对象的特殊方法

1
2
3
result = len([1,2,3])
result = [1,2,3].__len__()
print(result) # 3

列表(list)元素引用

程序运行到li[3]的时候,Python发现并理解[]符号,然后调用__getitem__()方法。

1
2
3
li = [1,2,3,4,5]
print(li[3])
print(li.__getitem__(3)) # 4

函数

函数也是一种对象。实际上,任何一个有__call__()特殊方法的对象都被当作是函数。比如下面的例子:

1
2
3
4
5
6
7
8
class Samples(object):
def __call__(self, a):
return a+5

add = Samples() # add代表一个函数
print(add(2)) # 函数方式调用 7
for i in map(add,[2,3,4,5]):
print(i) # 7 8 9 10

add为SampleMore类的一个对象,当被调用时,add执行加5的操作。add还可以作为函数对象,被传递给map()函数。

上下文管理器

上下文管理器(context manager)是Python2.5开始支持的一种语法,用于规定某个对象的使用范围。一旦进入或者离开该使用范围,会有特殊操作被调用 (比如为对象分配或者释放内存)。它的语法形式是with...as...

关闭文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# without context manager

f = open('record1.txt','w',encoding='utf-8')
f.write('ni9ne')
print(f.closed) # False
f.close()
print(f.closed) # True

# with context manager
with open('record2.txt','w',encoding='utf-8') as f:
print(f.closed) # False
f.write('ni9ne')
print(f.closed) # False
print(f.closed) # True
1
2
3
4
5
6
7
8
9
两段程序实际上执行的是相同的操作。
我们的第二段程序就使用了上下文管理器 (with...as...)。
上下文管理器有隶属于它的程序块。当隶属的程序块执行结束的时候(也就是不再缩进),上下文管理器自动关闭了文件 (我们通过f.closed来查询文件是否关闭)。
我们相当于使用缩进规定了文件对象f的使用范围。

上面的上下文管理器基于f对象的__exit__()特殊方法。
当我们使用上下文管理器的语法时,我们实际上要求Python在进入程序块之前调用对象的__enter__()方法,在结束程序块的时候调用__exit__()方法。
对于文件对象f来说,它定义了__enter__()和__exit__()方法(可以通过dir(f)看到)。
在f的__exit__()方法中,有self.close()语句。所以在使用上下文管理器时,我们就不用明文关闭f文件了。

自定义

任何定义了__enter__()__exit__()方法的对象都可以用于上下文管理器。文件对象f是内置对象,所以f自动带有这两个特殊方法,不需要自定义。

1
2
3
4
5
6
7
8
9
10
11
12
class MyVow(object):
def __init__(self, text):
self.text = text
def __enter__(self):
self.text = 'i say: ' + self.text
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.text = self.text + '!'

with MyVow("i'm fine") as vow:
print(vow.text) # i say: i'm fine
print(vow.text) # i say: i'm fine!

在进入上下文和离开上下文时,对象的text属性发生了改变(最初的text属性是”I’m fine”)。

__enter__()返回一个对象。上下文管理器会使用这一对象作为as所指的变量,也就是myvow。在__enter__()中,我们为myvow.text增加了前缀 (“I say: “)。在__exit__()中,我们为myvow.text增加了后缀(“!”)。

注意: __exit__()中有四个参数。当程序块中出现异常(exception),__exit__()的参数中exc_type, exc_value, traceback用于描述异常。我们可以根据这三个参数进行相应的处理。如果正常运行结束,这三个参数都是None。

对象的属性

Python一切皆对象(object),每个对象都可能有多个属性(attribute)。Python的属性有一套统一的管理方案。

属性的__dict__系统

对象的属性储存在对象的__dict__属性中。__dict__为一个词典,键为属性名,对应的值为属性本身。

1
2
3
4
5
6
7
8
9
10
11
12
class bird(object):
feather = True

class chick(bird):
fly = False
def __init__(self, age):
self.age = age

summer = chick(2)
print(bird.__dict__)
print(chick.__dict__)
print(summer.__dict__)

image-20220610183336857

Python中的属性是分层定义的,比如这里分为object/bird/chick/summer这四层。当我们需要调用某个属性的时候,Python会一层层向上遍历,直到找到那个属性。(某个属性可能出现在不同的层被重复定义,Python向上的过程中,会选取先遇到的那一个,也就是比较低层的属性定义)。

上面的情况中,我们已经知道了summer对象的类为chick,而chick类的父类为bird。如果只有一个对象,而不知道它的类以及其他信息的时候,我们可以利用__class__属性找到对象的类,然后调用类的__base__属性来查询父类

1
2
print(summer.__class__) # chick 查询对象所属的类    <class '__main__.chick'>
print(chick.__base__) # bird 查询父类 <class '__main__.bird'>

特性property

特性(property)。同一个对象的不同属性之间可能存在依赖关系。当某个属性被修改时,我们希望依赖于该属性的其他属性也同时变化。这时,我们不能通过__dict__的方式来静态的储存属性。Python提供了多种即时生成属性的方法。其中一种称为特性(property)。

特性使用**内置函数property()**来创建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class bird(object):
feather = True
class chicken(bird):
fly = False
def __init__(self,age):
self.age = age
def getAdlut(self):
if self.age > 1.0 : return True
else: return False
adult = property(getAdlut) # 建立特性

summer = chicken(2)
print(summer.adult) # True
summer.age = 0.4
print(summer.adult) # False

property()最多可以加载四个参数。前三个参数为函数,分别用于处理查询特性、修改特性、删除特性。最后一个参数为特性的文档,可以为一个字符串,起说明作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class num(object):
def __init__(self,value):
self.value = value
def getNeg(self):
return -self.value
def setNeg(self,value):
self.value = -value
def delNeg(self):
print('value also deleted')
del self.value
neg = property(getNeg, setNeg, delNeg)

x = num(1.1) # 生成对象
print(x.value) # 访问属性 1.1
print(x.neg) # 访问特性,动态修改为 -1.1
x.neg = -22 # 设置特性
print(x.value) # 访问特性, 动态修改为 22
del x.neg # 删除特性
print(x.value) # 访问特性报错: 'num' object has no attribute 'value'

使用特殊方法__getattr__

我们可以用__getattr__(self, name)来查询即时生成的属性。当我们查询一个属性时,如果通过__dict__方法无法找到该属性,那么Python会调用对象的__getattr__方法,来即时生成该属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class bird(object):
feather = True
class chicken(bird):
fly = False
def __init__(self,age):
self.age = age
def __getattr__(self, item):
if item == 'adult':
if self.age >= 1.0:return True
else:return False
else: raise AttributeError(item)

summer = chicken(2)
print(summer.adult) # 访问不存在的特性adult,进入__getattr__ True
summer = chicken(0.5)
print(summer.adult) # 访问不存在的特性adult,进入__getattr__ False
print(summer.weight) # 报错 AttributeError: weight

每个特性需要有自己的处理函数,而__getattr__可以将所有的即时生成属性放在同一个函数中处理。__getattr__可以根据函数名区别处理不同的属性。比如上面我们查询属性名weight的时候,raise AttributeError。

Python中还有一个__getattribute__特殊方法,用于查询任意属性。__getattr__只能用来查询不在__dict__系统中的属性

__setattr__(self, name, value)__delattr__(self, name)可用于修改和删除属性。它们的应用面更广,可用于任意属性。

闭包

函数对象的作用域

和其他对象一样,函数对象也有其存活的范围,也就是函数对象的作用域。函数对象是使用def语句定义的,函数对象的作用域与def所在的层级相同。

1
2
3
4
5
6
7
def line_conf():
def line(x):
return 2*x+1
print(line(5))

line_conf() # 11
print(line(5)) # NameError: name 'line' is not defined 说明这时已经在作用域之外。

如果使用lambda定义函数,那么函数对象的作用域与lambda所在的层级相同。

闭包

函数是一个对象,所以可以作为某个函数的返回结果

1
2
3
4
5
6
7
def line_conf():
def line(x):
return 2*x+1
return line

my_line = line_conf()
print(my_line(5)) # 11

引用了外部的变量

1
2
3
4
5
6
7
8
9
def line_conf1():
b=15 # 环境变量, 函数对象定义时可供参考的b值
def line(x):
return 2*x+b
return line

b = 5
my_line = line_conf1()
print(my_line(5)) # 25 line所参照的b值是函数对象定义时可供参考的b值15,而不是使用时的b值5。

一个函数和它的环境变量合在一起,就构成了一个闭包(closure) , 即闭包是一个包含有环境变量取值的函数对象。环境变量取值被保存在函数对象的__closure__属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def line_conf1():
b=15 # 环境变量, 函数对象定义时可供参考的b值
def line(x):
return 2*x+b
return line

my_line = line_conf1()

closure_tuple = my_line.__closure__
print(closure_tuple) # (<cell at 0x01BAE580: int object at 0x5F8EE890>,)
print(closure_tuple[0].cell_contents) # 15
# __closure__里包含了一个元组(tuple)。
# 这个元组中的每个元素是cell类型的对象。
# 第一个cell包含的就是整数15,也就是创建闭包时的环境变量b的取值。

image-20220610191532084

闭包与并行运算

闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。

在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。正适合函数式编程。

装饰器 decorator

装饰器可以对一个函数、方法或者类进行加工

装饰函数和方法

使用场景: 在原有功能上新增需求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 原代码
def square_sum(a,b):
return a**2+b**2
def square_diff(a,b):
return a**2-b**2
print(square_sum(3,4))
print(square_diff(3,4))

# 修改上述函数为从提示输入数据
def square_sum_input(a,b):
print('input:',a,b)
return a**2 + b**2
def square_diff_input(a,b):
print('input:',a,b)
return a**2 - b**2
print(square_sum_input(3,4))
print(square_diff_input(3,4))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用装饰器实现修改
def decorator1(F):
def new_F(a,b):
print('input:',a,b)
return F(a,b)
return new_F

@decorator1
def square_sum_input_d(a,b):
return a**2+b**2

@decorator1
def square_diff_input_d(a,b):
return a**2-b**2

print(square_sum_input_d(3,4))
print(square_diff_input_d(3,4))

# 相当于:
# square_sum_d = decorator1(square_sum_input_d)
# square_sum_d(3, 4)

装饰器可以用def的形式定义,如上面代码中的decorator。装饰器接收一个可调用对象作为输入参数,并返回一个新的可调用对象。装饰器新建了一个可调用对象,也就是上面的new_F。new_F中,我们增加了打印的功能,并通过调用F(a, b)来实现原有函数的功能。

如果我们有其他的类似函数,我们可以继续调用decorator来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

含参的装饰器

在上面的装饰器调用中,比如@decorator,该装饰器默认它后面的函数是唯一的参数。装饰器的语法允许调用decorator时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def pre_str(pre = ''):
def decorator(F):
def new_F(a,b):
print(pre+' input:',a,b)
return F(a,b)
return new_F
return decorator

@pre_str('-_-')
def square_sum_p(a,b):
return a**2+b**2

print(square_sum_p(3,4)) #-_- input: 3 4 25
# 相当于
# square_sum_p1 = pre_str('-_-') (square_sum_p)
# print(square_sum_p1(3,4))

上面的pre_str是允许参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有环境参量的闭包。当我们使用@pre_str('-_-')调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。

装饰类

在Python 2.6以后,装饰器被拓展到类。一个装饰器可以接收一个类,并返回一个类,从而起到加工类的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def ClassDecorator(aClass):
class newClass:
def __init__(self,age):
self.called_time = 0
self.call_class = aClass(age)
def dis(self):
self.called_time += 1
print('have been called ', self.called_time, 'times')
self.call_class.display()
return newClass

@ClassDecorator
class Bird:
def __init__(self,age):
self.age = age
def display(self):
print('bird age is:',self.age)

bird = Bird(5)
for i in range(3):
bird.dis() # have been called 1 times bird age is: 5 * 3

在decorator中,我们返回了一个新类newClass。在新类中,我们记录了原来类生成的对象(self.call_class),并附加了新的属性called_time,用于记录调用display的次数。我们也同时更改了display方法。

内存管理

对象的内存使用

为了探索对象在内存的存储,可以求助于Python的内置函数id()。它用于返回对象的身份(identity)

1
2
3
a = 1   # 整数1为一个对象。而a是一个引用。
print(id(a)) # 1603200944 内存地址的十进制表示
print(hex(id(a))) # 0x5f8ee7b0 内存地址的十六进制表示

整数和短小的字符,Python都会缓存这些对象,以便重复使用。

当我们创建多个等于1的引用时,实际上是让所有这些引用指向同一个对象。

1
2
3
4
b = 1
print(id(a)) # 1603200944
print(id(b)) # 1603200944
# 可见a和b实际上是指向同一个对象的两个引用

为了检验两个引用指向同一个对象,我们可以用is关键字。is用于判断两个引用所指的对象是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
a = 1
b = 1
print(a is b) # True
a = 'good'
b = 'good'
print(a is b) # True
a = 'today is a good day!, isnt it'
b = 'today is a good day!, isnt it'
print(a is b) # True
a = []
b = []
print(a is b) # False
# 长的字符串和其它对象可以有多个相同的对象,可以使用赋值语句创建出新的对象。

在Python中,每个对象都有存有指向该对象的引用总数,即引用计数(reference count)。

我们可以使用sys包中的getrefcount(),来查看某个对象的引用计数

需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上创建了一个临时的引用。因此,getrefcount()所得到的结果,会比期望的多1。

1
2
3
4
5
6
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) # 2
b = a
print(getrefcount(a)) # 3
print(getrefcount(b)) # 3

对象引用对象

Python的一个容器对象(container),比如表、词典等,可以包含多个对象。实际上,容器对象中包含的并不是元素对象本身,是指向各个元素对象的引用。

也可以自定义一个对象,并引用其它对象:

1
2
3
4
5
6
7
8
9
class from_obj(object):
def __init__(self,to_object):
self.to_object = to_object

b = [1,2,3]
a = from_obj(b)
print(id(a.to_object)) # 20977608
print(id(b)) # 20977608
print(a.to_object is b) # True 可以看到,a引用了对象b。

对象引用对象,是Python最基本的构成方式。

即使是a = 1这一赋值方式,实际上是让词典的一个键值”a”的元素引用整数对象1。

该词典对象用于记录所有的全局引用。该词典引用了整数对象1。

我们可以通过内置函数globals()来查看该词典。

当一个对象A被另一个对象B引用时,A的引用计数将增加1。

1
2
3
4
5
6
7
8
a = 1
print(globals()['a']) # 1

from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) # 2
b = [1,2,a,a]
print(getrefcount(a)) # 4

容器对象的引用可能构成很复杂的拓扑结构。我们可以用objgraph包来绘制其引用关系,objgraph是Python的一个第三方包。安装之前需要安装xdot

1
2
# pip install xdot  &&  pip install objgraph
# 下载graphviz https://www.graphviz.org/download/
1
2
3
4
5
x = [1,2,3]
y = [x,dict(key1=x)]
z = [y,(x,y)]
import objgraph
objgraph.show_refs([z],filename='ref.png') # Image generated as ref.png

image-20220610194259663

image-20220610194323329

两个对象可能相互引用,从而构成所谓的引用环(reference cycle)。引用环会给垃圾回收机制带来很大的麻烦

1
2
3
4
5
import objgraph
a = []
b = [a]
a.append(b)
objgraph.show_refs([a],filename='ref.png')

image-20220610194515112

即使是一个对象,只需要自己引用自己,也能构成引用环。

1
2
3
4
import objgraph
a = []
a.append(a)
objgraph.show_refs([a],filename='ref.png')

image-20220610194620208

引用减少

某个对象的引用计数可能减少。比如,可以使用del关键字删除某个引用:

1
2
3
4
5
6
a = [1,2,3]
b = a
from sys import getrefcount
print(getrefcount(b)) # 3
del a
print(getrefcount(b)) # 2
1
2
3
4
5
6
7
# 如果某个引用指向对象A,当这个引用被重新定向到某个其他对象B时,对象A的引用计数减少:
c = [1,2,3]
d = c
from sys import getrefcount
print(getrefcount(d)) # 3
c = 1
print(getrefcount(d)) # 2

垃圾回收

当Python的某个对象的引用计数降为0时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。

Python只会在特定条件下,自动启动垃圾回收

运行时记录其中分配对象(object allocation)和取消分配对象(object deallocation)的次数。

当两者的差值高于某个阈值时,垃圾回收才会启动。

可以通过gc模块的get_threshold()方法,查看该阈值:

1
2
3
4
5
import gc
print(gc.get_threshold()) # (700, 10, 10)
# 700即是垃圾回收启动的阈值。可以通过gc中的set_threshold()方法重新设置。
# gc.set_threshold(700,10,5)
# 我们也可以手动启动垃圾回收,即使用gc.collect()。

分代回收

1
2
3
4
5
6
7
# 分代(generation)回收的策略。这一策略的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。
# Python将所有的对象分为0,1,2三代。所有的新建对象都是0代对象。当某一代对象经历过垃圾回收,依然存活,那么它就被归入下一代对象。
# 垃圾回收启动时,一定会扫描所有的0代对象。如果0代经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理。
# 当1代也经历了一定次数的垃圾回收后,那么会启动对0,1,2,即对所有对象进行扫描。
# get_threshold()返回的(700, 10, 10)返回的两个10。
# 也就是说,每10次0代垃圾回收,会配合1次1代的垃圾回收;而每10次1代的垃圾回收,才会有1次的2代垃圾回收。
# 同样可以用set_threshold()来调整,比如对2代对象进行更频繁的扫描。
1
2
3
import gc
gc.set_threshold(700,10,5)
print(gc.get_threshold()) # (700, 10, 5)

孤立的引用环

引用环的存在会给上面的垃圾回收机制带来很大的困难。

这些引用环可能构成无法使用,但引用计数不为0的一些对象。

1
2
3
4
5
6
7
8
9
from sys import getrefcount
a = []
b = [a]
a.append(b)
print(getrefcount(b)) # 3
del a
del b
import gc
print(gc.collect()) #2

先创建了两个表对象,并引用对方,构成一个引用环。删除了a,b引用之后,这两个对象不可能再从程序中调用,就没有什么用处了。但是由于引用环的存在,这两个对象的引用计数都没有降到0,不会被垃圾回收。

img

为了回收这样的引用环,Python复制每个对象的引用计数,可以记为gc_ref。假设,每个对象i,该计数为gc_ref_i。Python会遍历所有的对象i。对于每个对象i引用的对象j,将相应的gc_ref_j减1。

img

在结束遍历后,gc_ref不为0的对象,和这些对象引用的对象,以及继续更下游引用的对象,需要被保留。而其它的对象则被垃圾回收。