Python入门自学进阶——6--类与对象-成员修饰符特殊成员及元类

Posted kaoa000

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python入门自学进阶——6--类与对象-成员修饰符特殊成员及元类相关的知识,希望对你有一定的参考价值。

一、成员修饰符

Python中,类中的字段分为公有成员、私有成员

字段名如果是以双下划线开头,则为私有成员。

__age是私有成员,外部无法直接访问 ,需要在类中定义一个方法来读取这个私有成员变量:

 静态字段也可以定义为私有成员,也需要定义一个内部的方法来间接获得这个私有成员,否则将错误:

 字段能够定义成公有、私有,方法也可以定义成公有和私有,一样是前面加双下划线:

 同样需要定义一个公有方法访问:

 继承中的私有成员的访问测试:父类中有私有成员,被子类继承后,子类是否能够访问父类的私有成员呢?

通过上面的测试,子类G继承了父类F,但是子类的对象obj中的方法也不能访问父类F中的私有成员f2,所以 要访问一个私有成员,只能在相同的类中定义公有方法来访问私有成员,不能跨类访问,继承关系也不行。

二、特殊成员

前边学过的__init__(self)是一个特殊成员,叫做构造方法。类的后面加上括号,就会执行这个方法。如类Foo,那么Foo()就执行__init__(self)

另一个特殊成员:__call__(self,*args,**kwargs),这个方法是在生成的对象后面加上括号时调用,如:如类Foo,生成对象obj = Foo(),那么,obj()就会执行__call__()方法。

第三个特殊成员,对于字符串s = ‘123’,可以使用i = int(s)转换为整型,s字符串也是一个对象,它实际上是s = str(‘123’)的变形,是类str的一个对象,int将一个对象变换为整数,那么int是否能将其他的类对象也变换为整型呢? 如

但是如果在被转换的对象中定义了__int__()方法,就可以整型转换:

所以,int()在本质上是调用了被转换对象的__int__()方法,并将返回值赋值给int对象。

同理,还有一个__str__()方法,将对象转换为字符串。每个类都有这个方法,默认返回是<__main__.Foo object at 0x0000000002121748>这种形式,print(obj)实际就是打印obj.__str__()的返回结果。

自定义__str__()方法:

 特殊成员还有很多,对于Python内置的类,其中带有双下划线的都是一些特殊的成员。例如,对于字符串这个类str:

 其中有一个__add__()成员,类似实现加的操作,我们可以在自己的类中进行定义:

 上例中obj1+obj2没法进行操作,如果在类中定义__add__()方法:

 增加__add__()方法,self是对象自身,other是+号后的对象,这里即是obj2,obj1+obj2,实际上就是obj1调用自己的__add__()方法,self就是obj1,other就是obj2,这样两个类就可以进行相加了。并且返回的是年龄的和。当然,也可以相加后返回一个对象,如:

 返回的是一个对象,是Foo类的对象。相应的还有减、乘、除等对应的特殊成员方法。

构造方法是对象创建后自动执行的方法,对应的,当对象销毁时,销毁前会自动执行的一个方法叫做析构方法__del__(),对象的销毁是由垃圾回收机制销毁的。

 __dict__特殊成员,将对象中封装的所有内容通过字典的形式返回

如果是类,将类中的成员以字典形式返回。 

类似列表的操作涉及的特殊成员:如下列表操作

p = [1,2,3,4,5]
t = p[2]          #按索引取列表中的某个元素
p[3] = 444     #对列表中的某个元素进行重新赋值
del p[1]         #删除列表中的某个元素

我们是否也可以定义一个类,通过索引获取某个元素。如定义一个Foo类,通过下标索引获取一个元素值。

 __getitem__(self,item)特殊成员方法,通过索引获取元素,就是调用类中的此方法

 同理,__setitem__(self,key,value)对应对相应索引元素重新赋值,__delitem__(self,key)对应删除相应索引元素。

 注意,这只是操作上的一种对应,具体方法内的逻辑,就是我们自己想实现的东西。

