6.1 一切皆为对象
1.运算符
我们知道,列表是list类,若用dir(list)查看就会看见若干属性例如__add__(),__class__()等:
用__add__()方法来说,从样式上看它是特殊方法,特殊之处在于,这个方法定义了’+‘运算符对于list对象的意义。比如两个list对象相加时:
print([1,2,3]+[4,5])
#打印结果为[1,2,3,4,5]
对于list对象,它的运算符,’+‘,’-‘,’>‘,'<',and,or等都是通过这些特殊方法实现的:
'abc'+'xyz'
#实际上执行了如下操作
'abc'.__add__('xyz')
所以可以基本上这样认为:两个对象是否能够进行加法运算,首先要看相应的对象有没有__add__()这个方法,若有,我们便可以对这个对象执行加法操作。
尝试以下操作并推断他们所代表的运算符:
(1.8).__mul__(2.0)
True.__or__(False)
打印结果:
由此推断:
__mul__()方法相当于运算符*
__or__()方法相当于运算符或or
有了这些认知基础,那我们可以尝试列表之间的减法,由于list之间不能直接相减,故我们需要自定义一个特殊方法代表运算符“-”:
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]))
打印结果:
2.元素引用
下面是我们常见的列表元素引用方法:
li = [1,2,3,4,5,6]
print(li[3])
#打印结果 4
上面的程序运行到print(li[3])中的[3]时,python发现并理解了[]符号,然后调用__getitem__()方法:
li = [1,2,3,4,5,6]
print(li.__getitem__(3))
#打印结果 4
看下面程序,推断它的对应:
li = [1,2,3,4,5,6]
li.__setitem__(3,0)
print(li)
由于打印结果为:,故可知__setitem__()是list的赋值操作。
exa = {'a':1,'b':2}
exa.__delitem__('a')
print(exa)
打印结果为:,故可知__delitem__()是字典的元素删除方法。
3.内置函数的实现
与运算符类似,许多内置函数也是调用对象的特殊方法。
len([1,2,3]) #返回列表中元素的总数
实际上的运行为:
[1,2,3].__len__()
还有很多特殊方法,例如:
print((-1).__abs__()) #打印1 此为绝对值运算符
print((2.3).__int__()) #打印2 此为取整运算
6.2 属性管理
1.属性覆盖的真实
在继承中,我们提到了属性覆盖的机制,为了深入了解属性覆盖,我们需要了解python的__dict__属性。
当我们调用对象的属性时,这个属性可能有很多来源,除了来自对象属性和类属性,这个属性还可以从祖先类那里继承而来。
实际上,一个类或对象拥有的属性会记录在__dict__中,这个__dict__是一个词典,键为属性名,对应的值为某个属性。python在寻找对象属性时,会按照继承关系依次寻找__dict__.
class Bird(object):
feather = True
def chirp(self):
print('Birdchrip')
class Chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def chirp(self):
print('Chickenchrip')
Neo = Chicken('2')
print('===>Neo')
print(Neo.__dict__)
print('===>Chicken')
print(Chicken.__dict__)
print('===>Bird')
print(Bird.__dict__)
print('===>object')
print(object.__dict__)
打印结果为:
这个排列顺序是按照与Neo对象的亲近关系而排,第一部分为Neo对象自身的属性,就是age。第二部分为Chicken类的属性,比如fly()和__init__()方法。第三部分是Bird类的属性,比如feather属性。最后一部分属于object类,有诸如__doc__()之类的属性。
如果我们用dir()来查看对象Neo的属性的话,可以看到Neo对象包含了全部四个部分,也就是说,对象属性是分层管理的,对象Neo能接触到所有的属性,分别存在Neo/Chicken/Bird/object这四层,当我们调用某个属性的时候,python会一层一层向下遍历直到找到那个属性。由于对象不需要重复存储其祖先类的属性所以分层管理的机制可以节省存储空间。
属性覆盖的原理:某个属性可能在不同的层被重复定义。python在向下的遍历过程中会选取先遇到的那一个,这也正是属性覆盖的原理所在。
在上例中可以看到,Chicken和Bird类都有chirp()方法,如果从Neo调用chirp()方法,那么优先使用的将是和对象Neo关系更近的Chicken的版本:
class Bird(object):
feather = True
def chirp(self):
print('Birdchrip')
class Chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def chirp(self):
print('Chickenchrip')
Neo = Chicken('2')
Neo.chirp()
打印
子类的属性比父类的同名属性更优先,这也是属性覆盖的关键之处。
以上的程序都是调用属性的操作,如果进行赋值,那么python就不会进行分层深入查找了,下面创建一个新的Chicken类对象autumn,并通过autumn修改feather这一类属性:
class Bird(object):
feather = True
def chirp(self):
print('Birdchrip')
class Chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def chirp(self):
print('Chickenchrip')
Neo = Chicken(2)
autumn = Chicken(3)
autumn.feather = False
print(autumn.feather)
print(Neo.feather)
print(autumn.__dict__)
打印结果:
由结果可以看出,尽管autumn修改了feather的属性值,但并没有影响到Bird类的类属性。当我们使用__dict__方法查看autumn对象的属性时,会发现新建了一个名为feather的对象属性。
因此,python在对对象属性赋值时,只会搜索对象本身的__dict__,如果找不到对应属性则会在__dict__中增加。
2.特性
特性(property):python中一种即时生成属性的方法,是一种特殊的属性
下面举例:为Chicken类增加一个表示成年与否的特性adult,当对象年龄(age)超过1岁时,adult为真,否则为假
class Bird(object):
feather = True
class Chicken(Bird):
fly = False
def __init__(self,age):
self.age = age
def get_adult(self):
if self.age > 1.0:
return True
else:
return False
adult = property(get_adult)
Neo = Chicken(2)
print(Neo.adult)
Mary = Chicken(0.5)
print(Mary.adult)
Neo.age = 0.1
print(Neo.adult)
打印结果:
特性使用内置函数property()来创建,property()最多可以加载四个参数。前三个参数为函数,分别用于设置【获取】,【修改】和【删除】特性时,python应该执行的操作。最后一个参数为特性的文档,可以是一个字符串,作说明之用:
neg = property(get_neg,set_neg,del_neg,"I'm negative")
语句中,当一个数字确定时,它的负数总是确定的,这个由get_neg来实现,当我们需要修改一个负数时,它本身的值也会随之变化,这个由set_neg来实现,del_neg的作用在于,如果删除特性neg,那么应该执行的操作应该是删除属性value。property最后一个参数“I'm negative”为特性neg的说明文档。
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.value)
print(num.neg.__doc__)
del x.neg
3.__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)
Neo = Chicken(2)
print(Neo.adult)#打印True
Mary = Chicken(0.2)
print(Mary.adult)#打印False
print(Mary.male)#抛出AttributeError异常
每个特性都需要有自己的处理函数,而__getattr__()可以将所有的即时生成的属性放在同一个函数中处理,__getattr__()可以根据函数名区别处理不同的属性。需要注意的是,__getattr__()只能用于查询不在__dict__系统中的属性。
__setattr__(self,name,value)和__delattr__(self,name)可以用于修改和删除属性,他们的应用面更广,可以用于任意属性。
6.3 动态类型
1.动态类型
动态类型(Dynamic Typing)是python的一个重要概念。
之前我们知道,python不需要声明,在赋值时,变量可以重新赋值为其他任意值。这种类型改变的能力我们称之为动态类型。
a = 1
在python中,整数1是个对象,对象名为‘a’,但更精确的说,对象名其实是一个指向对象的一个引用。对象是存储在内存中实体,但我们并不能直接接触到对象,对象名是指向这一对象的引用(reference)。
通过内置函数id()我们能查看到引用指向的是哪个对象,这个函数能返回对象的编号:
a = 1
print(id(a))
print(id(1))
打印结果:
可以看到,赋值之后,对象1和引用a返回了相同的编号。
在python中,赋值其实是用对象名这个筷子去夹盘中的食物。每次赋值时,我们让左侧的引用指向右侧的对象。引用能随时指向一个新的对象:
a = 3
print(id(a))
a = 'at'
print(id(a))
打印结果:
从两次返回的id可以得知,对象名a所指向的对象发生了改变,对象类型也发生了改变,因此,python是一门动态类型语言。
除了直接打印id的方法来判断两个引用是否指向同一个对象,我们还能用is运算来判断:
a = 1
b = 1
print(a is b)
打印结果:
2.可变与不可变对象
一个对象可以有多个引用:
a = 5
print(id(a)) #打印1610744400
b = a
print(id(a)) #打印1610744400
print(id(b)) #打印1610744400
a = a + 2
print(id(a)) #打印1610744464
print(id(7)) #打印1610744464
print(id(b)) #打印1610744400
打印结果:
通过程序结果可知,我们让a与b指向同一个整数对象5,其中,b=a的含义是引用b指向引用a所指的那一个对象。然后对a增加2,实际上是让a指向了(5+2=7)这个7,这时,b的指向没有发生变化。
※但是对于列表:
list2 = [1,2,3]
list1 = list2
list1[0] = 10
print(list2) #打印[10,2,3]
可以看到,我们改变了list1时,list2也随着发生了改变。但这和引用的独立性不矛盾,因为list1与list2的指向没有发生偏差,还是同一个列表。列表是一个包含了多个引用的集合,其每一个元素都是一个引用,比如list[0],list[1]等,这个引用又指向一个对象,比如1,2,3。list[0]=10这个操作不是对整个列表list1,而是单对list[0]这个元素而作,所以,列表中的一个元素的指向发生了变化,那么引用整个列表的对象都会受到影响。
因此,在操作列表时,如果通过元素引用改变了某个元素,那么列表对象自身会发生改变,列表这种自身能发生改变的对象被我们称为可变对象(Mutable Object)。词典也是可变对象。
但是整数,浮点数,字符串就不能改变自身,赋值最多能改变其引用的指向,这种被我们称为不可变对象(Immutable Object),元组包含多个元素,但已定值,所以元组也是不可变对象。
3.从动态类型看函数参数传递
函数的参数传递,本质上传递的是引用:
def f(x):
print(id(x)) #打印1610744272
x = 100
print(id(x)) #打印1610744272
a = 1
print(id(a)) #打印1610747440
f(a)
print(a) #打印1
参数x是一个新的引用,当我们调用函数f()时,a作为数据传递给函数,因此x会指向a所指的对象,也就是进行一次赋值操作。如果a是不可变对象,那么引用a与x之间相互独立,即对参数x的操作不会影响到引用a。
如果传递的是可变对象,那么情况就会发生变化:⚠
def f(x):
x[0] = 100
print(x)
a = [1,2,3]
f(a)
print(a) #打印 [100, 2, 3]\n[100, 2, 3]
可以看到,在f(x)内被改变的x[0]元素影响到了原本的list a,编程时要对此问题多加注意。
6.4 内存管理
1.引用管理
语言的内存管理是语言设计的一个重要方面,它是决定语言性能的重要因素。
以下是python这门动态语言的面向对象的语言的内存管理方式:
对象内存管理是基于对引用的管理,在python中,对象与引用分离,一个对象可以有多个引用,而每个对象中都存有指向该对象的引用总数,即引用计数(Reference Count)。我们可以使用标准库sys包getrefcount(),来查询某个对象的引用计数。需要注意的是,当使用某个引用作为参数,传递给getrefcount()时,参数实际上是创建了一个临时的引用,因此,getrefcount()所得到的结果会比期望的多1:
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) #打印2
b = a
print(getrefcount(b)) #打印3
2.对象引用对象
我们之前提到了一写可变对象,例如列表和词典,这些都是数据的容器对象,可以包含多个对象,实际上容器对象中包含的并不是对象本身,而是指向各个元素的引用。我们也可以自定义一个对象,并引用其他对象:
class from_obj(object):
def __init__(self,to_obj):
self.to_obj = to_obj
b = [1,2,3]
a = from_obj(b)
print(id(b)) #打印2028587858312
当一个对象a被另一个对象b引用时,a的引用计数将增加1
from sys import getrefcount
a = [1,2,3]
print(getrefcount(a)) #打印2
b = [a,a]
print(getrefcount(a)) #打印4
由于对象b引用了两次a,因此a的引用计数增加了2。