摘要:实例变量与类变量事实上,字段除了独属于实例之外,跟普通变量没有什么差别,所以实例的字段也被称为实例变量。在类的定义中,与实例变量对应的还有类变量,类变量与实例变量类似,通过操作符来访问。类变量跟类的方法都可以被称为类的成员。
该系列文章:
《python入门,编程基础概念介绍(变量,条件,函数,循环)》
《python中的数据类型(list,tuple,dict,set,None)》
《在python中创建对象(object)》
在上一篇文章《python中的数据类型(list,tuple,dict,set,None)》的1.2小节里我们就简要介绍过对象(object)跟类(class)的概念。也知道了python中内置的所有数据类型都是对象,拥有自己的方法。那么当这些内置的数据类型无法满足我们的需求时,我们如何创建我们自己的类型(type)呢?答案就是通过创建我们自己的类(class)。通过我们自己动手实现的类,我们就可以创建以这个类为模板的对象。从这样的流程来看,面向对象的编程方式是自顶而下,首先需要全盘考虑,才能创建一个足够好的模板,也即类。然后才能将类实例化为对象,通过对象中的属性来解决问题或者与其他对象互动。
创建一个最简单的类可以通过下面这样的写法:
class User: pass #更多代码 #更多代码
上面的代码中class是关键字,表明我们要创建一个类了,User是我们要创建的类的名称。通过“:”和缩进来表明所有缩进的代码将会是这个类里的内容。从User类中创建一个该类的实例通过下面的写法:
""" 创建一个实例,通过类名加括号的形式,类似调用函数 """ u=User()
对象(客体)有自己的特征和自己可以做到的事,对应到程序里就是字段(field) 和方法(method) ,这两个都是对象的属性(attribute) 。对象的字段类似于普通变量,所不同的是对象的字段是对象独有的。对象的方法类似于普通函数,所不同的是对象的方法是对象独有的。上篇文章中我们已经见到过如何使用字段跟方法,那就是通过.操作符。
1.0.定义方法(method)在类中定义对象的方法(method)比较简单,跟实现普通函数类似,只有一点不同,那就是不管方法需不需要参数,你都需要把self作为一个参数名传进去,self这个参数在我们调用方法时我们可以直接忽略,不赋值给它。举个例子:
class User: def hi(self): print("hi!") u=User() u.hi() """ 程序输出: hi! """
self这个参数名是约定俗成的。在User类的代码块里定义hi方法时,传入的参数self将会是某个实例(对象)本身。当u作为User类的实例被创建,并且通过u.hi()调用hi方法时,python解释器会自动将其转换成User.hi(u)。通过传入实例(对象)本身,也即self,方法(method)就能够访问实例的字段(filed),并对其进行操作,我们之后可以从新的例子中看到。
1.1.声明字段(field)要在类中声明对象的字段,有一个特殊的方法(method)可以做到,那就是__init__方法,这个方法在init前后都要写上两个下划线__。__init__方法会在实例一开始创建的时候就被调用,init是initialization的缩写,顾名思义,就是初始化的意思。__init__方法在创建对象的时候由python解释器自动调用,不需要我们手动来调用。看个例子:
class User: """ 注意self总是在括号里的最左边 """ def __init__(self,name,age): self.name=name self.age=age def hi(self): print("hi!I"m {}".format(self.name)) u=User("li",32) u.hi() print(u.name+","+str(u.age)) """ 程序输出: hi!I"m li li,32 """
上面的代码里,在User类的__init__方法中self被传入,那么就可以通过self.name跟self.age来声明对象的两个字段,并将传入该方法的参数name跟age赋值给它们。当我们创建类型为User的对象的时候,传入实际的参数"li"跟32,这两个参数被python解释器传入__init__方法中,"li"对应name,32对应age,__init__方法立即被调用,将实例u的字段一一建立。
self.name=name粗看貌似都是name变量,但self.name是实例的字段,专属于实例,name是创建对象时将要传入的一个参数,将它赋值给self.name是没有歧义的。
1.2.实例变量与类变量事实上,字段除了独属于实例之外,跟普通变量没有什么差别,所以实例的字段也被称为实例变量。在类的定义中,与实例变量对应的还有类变量,类变量与实例变量类似,通过.操作符来访问。类变量是任何实例共享的,可以理解为是该类型所共有的特征,比如,在User类中,我们可以计算一共有多少被实例化了的用户:
class User: """ 计算被实例化的用户数量 """ count=0 def __init__(self,name,age): """每次创建一个对象,用户数量在原有基础上加1""" User.count=User.count+1 self.name=name self.age=age def hi(self): print("hi!I"m {}".format(self.name)) def die(self): print("I"m {}, dying...".format(self.name)) User.count=User.count-1 del self @classmethod def print_count(cls): print("共有{}名用户".format(cls.count)) u=User("li",32) User.print_count() u.hi() u1=User("ma",30) u1.__class__.print_count() u1.hi() u.die() User.print_count() u1.die() User.print_count() """ 程序输出: 共有1名用户 hi!I"m li 共有2名用户 hi!I"m ma I"m li, dying... 共有1名用户 I"m ma, dying... 共有0名用户 """
上面代码中的count就是类变量,可以通过User.count来访问。python通过@classmethod来表明它下面定义的方法是类的方法(method),类的方法中的cls是类本身,跟self使用方法类似,调用类方法时可以直接忽略。类变量跟类的方法(method)都可以被称为类的成员。除了使用类似User.count这样的方式来访问和使用之外,该类的实例还可以通过__class__属性来访问和使用类成员,比如上面代码中的u1.__class__.print_count()。
上面代码中定义的字段跟方法都是公开的,可以通过.操作符访问。但如果属性是以形如__name这样以双下划线为开头的名称,则python会自动将名称换成_classname__name,其中classname就是类的名称,这样,通过an_object.__name是访问不到的。
1.3.继承(inherit)现实世界里,某一类相似客体的类型被抽象为一个概念(名称),同时又有另一类相似客体的类型被抽象为一个概念(名称),这时候我们可能会发现,这两个概念(名称)之间很相似,于是我们把这两个相似概念再抽象成一个概念(名称),这样的过程可以重复多次。举个例子,我们有幼儿园,同时有小学,这时候我们可以把幼儿园跟小学抽象成学校。那跟现实类似,对象的类型跟类型之间,可以抽象成另一个类型。在文章最开头我们说面向对象编程是自顶而下,这跟一层层向上抽象的过程正好相反,我们会在一开始思考如何创建某个类,然后把这个类作为基类(父类) ,更具体的创建一(几)个新类,这一(几)个新类不仅拥有基类的属性,还会增加自己独有的属性。我们把这样的新类(class)叫做子类,是从基类继承而来的。
从1.2小节的代码例子中,我们创建了一个User(用户)类,假如我们现在需要区分免费用户和付费用户,免费用户跟付费用户都具有name、age、hi属性,同时免费用户跟付费用户还具有自己独有的一些属性。如果我们直接分别创建免费用户类跟付费用户类,那么这两个类之间共同的属性就会被定义两次,需要复制粘贴同样的代码,这样的代码质量不高并且不够简洁。相反,我们可以让免费用户跟付费用户都从User类继承下来,这样就可以重用(reuse) User类的代码,并且让代码相对简洁高效一点。还有一个好处就是,当免费用户跟付费用户之间共同的属性有了变化(增减),我们可以直接修改父类User,而不用分别修改,当子类的数量很多时,这种好处会显得非常明显。修改父类之后,各子类的独有属性不会受到影响。
1.2小节的代码例子中,我们对实例的数量进行统计,当子类从User类继承下来后,在子类实例化对象的时候,父类的实例数量在python中默认也会增多,这说明我们可以把子类的实例看做是父类的实例,这被称为多态性(polymorphism)。免费用户类跟付费用户类如何从父类继承,如下:
class User: """ 计算被实例化的用户数量 """ count=0 def __init__(self,name,age): """每次创建一个对象,用户数量在原有基础上加1""" User.count=User.count+1 self.name=name self.age=age def hi(self): print("hi!I"m {}".format(self.name)) def die(self): print("I"m {}, dying...".format(self.name)) User.count=User.count-1 del self @classmethod def print_count(cls): print("共有{}名用户".format(cls.count)) class Free_user(User): def __init__(self,name,age,number_of_ads): User.__init__(self,name,age) self.number_of_ads=number_of_ads def hi(self): User.hi(self) print("I"m free_user") class Paying_user(User): def __init__(self,name,age,plan): User.__init__(self,name,age) self.plan=plan def hi(self): print("hi!I"m {},paying_user".format(self.name,)) u=Free_user("li",32,5) User.print_count() u.hi() u1=Paying_user("ma",30,"5$") User.print_count() u1.hi() u.die() User.print_count() u1.die() User.print_count() """ 程序输出: 共有1名用户 hi!I"m li I"m free_user 共有2名用户 hi!I"m ma,paying_user I"m li, dying... 共有1名用户 I"m ma, dying... 共有0名用户 """
上面代码中首先创建了User基类,然后从User类继承下来两个子类:Free_user跟Paying_user。子类要从某个类继承而来,需要在类名后面跟上括号,在括号中填入基类的名称,形如这样:Free_user(User)。
在子类的__init__方法中,通过调用基类的__init__方法把继承自基类的共有字段创建出来,调用的时候将self跟传入的属于基类部分的参数原样传入,上面代码中将传入的name跟age原样传入了基类的__init__方法中了。因为我们在子类中定义了__init__方法,所以python不会自动调用基类的__init__方法,而需要我们显式地调用它。如果子类中没有定义__init__方法,则python会在创建子类的实例时自动调用基类的__init__方法。这是为什么呢?我们在子类中对基类的hi方法进行了重写,但对die方法则没有,但是我们通过子类创建的实例能够调用die方法,这说明在调用die方法时子类的实例被看做了父类的实例了,同时在调用hi方法时却都调用了重写的子类中的方法了,这说明,当实例调用一个方法时,会首先在实例化自己的类中寻找对该方法的定义,如果没有,则在该类的父类中寻找,如果父类中还是没有,则会在父类的父类中寻找,这样的过程一直重复,直到再也没有父类了,如果还是没有该方法,那程序就会报错。
最后,从前一小节我们知道,以双下划线为前缀的属性名会被python自动替换成另一个名称,这时候当父类中有个以双下划线为前缀的属性,我们在子类中也有一个相同的属性的时候,由于python自动替换了这两个属性名称,子类中的方法并没有覆盖掉父类中的属性,而只是,子类中的该属性跟父类中的该属性是两个不同的属性。看例子:
#其他一些属性用“,”省略了 >>> class a: ... def __a_method(self): ... pass ... >>> dir(a()) ["__class__", ,,, "_a__a_method"] >>> class b(a): ... def __a_method(self): ... pass ... >>> dir(b()) ["__class__", ,,, "_a__a_method", "_b__a_method"] >>>
可以看到,__a_method在a类的实例中被替换成了_a__a_method,在b类的实例中被替换成了_b__a_method,这跟_a_a_method不同,b类的实例中继承了_a_a_method,同时,还有自己类型的_b_a_method。
1.4.类(class),对象(object)与类型(type)之间的关系看个例子:
>>> list.__base__>>> type(list) >>> class User: ... pass ... >>> User.__base__ >>> type(list)
类的属性__base__可以指明该类是继承自哪个类。从上面的例子可以看到,python中定义内置数据类型的类(class)(以list为例)从object类继承而来,但是其类型(type)是type类,我们自己创建并且没有明确指定从是哪个类继承而来的类(以User为例),跟内置类型一样,从object类继承而来,其类型(type)是type类。如果我们自己创建的,并且从层次更低的类(比如User类)中继承而来,那么该类的__base__(基类)跟type函数所显示的类型是怎么样的呢,接上面的例子:
>>> Free_user.__base__>>> type(Free_user) >>>
可以看到,Free_user从User继承而来,但其类型依然是type,所以所有的类(class)的类型应当都是type,是不是这样呢,object会不会例外呢?type自己呢?它的类型呢?下面的例子验证了它们的类型也是type:
>>> type(object)>>> type(type)
从例子中我们知道了,所有的类(class)的类型都是type类,所以,所有的类(class)都是从type类中实例化出来的实例,甚至type类实例化了自己,所以所有的类包括object和type都是实例,这说明所有的类都是对象 。为了清晰的指代不同种类的对象,我们把本身是类的对象称为类对象,把从类中实例化而来并且本身不是类的对象成为实例对象,实例对象是层次最低的对象。
我们可以看到object类跟type类是python中抽象层次最高的对象了。那么它们两个的关系如何呢?看例子:
>>> type.__base__>>> print(object.__base__) None >>>
可以看到,type从object继承而来,并且我们已经知道object的类型是type。从例子中可以看到object没有基类,所以如果把类的层层继承想象成一条锁链,那么object将是继承链上的顶点。前面我们提到的对象的一些特殊属性如__init__、__class__继承自object,而__base__这个特殊属性则只有类对象才有,是在type中定义的。如下:
#其他一些属性用“,”省略了 >>> dir(object) ["__class__", ,,, "__init__", ,,, "__subclasshook__"] >>> dir(type) ["__abstractmethods__", "__base__", "__bases__", ,,, "mro"] >>>
[欢迎浏览我的个人博客,https://diwugebingren.github.io
](https://diwugebingren.github....
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/44058.html
摘要:如果还是没有找到,就会使用父类中的元类来创建类。元类通常用于处理比较复杂的情况。这是因为使用了元类,它会将中定义的字段转换成数据库中的字段。中所有数据类型都是对象,它们要么是类的实例要么是元类的实例。 原文地址:what is metaclass in Python?我的简书地址::nummy 类即对象 在理解元类之前,需要先掌握Python中的类,Python中类的概念与SmallT...
摘要:可以使用标准的索引切片迭代操作访问它,其中每项操作均锁进程同步,对于字节字符串,还具有属性,可以把整个数组当做一个字符串进行访问。当所编写的程序必须一次性操作大量的数组项时,如果同时使用这种数据类型和用于同步的单独大的锁,性能将极大提升。 上一篇文章:Python进程专题5:进程间通信下一篇文章:Python进程专题7:托管对象 我们现在知道,进程之间彼此是孤立的,唯一通信的方式是队...
摘要:原链接中的元类是什么类也是对象在理解元类之前,需要掌握中类概念。事实上,是中用于创建所有类的元类。类本身是元类的对象在中,除了,一切皆对象,一切都是类或者元类的对象。事实上是自己的元类, 学习契机 项目中使用Elasticsearch(ES)存储海量业务数据,基于ES向外提供的API进一层封装,按需处理原始数据提供更精确、更多样化的结果。在研究这一层的代码时接触到@six.add_me...
摘要:对于中的列表,情况并非如此。现在我们将继续讨论如何在列表中添加新元素以及更多内容。可变性意味着改变其行为的能力。该位置是元素需要保留在列表中的位置。默认值是列表的最大允许索引,即列表的长度。用于给出列表的长度,即列表中存在的元素的数量。 showImg(https://segmentfault.com/img/remote/1460000019111677); 来源 | 愿码(Cha...
摘要:说起这个函数就需要先了解的变量存储机制了变量是动态变量,不用提前声明类型。当我们写时,解释器干了两件事情在内存中创建了一个的字符串在内存中创建了一个名为的变量,并把它指向。 id(object) Return the identity of an object. This is an integer (or long integer) which is guaranteed to be...
阅读 2291·2023-04-25 20:07
阅读 3262·2021-11-25 09:43
阅读 3634·2021-11-16 11:44
阅读 2500·2021-11-08 13:14
阅读 3152·2021-10-19 11:46
阅读 868·2021-09-28 09:36
阅读 2903·2021-09-22 10:56
阅读 2331·2021-09-10 10:51