列表还有一个切片的操作,这个操作在python3.x,对应的也是上面的三个方法,不同的是这时我们就要分析item,根据不同的item进行操作。

 当为切片时,传入的item是slice类型,slice中有三个属性,start,stop和step,对应切片的三个参数,这里就是start = 1,stop=4,step=2。具体方法的逻辑怎么实现,也是根据实际需要写。

 __iter__(),可迭代对象必须具有的方法

 Foo不可迭代,所以不能使用for语句。增加__iter__()方法

 注意返回的是迭代器,如果不是,也将出错:

使用for语句,其实就是执行next()方法,而可迭代对象中是没有此方法的,如上面的Foo类,只有__iter__()方法,通过这个方法,生成的迭代器才有next()方法,最终for循环迭代的是这个迭代器。

即类中有__iter__()方法,类所生成的对象就是可迭代对象,可迭代对象执行__iter__()方法,必须返回一个迭代器,对迭代器执行next()方法或者调用迭代器的next()方法获取值。

三、metaclass,元类,类的祖先

python中一切事物都是对象,所以,类也是对象

我们从我们正常认知中的对象的生成开始研究,假设如下定义一个类和生成一个对象:

当Python加载到class Foo: pass 时(可理解为Python读你写的源代码文件,即.py文件),在内存中就生成了类Foo,也就是内存中开辟了一块内存,其中有Foo的各项成员项,如字段、方法等。然后执行Foo('aaa'),在内存中就又开辟一块内存,也就是生成一个新的对象,只是这个对象没有变量指向,所以,生成后,直接又被垃圾回收了,然后又生成一个对象obj,相当于在命名空间中增加一个变量obj,然后在内存在再开辟一块内存,这个obj指向这块内存,在内存中保存有指向其类,即Foo的指针,还有一些obj对象特有的字段等。

在这里,对象的生成,就是通过类名+括号,即Foo('参数'),其实,这个过程感觉又分为几步,第一步是在内存中开辟空间,这个空间是有特定结构的,这就是生成对象,这时就有内存块的地址了,第二步将这个地址赋给变量obj,第三步,调用类的__init__(self,arg)构造方法,将obj传给self参数,其他参数传给对应的参数,也就是执行初始化,因为self是obj对象,就是在obj对象,即开辟的内存中增加相应的字段。而对象名+括号,这里就是obj(),是执行类Foo中的__call__()方法。当对象不再使用时,也就是其被引用数为零时,被垃圾回收,回收前执行了__del__()方法。通着这个例子,看到被回收的是对象,是对象被回收执行类中的析构方法__del__()。类Foo被回收呢?这里没有显示。

对象的生成,obj = Foo('aaa'),就是一种语法结构,Python解释器加载到这种语句,就会执行相应的对象生成操作,首先是查找Foo类是否存在,存在,就开辟内存块,即生成对象(所以对象也就是一块有结构的内存空间),当然中间还有对对象空间的结构化及部分数据的初始化,然后将内存块地址赋值给对象变量obj,然后在执行构造方法。

在这里突然就感觉到obj = Foo('aaa')语法其实跟函数调用是完全一样的,如果存在一个函数Foo,会出现什么情况?测试一下:

左图类定义在上,函数定义在下,最终下面的Foo认定为函数,右边函数在上类在下,认定成类。

所以,函数名和类名在Python中也是一个变量名,谁在最后,谁的值为准,也就是前面的赋值被后面的覆盖了。

 看完了对象的生成,再看类的生成,前面只是说Python加载了类定义后,在内存中生成了类,到低是怎样生成的呢?

类的定义还有另外一种方法:

 

  Bar使用type也生成了一个类。type的参数,第一个是类的名字,就是类中__name__的值,第二个参数是这个类Bar继承的父类的元组,这里继承了object类,第三个参数类的成员,是一个字典格式,键值对中键是字段名或方法名,值是字段的值或是方法(函数)名或是lambda表达式。通过这里,可以推测:正常的类定义,就是class定义的类的加载,是不是就是读取类的定义后,分析分解后,将相关的部分作为type的参数,然后使用type生成类

