对象编程
轻松看待对象
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73//对象的来历
了解对象,先了解类(class)和对象(object)
在面向过程编程中,函数和模块提高了程序的可复用性.
在面向对象编程中,类和对象同样提交了程序的可复用性,除此之外,类和对象这两种语法结构还加强了程序模拟真实世界的能力."模拟"正式面向对象编程的核心.
python允许程序员以纯粹的面向过程的方式来使用它,所以人们有时会忽略它那颗面向对象的心.
python的一条哲理理念是"一切皆对象"
//类
class Bird(object):
feather = True
reproduction = "egg"
def chirp(self,sound):
print(sound)
python使用关键字class来定义类,冒号和缩进说明了属于这个类的代码
python将一些静态描述信息定义为类的属性;将一些"行为"属性定义为方法(方法是在类内部定义函数来说明)
//对象
通过调用类,我们可以创造出这个类下面一个对象
summer = Brid()
说明summer是属于鸟类的一个对象,作为鸟类的对象summer将拥有鸟类的属性和方法
对属性的引用:
print(summer.reproduction)
对方法的调用:
print(summer.chirp('jijiji'))
在调用方法时,我们传递了一个参数('jijiji'),这正是方法和函数的区别
尽管在定义类的方法时,我们必须加上self参数,但self只能用在类定义的内部,所以在调用方法时不需要对self传入参数
为了完整描述个体,除了共性的类属性外,我们还需要用于说明个性的对象属性,我们通过self来操作对象的属性:
class Bird(object):
feather = True
reproduction = "egg"
def chirp(self,sound):
print(sound)
def set_color(self,color):
self.color = color
summer = Bird()
summer.set_color('red')
print(summer.color)
由于对象属性依赖于self,所以我们必须在某个方法内部才能操作类属性.因此,对象属性没办法像类属性一样,在类下方直接赋初始值
python定义了一系列特殊方法用于初始化对象属性.我们称为魔法方法
例如,__init__()方法,python会在每次创建对象时自动调用,因此可以在__init__()方法内部来初始化对象属性
class Bird(object):
def __init__(self,sound):
self.sound = sound
print('my sound is ',sound)
def chirp(self):
print(self.sound)
summer = Bird('jijiji')
summer.chirp()
self除了操作对象属性外,还可以在一个方法内部调用同一类下的其它方法:
class Bird(object):
def chirp(self,sound)
print(self.sound)
def chirp_repeat(self.sound,n):
for i in range(n):
self.chirp(sound)
summer = Bird()
summer.chirp('jijiji',5)继承
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
48
49类还可以细分为子类,我们通过继承来表达上述概念
例如:
class Bird(object):
feather = True
reproduction = 'egg'
def chirp(self,sound):
print(self.sound)
class Chicken(Bird):
how_to_move = 'walk'
edible = True
class Swan(Bird):
how_to_move = 'swim'
edible = False
summer = Chicken()
print(summer.feather)
summer.chirp('haha')
在类定义时,括号里为Bird,这说明,鸡类是属于鸟类的一个子类,即Chicken继承自Bird
我们可以通过继承来减少程序中重复信息和重复语句
面向对象语言及其继承机制,正式模拟人的意识分类过程
在继承过程中,我们可以在子类中增加父类不存在的属性,从而增强子类功能.此外,还可以在子类中替换父类已经存在的属性
例如:
class Bird(object):
def chirp(self):
print('make sound')
class Chicken(Bird):
def chirp(self):
print('ji')
bird = Bird()
bird.chirp()
chicken = Chicken()
chicken.chirp()
在继承过程中,我们可以通过super关键字在子类中调用父类被覆盖的方法
class Bird(object):
def chirp(self):
print('make sound')
class Chicken(Bird):
def chirp(self):
super().chirp()
print('ji')
bird = Bird()
bird.chirp()
chicken = Chicken()
chicken.chirp()重新来看对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//列表对象
a = [1,2,3,4,5]
我们知道a属于list类型,也就是列表类型.其实,所谓的类型就是对象所属的类的名字
当我们新建一个表时,实际上是在创建list类的一个对象
type()查看类型
dir()查询一个类或对象的所有属性
help()查询函数的说明文档
pass是python中一个特殊的关键字,用于说明在该语法结构中"什么都不做",这个关键字保持了程序结构完整性
//元组和字符串对象
a = (1,2,3)
元组与列表一样都是序列,但元组不能变更内容.因此,元组只能进行查询操作.
b = "abc"
字符串是特殊的元组.字符串有一些方法可以改变字符串,这些方法并不是修改字符串对象,而是删除原有字符串,再建立一个新的字符串,所以并没有违背元组不可变性
//字典对象
字典同样是一个类意想不到的对象
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65//循环对象
循环对象是一个包含__next__()方法的对象,这个方法的目的是生成循环的下一个结果.在生成过循环的所有结果之后,该方法将抛出StopIteration异常
通过iter()方法将一个列表转换为循环对象:
a = iter([1,2])
a.__next__() 显示1
a.__next__() 显示2
a.__next__() 显示StopIteration异常
或者:
for item in iter([1,2]):
print(item)
for结构自动调用__next__()方法,将该方法的返回值赋值给item
相对于序列,循环对象的好处在于: 不用在循环还没有开始的时候,就生成要使用的元素.所有要使用的元素在循环过程中逐渐生成.这样,不仅节省空间,提高效率,还使编程更加灵活
我们可以借助生成器来自定义循环对象,例如:
def gen():
a = 100
yield a
a = a*8
yield a
yield 1000
for i in gen():
print(i)
python中的range()同样是一个循环对象,而不是一个序列
//函数对象
任何一个有__call__()方法的对象都被当做是函数
例如:
class Some(object):
def __call__(self,a):
return a + 5
add_five = Some()
print(add_five(2))
//模块对象
python中的模块对应一个.py文件.模块也是对象.
例如:直接引用标准库中的模块time
import time
print(dir(time))
导入模块的三种方法:
import time
import time as t
from time import sleep
可以将功能相似的模块放在同一个文件夹下,构成一个模块包,比如:
mkdir this_dir
touch this_dir/__init__.py
import this_dir.module
注意:
* 每个模块都有一个__name__()属性,用于记录模块的名字
* 当一个.py文件作为主程序运行时,这个文件也会有一个对应的模块对象.但这个模块对象的__name__属性会是__main__
//异常对象
可以在程序中加入异常处理try结构,用于捕捉程序中出现的异常
try:
...
except xxxx as e:
...
利用except...as...的语法,我们在except结果中用e来代表捕获到的类型对象一切皆对象
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//运算符
list是列表的类,dir(list)查看列表属性,能看到一个属性是__add__().从样式上看,__add__()是特殊方法,它定义了"+"运算对于列表对象的意义,两个list对象相加时,会进行合并列表操作.
print([1,2,3]+[4,5,6]) 等价于 [1,2,3].__add__.[4,5,6]
默认列表是不能进行减法操作的,我们可以定义"-"操作符,例如:
class SuperList(list):
def __sub__(self,b):
a = self[:]
b = b[:]
while len(b) > 0:
element_b = b.pop()
if element_b in a:
a.remove(element_b)
return a
print(SuperList([1,2,3]) - SuperList[3,4])
//元素引用
a = [1,2,3,4,5,6]
print(a[3])
上面程序运行到a[3]时,python发现并理解[]符号,然后调用__getitem__()方法.还有其他特殊函数__setitem__(),__delitem__()等
//内置函数的实现
与运算符类似,很多内置函数也都是调用对象的特殊方法,比如:
len([1,2,3]) 等价于 [1,2,3].__len__()属性管理
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68//属性覆盖的背后
我们在继承中,提到了python中属性的覆盖机制.为了深入理解属性覆盖,我们有必要理解__dict__属性.
当我们调用对象的属性时,这个属性可鞥呢有很多来源.除了来自对象属性和类属性,这个属性还可能是从祖先类那里继承来的.
一个类或对象拥有的属性,会记录在__dict__中.这个__dict__是一个词典,键为属性名,对应的值为某个属性.python在寻找对象属性时,会按照继承关系一次寻找__dict__.
对象属性是分层管理的.对象包含了所有属性,当我们调用某个属性时,python会一层层向下遍历,直到找到那个属性为止
某个属性可能在不同层被重复定义,python在向下遍历过程中,会选取先遇到的那一个,这正是属性覆盖的原理所在
子类的属性比父类的同名属性有优先权,这正是属性覆盖的关键
python在为属性赋值时,只会搜索对象本身的__dict__,如果找不到对应属性,则将在__dict__中添加
//特性
同一个对象的不同属性之间可能存在依赖关系.
当某个属性被修改时,我们希望依赖于该属性的其它属性也同时变化,这时,我们不能通过__dict__的静态词典方式来存储属性
python提供了多种即时生成属性的方法,其中一种称为特性.特性是特殊的属性.
特性使用内置函数property()来创建,property()最多可以加载四个参数:前三个参数为函数,分别用于设置获取,修改和删除特性时,python应该执行的操作.最后一个参数为特性的文档,可以为一个字符串,起说明作用
例如:
class num(object):
def __init__(self,value):
self.value = value
def get_neg(self):
return -self.value
def set_neg(self,value):
self.value = -value
def del_neg(self):
print("value also deleted")
del self.value
neg = property(get_neg,set_neg,del_neg,"I'm negative")
x = num(1.1)
print(x.neg)
x.neg = -22
print(x.neg)
print(x.value)
print(num.neg.__doc__)
//__getattr__()方法
除了内置函数property外,我们还可以使用__getattr__(self,name)来查询即时生成的属性
当我们调用对象的一个属性时,如果通过__dict__机制无法找到该属性,那么python就会调用对象的__getattr__()方法,来即时生成该属性
例如:
class Bird(object):
feather = True
class Chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def __getattr__(self,name):
if name == "adult":
if self.age > 1.0:
return True
else:
return False
else:
raise AttributeError(name)
summer = Chicken(2)
print(summer.adult)
summer.age = 0.5
print(summer.adult)
print(summer.test)
__getattr__()可以将所有的即时生成属性放在一个函数中处理.
__getattr__()可以根据函数名区别处理不同的属性
__getattr__()只能用于查询不在__dict__系统中的属性
__setattr__()用于修改属性
__delattr__()用于删除属性动态类型
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
48
49
50
51
52
53
54//动态类型
动态类型是python的另一个重要核心概念.
python的变量不需要声明,在赋值时,变量可以重新赋值为其它任意值
例如: a = 1 整数1是一个对象,对象的名字是a.
综上: 对象名其实是指向对象的一个引用
对象是存储在内存中的实体,但我们不能直接接触到该对象.对象名是指向这一对象的引用,借着引用操作对象.
通过内置函数id()我们能查看到引用指向的是哪个对象,id()返回对象的编号
除了可以直接打印id外,我们还可以用is运算来判断两个引用是否指向同一个对象
//可变和不可变对象
一个对象可以有多个引用
例如:
a = 5
print(id(a))
b = a
print(id(b))
a = a + 2
print(id(2))
在操作列表对象时,如果通过元素引用改变了某个元素,那么列表对象自身会发生改变,这种自身能发生改变的对象,称为可变对象.但像整数,浮点数和字符串,则不能改变对象本身.赋值最多只能改变引用的指向,这种对象称为不可变对象
//从动态类型看函数的参数传递
函数的参数传递,本质上传递的是引用
例如:
>>> def f(x):
... print(id(x))
... x = 100
... print(id(x))
...
>>> a = 1
>>> print(id(a))
6806632
>>> f(a)
6806632
6808240
>>> print(id(a))
6806632
参数x是一个新的引用,当我们调用函数f时,a作为数据传递给函数,因此x会指向a所指向的对象,也就是进行一次赋值操作.
如果a是不可变对象,那么引用a和x之间相互独立,即对参数x的操作不会影响引用a
如果a是可变对象,那么引用x的操作会对引用a产生影响,即通过一个引用操作可变对象,会影响到其它引用.
例如:
>>> def f(x):
... x[0] = 100
... print(x)
...
>>> a = [1,2,3]
>>> f(a)
[100, 2, 3]
>>> print(a)
[100, 2, 3]
a指向了一个可变列表.在函数调用时,a把指向传递给了参数x.这时,a和x两个引用都指向了同一个可变列表.当函数对内部列表进行操作,会被外部的引用a看到内存管理
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105//引用管理
python是一种动态类型的,面向对象的语言内存管理方式
对象内存管理是基于对象引用的管理!!!
在python中,引用与对象分离: 一个对象可以有多个引用,而每个对象中都存有指向该对象的引用总数,即引用计数
引用计数可以通过sys包中的getrefcount()来查看,例如:
>>> from sys import getrefcount
>>> a = [1,2,3]
>>> print(getrefcount(a))
2
>>> b = a
>>> print(getrefcount(a))
3
注意: getrefcount所得到的结果会比预期多1
//对象引用对象
可变对象(列表,字典)等它们都是数据容器对象,可以包含多个对象
容器对象中包含的并不是元素对象本身,而是指向各个元素对象的引用
当一个对象a被另一个对象b引用时,a的引用计数将增加1
例如:
>>> from sys import getrefcount
>>> a = [1,2,3]
>>> print(getrefcount(a))
2
>>> b = [a,a]
>>> print(getrefcount(a))
4
容器对象的引用会构成很复杂的拓扑结构,可以使用objgraph包来绘制其引用关系
例如:
>>> import objgraph
>>>
>>> x = [1,2,3]
>>> y = [x,dict(key=1)]
>>> z = [y,(x,y)]
>>>
>>> objgraph.show_refs([z],filename="ref_top.png")
Graph written to /tmp/objgraph-YJf5mO.dot (9 nodes)
Image generated as ref_top.png
两个对象可能相互引用,从而构成引用环.即时单个对象,自己引用自己,也能构成引用环
例如:
>>> aa = []
>>> bb = [aa]
>>> aa.append(bb)
>>> objgraph.show_refs([aa],filename="aa_top.png")
Graph written to /tmp/objgraph-odsHZ9.dot (2 nodes)
Image generated as aa_top.png
>>> cc = []
>>> cc.append(cc)
>>> objgraph.show_refs([cc],filename="cc_top.png")
Graph written to /tmp/objgraph-Wtvf78.dot (1 nodes)
Image generated as cc_top.png
引用环会给垃圾回收机制带来很大的麻烦
使用del关键字删除某个引用,也可以用于删除容器中的元素
例如:
>>> from sys import getrefcount
>>> a = [1,2,3]
>>> b = a
>>> print(getrefcount(b))
3
>>> a = 1
>>> print(getrefcount(b))
2
//垃圾回收
当python中的对象越来越多时,它们占据越来越大的内存,它会在适当的时候启动垃圾回收,用于清理没用的对象
原理上,当python的某个对象的引用计数为0时,即没用任何引用指向该对象时,该对象就称为要被回收的垃圾了.当垃圾回收启动时,python扫描到这个引用计数为0的对象,就会将它所占据的内存清空.
当python运行时,会记录其中"分配对象"和"取消分配对象"的次数.当两者的差值高于某个阀值时,垃圾回收才会启动
我们可以通过gc模块的get_threshold()方法看到该阀值:
>>> import gc
>>> print(gc.get_threshold())
(700, 10, 10)
700 垃圾启动的阀值,通过gc的set_threshold()重新设置
10 每10次0代回收,会配合1次1代的垃圾回收,此时清理0,1代
10 每10次1代回收,会配合1次2代的垃圾回收,此时清理0,1,2代
也可以手动回收,通过gc模块的collect()方法
python除了上面的基础回收外,还同时采用了分代回收策略:存活的时间越久,越不可能在后面的程序中变成垃圾.
python将所有的对象分为0,1,2三代:
所有的新建对象都是0代对象,当某一代对象经历过垃圾回收,依然存活,那么它就进入下一代对象.垃圾回收启动时,一定会扫描所有的0代对象.如果0代对象经过一定次数垃圾回收,那么就启动对0代和1代的扫描清理.当1代也经历了一定次数的垃圾回收后,就会启动对0,1,2代的扫描,即对所有对象进行扫描
//孤立的引用环
引用环的存在会给上面的垃圾回收机制带来很大的困难
例如:
>>> a = []
>>> b = [a]
>>> a.append(b)
>>> del a
>>> del b
由于引用环的存在,这两个对象的引用计数都没有将到0,所以不会被垃圾回收.
为了回收这样的引用环,python会复制每个对象的引用计数,可以标记为gc_ref.
假设,每个对象i,该计数为gc_ref_i.python会遍历所有对象i,对于每个对象i所引用的对象j,将相应的gc_ref_j减1,遍历后的结果gc_ref为0,进行垃圾回收.如果不为0,则对象被保留