一个与Foo一样的类:

 这里涉及到了type,type本身是python的一个内置类,所以当使用Bar = type()这种形式来生成类时,是不是有一种熟悉的感觉,对比一下对象的生成:obj = Foo(),那么,就可以说Bar也是一个对象。这就是前面说的:“ython中一切事物都是对象,所以,类也是对象。”

看一下生成的关系:obj------>Bar------>type,虚线箭头代表前面的是由后面的实现的,是一种实现关系,也就是箭头指向的是前面的类型,也叫类型关系,即obj是由类class实现的,obj是Bar类的对象,Bar又是由类type实现的,Bar是type类的对象,那么type类是由谁生成的呢?type类是python的原生类,它很特殊,type类是由它自身生成的。type即是类,又是对象。这是对象的实现。

涉及到的第二个特殊类是object,这是从类的继承上来考虑,我们知道python中类可以继承,如定义类Foo,在定义另一个类Foo2(Foo),可以继承Foo,那么定义的Foo,如:class Foo:是没有继承类吗?不是,python中类定义时没有写继承的类,则默认都是继承object类的,object是所有类的父类(又叫基类或超类),而object又继承谁呢?object是一个特殊类,它是python中类的祖先,它没有继承任何类,也是原生类,由C语言定义。

既然object是类,那它由谁生成?object类也是由type实现的,也就是说:type是实现关系中的祖宗,object是继承关系中的祖宗,type实现了object,type又继承自object

看下面的测试结果:

执行的结果:

 type类和object类是特殊的类,type类继承自object类,type又实现了object类,type类又实现了type,因为它们是Python的原始祖宗,不能以通常的类与对象的实现关系考虑,只要知道它们是相辅相成,同时存在就行了。我自己想到了一个类比,就好像中国古代神话中的盘古开天辟地,这两个类就相当于盘古和那个蛋,世界万物都是由他两产生,但是盘古和蛋怎么产生的不知道(但这两个类怎么产生的可以看python的C源代码应该能弄懂)。

网上找到的一些关于type和object的详解知识:

面向对象的体系中,存在两种关系:

● 父子关系(以实线描述):也就是继承关系,这种关系存在于某个类(subclass,子类)是另一个类(superclass,超类、父类、基类)的特别版本之中。通常描述为“子类是一种父类”。比如:蛇是一种爬行动物(Snake is a kind of reptile)。其中,蛇(snake)是子类,爬行动物(reptile)是父类。蛇拥有爬行动物的特征,同时,又拥有标志自己是一条蛇的特征。
● 类型实例关系(以虚线描述):也就是实现关系,这种关系存在于两个对象之中,其中一个对象(实例)是另一个对象(类型)的具体实现。我有一条宠物蛇叫Squasher,那么Squasher就是蛇的一个实例。英文描述为:"Squasher is an instance of snake".

 

python面向对象的体系中,两条很有用的规则:

●  Dashed Arrow Up Rule:If X is an instance of A, and A is a subclass of B, then X is an instance of B as well.翻译过来是“虚线向上规则”:如果X是A的实例,同时A又是B的子类,那么,X也是B的实例。;
●  Dashed Arrow Down Rule:If B is an instance of M, and A is a subclass of B, then A is an instance of M as well.翻译过来是“虚线向下规则”:如果B是M的实例,同时A是B的子类,那么,A也是M的实例。这条规则很少会用到,但对理解上面的内容很重要。

 虚线向上(2a方向)的分析:一个带箭头的蓝色虚线,从X端出发,射向A端,此时,A端为箭头端,虚线代表类型实例关系,也就是实现关系,所以A端是类型,即XA的实例(换句话说,AX的类型),通过命令X.__class__我们可查看X的类型。再看,一条带箭头的蓝色实线从A端射向B端,B端是箭头端,实线代表父子关系,即继承关系,所以B端是父类,即AB的子类这时候,我们通过将X端射向A端的虚线,向上抬,即沿2a方法上抬,射向B端(上图右上方有一条标志为implied[这个单词意思是隐藏]的向上淡红色虚线),就实现了表述X也是B的实例的目的。也就是XA的实例(对象),同时X也是B的实例(对象),虚线向上嘛

使用我们上面的Foo和Bar、obj的例子: 

 

 可以看到,obj既是Bar的实例(对象),也是Foo的实例(对象)。

虚线向下(2b方向)的分析:一条带箭头的蓝色实线从A端射向B端,B端是箭头端,实线代表父子关系,即继承关系,所以B端是父类,即AB的子类,一个带箭头的蓝色虚线,从B端出发,射向M端,此时,M端为箭头端,虚线代表类型实例关系,也就是实现关系,所以M端是类型,即BM的实例(换句话说,B是M类型),通过命令B.__class__我们可查看B的类型。这时候,我们通过将B端射向M端的虚线末端,向下压,即沿2b方法下压,变成A端射向M(上图左下方有一条标志为implied[这个单词意思是隐藏]的向上淡红色虚线),就实现了表述A也是M的实例的目的。也就是B是M的实例(对象),同时A也是M的实例(对象),虚线向下嘛

 

 最后网上找的一个关系图:

 上面的图示的说明:type类继承object类,并通过自我实现,实现了type类(生成一个实例或说对象),object类又通过type类实现了object类(生成一个实例或说对象),这就实现了开天辟地,然后通过这两个类,可以生成和实现其他的类和对象。

上面的图好像有点问题,在我测试的python3.x版本中,使用的都是<class 'object'>,没有<type ‘object'>这种结果。

所有的类型都是类,类是类的类(即元类)的实现,算是一种特殊的实例(对象),但是叫做类,即class,即由class 关键字定义或type()实现的叫做类,其他的由普通类实现的,即类名+括号:类名()实现的叫做对象(实例)。

 这里出现了元类的概念,就是类的类,类的祖宗,在这里就可以看成type就是元类。从上图可看出,所有的其他类及实例(对象)都是依赖type来生成。

下面就研究一下元类:

因为type是python的内置类,对它不好进行一些跟踪测试,我们可以使用继承的办法,生成一个继承type的类,这个类也就成了一个元类(虚线向下的应用),然后研究一下元类的特性。

先从普通类与对象的实现上做对比:

最常用的一种形式:
class Foo:
     pass
obj = Foo()
obj()

主要研究类与对象的加载以及类中方法执行的顺序。

首先,解决一个以前的认识误区,以前了解的是只要看到类名+括号,就是执行类的构造方法,这个理解不对,二是一个疑惑,对于obj = Foo(),最终是要执行构造方法,在讲到构造方法时,常说self参数就是obj,是python解释器将obj自动加载到Foo类中,现在理顺一下。

先说第二个,就是Foo先生成对象,赋值给obj,然后obj调用了构造方法,还是Foo生成对象,执行完构造方法,再将构造好的对象赋值给obj,因为前面看到的都是说谁调用,self就是谁,所以讲到构造方法时,说self就是obj时,自然想到是obj调用了构造方法。看下面例子

 去掉obj =,相当于没有给obj赋值,按照理解,是不是就不执行构造方法呢?看到结果,显然是不是这样的,也就是说Foo()首先是生成对象,然后直接又调用了构造方法__init__(self),这时的self就是生成的对象,并没有赋值给obj,所谓对象,也就是一块有结构的内存,而代表对象的就是这块内存的首地址。所以obj = Foo()是先生成最原始的一个对象,然后调用构造方法,进行对象的加工,初始化,这时传给构造方法的self就是这个内存块首地址,当然如果有其他参数一并传递,做好这些后,将一个完整的对象交给obj。所以这一句的理解可以先这个理解:生成obj变量,变量值为None,然后执行Foo(),构造一个对象(这里说的构造包括生成对象和调用构造方法初始化),然后将对象的地址赋给obj。

现在再看Foo()的执行,既然说到了生成一个新对象,那就不会是一开始就执行__init__()构造方法,肯定是一个对象生成的方法,这个方法就是__new__()方法。如果了解过java,就会知道java中生成一个新对象就是使用new方法,new一个新对象。

 看运行结果,先执行new,然后执行init,这时对象生成完毕,但是因为没有任何对象引用,又会直接被垃圾回收,所以执行了del。

在增加一个括号Foo()()

 至此,执行的顺序就是先执行__new__()------->__init__()-------->__call__()------->__del__(),call属于后期的对象的调用,可以不执行这一步。

执行的流程示意图:

第一步:python加载到class Foo时,类Foo加载到内存
第二步:加载obj = ,生成变量obj,值应为为None(个人猜测)
第三步:加载Foo(),执行Foo类的__new__()方法,生成原初对象
第四步:执行Foo类的__init__()方法,将生成的原初对象地址传给self,其他参数对应传递(这里是空),进行原初对象的构造。
第五步:将构造完成的对象赋值给变量obj
第六步:加载obj(),即主动执行Foo类的__call__()方法,这时的self传递的应该是obj。
第七步:执行完毕后,对象不再被引用,被垃圾回收,回收前,调用__del__()方法,此时self也是将obj传进去。

下面再研究类的加载过程,研究元类与类的关系:

最终的元类就是type,一个类的定义与生成可以看成下面的类比:

看看与对象的生成:

 非常类似,只是用type代替了Foo,用Foo代替了obj。具体实现一个元类来看一下执行的过程:

 先看加载到Class定义结束时的情况。

接着加上对象的生成过程:obj = Foo()以及obj(),最后看一下obj的类型

 最终的流程:

 第一步:python加载到元类class Mytype时,类Mytype加载到内存
第二步:Python加载类Foo,读取并分析
第三步:生成Foo类,执行元类Mytype的的__new__()方法,生成原初类。
第四步:执行Mytype类的__init__()方法,将生成的原初类地址传给self,其他参数对应传递(这里是空),进行原初类的构造,这里就生成了完整的Foo类。
第五步:开始执行Foo(),这里先要以Mytype类的对象的身份,即是对象+括号的身份,那么就是执行Mytype的call方法。
第六步:执行完Mytype的call方法后,再以类名+括号的身份,即类名+括号的身份,执行Foo类的new方法。它的实质是上一步的call方法主动调用Foo类的new方法。new方法会生成Foo类的原初对象。
第七步:执行完Foo的new方法,接着要执行Foo的init,对上一步生成的原初对象进行构造。(但是在结果中没发现这一步,对Foo的new进行了一下修改,主动调用一下init方法

第八步:将构造完毕的Foo对象赋值给obj,此时,obj对象生成完毕。
第九步:以对象+括号的身份,执行Foo类的call方法。

通过其后的三个type,可以看出,obj的类型是Foo,Foo类型是Mytype,Mytype的类型是type。

 所以一个普通的类的对象的方法调用过程是:

元类的new--->元类的init--->元类的call--->类的new(实质是元类的call主动调用)--->类的init--->类的call(需要程序主动调用)

 

以上是关于Python入门自学进阶——6--类与对象-成员修饰符特殊成员及元类的主要内容,如果未能解决你的问题,请参考以下文章

Python入门自学进阶——7--类与对象-异常反射单例模式

Python入门自学进阶-Web框架——4HttpRequest和HttpResponse及模板

Python入门自学进阶——4--序列化和结构化数据

Python入门自学进阶-Web框架——20Django其他相关知识2

Python入门自学进阶-Web框架——5Django的Model,即ORM对象关系映射

Python入门自学进阶-Web框架——20Django其他相关知识2