Python3学习笔记——创建自定义对象
学习目录
1.前言——面向对象编程
这些照搬来的概念我个人有点难理解,就尝试先接受再理解。
面向对象是以“对象”为中心思想,将要解决的问题分解为各个对象,对象拥有自己的属性和行为。Python设计之初就是一门面向对象的语言,Python的所有东西都是对象。创建对象类型是Python的核心概念,创建对象类型要学会:如何创建对象,多态,封装,方法,属性,超类和继承。
由此创造的东西是抽象的,而抽象是程序能被人理解的关键所在。计算机喜欢具体的命令,而人通常不是这样。好比有人问路,你会回答”向前走到路口后右转,然后直走从天桥过马路“,但是你不会回答”向前走100步,然后逆时针转身90°,接着直走150步,走上25层阶梯后顺时针转身90°,直走50步后顺时针转身90°,接着走下25层阶梯……“。而抽象就能做到将具体的命令变为抽象的命令,你无需知道中间是如何运作的,也不需要关心。而这些具体的细节,将在其他部分给出。
2.对象魔法
面向对象中,对象大致意味着一系列属性和方法,之所以用对象而不用全局变量或函数,有如下原因:
-
多态:对不同类型的对象执行相同操作,这些操作“就像被施了魔法”一样可以正常运行。
-
封装:对外部隐藏有关对象工作原理的细节。
-
继承:可以从通用类创建出专用类。
2.1.多态(Polymorphism)
简单理解:无需知道变量指向哪种类型,也能够对其执行操作,且操作行为随对象所属类型而异。
如果我收到一个对象,我不知道对象是如何实现的——它可能是众多“形态“中的任意一种,那我只要知道我可以利用这个对象获得我想要的结果即可。
与对象属性相关联的函数成为方法。
多态形式多种多样,比如"+"号可以连接字符串,也可以为数字做加法等,其参数可以是任何支持加法的对象。
很多函数和运算符都是多态的,由此编写的程序也可能如此,即使我不是有意为之。
2.2.封装
封装指的是向外部隐藏不必要的细节。这有点多态,即无需知道内部的细节即可使用。它们都是抽象的原则,让你无需关心不必要的细节。
简单来说,我希望每个对象拥有自己的状态,而不是每个人公用一颗心脏。
2.3.继承
如果我已经创建了一个类,而我需要写一个与之类似的类,这个新类可能只是新增了几个方法,那我只需要继承以前写的类,并附上新增的方法即可。而不需要把代码CTRL+C/V上去。
3.类
3.1.类是什么
每一种都对象属于特定类,是类的实例。
注意:英语交流中用复数形式表示类,Python中约定用单数首字母大写表示。
3.2.创建类
>>> class Person:
def set_name(self,name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("Hello,I am {}.".format(self.name))
class 关键字用于创建一个类,class之后为类的名称并以冒号结尾
>>> class ClassName:
'''类的帮助信息'''' # 类文档字符串
class_suite # 类体
帮助信息可以通过ClassName.__doc__查看
3.3.参数self
参数self代表类的示例,而非类,它指向对象本身。
类的方法与一般的函数只有一个特别的区别:它们必须有一个额外的第一个参数名称。
3.4.隐藏对象
要让方法或属性成为私有的(不能从外部访问),只需要在其名称前面以两个下划线打头即可。
>>> class ClassName:
def __Secret(self):
print("Bet you can't see me")
现在不能从外部访问__Secret,但是从内部(ClassName类)可以。
在类定义中,对所有以两个下划线打头的名称都进行转换,即在开头加上一个下划线和一个类名。知道这个以后还是可以从外部访问:
>>> s=ClassName()
>>> s._ClassName__Secret(self)
Bet you can't see me
所以理解为双下划线意味着通过隐形修改名字,虽然无法阻止别人访问私有方法和属性,但是这种方法强烈警告别人不要这么做。如果你既不希望别人访问又不希望修改名字,那么可以用一个下划线打头。虽然这只是约定,但是也有一些作用。比如,from module import *就不会导入以单下划线打头的名称。
3.5.类的命名空间
首先了解类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。比如:
>>> class ClassName:
number=0
def init(self)
ClassName.number+=1
>>> m1=ClassName()
>>> m1.init()
>>> ClassName.number
1
>>> m2=ClassName()
>>> m2.init()
>>> ClassName.number
2
可见,number的值不受实例化是m1或m2.
但是如果给一个实例中的number赋值,那新值就会写入实例中,这个属性遮住了类级的变量。
>>> m1.number='Two'
>>> m1.number
'Two'
>>> m2.nember
2
3.6.指定超类与继承
继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。
子类扩展了超类的定义。指定超类,可在class语句中的类名后加上超类名,并用圆括号括起。
>>> class Person:
def set_name(self,name):
self.name = name
def get_name(self):
return self.name
def greet(self):
print("How are you?")
>>> class Human(Person):
def greet(self):
print("Hello,I am a human")
Human类直接继承了Person类的set_name和get_name方法,因此无需重新定义。同时Human类以提供新定义的方式重写了方法greet的定义。
确定一个类是否属于另一个类,可以用内置方法issubclass
>>> issubclass(Human,Person)
True
>>> issubclass(Person,Human)
False
同样也可以适用于判断对象是否是特定类的实例,并且可以判断直接实例和简介实例。
>>> s = Human()
>>> issubclass(s,Human)
True
>>> issubclass(s,Person)
True
利用属性__class__可以得到对象属于哪个类。
>>> s.__class__
<class '__main__.Human'>
如果有多个超类,称为多重继承,但是这会带来很多”并发症“,所以除非万不得已不要使用。使用时务必要注意,如果多个超类以不同的方式实现了同一个方法(即不同超类有同名方法),需要在class语句中谨慎排列这些超类。位于前面的类的方法会覆盖位于后面的类的方法。当出现多个超类的超类相同时,查找特定方法或属性时访问超类的顺序称为方法解析顺序(MRO),它的算法非常复杂,但是效果非常好,知道即可。
>>> class Mankind:
def greet(self):
print("Hello,I am a Mankind")
>>> class Man(Person,Mankind):
pass
>>> sk=Man()
>>> sk.greet()
How are you?
>>> class Lady(Mankind,person):
>>> sl=Lady()
>>> sl.greet()
Hello,I am a Mankind
可见,超类的排序会影响其中的方法调用。
3.7.接口与内省
处理多态对象时,只需要关心接口(协议)——对外暴露的方法和属性。Python不显式地指定对象必须包含哪些方法才能用作参数,而是假定对象能够完成你要求它完成的任务。如果完不成,程序通常失败。通过检查所需方法是否存在,可以实现要求对象遵循特定的接口。
>>> hasattr(s,'get_name')
True
同时还可以检查方法是否可以调用
>>> callable(getattr(s,'get_name',None))
True
方法getattr用于检查属性是否存在,并且可以指定属性不存在时使用默认值,这里指定为’None’。
同时还要介绍函数setattr,用于设置对象属性:
>>> setattr(s,'name','Emore')
>>> s.name
'Emore'
查看对象中储存的所有值,可以检查其__dict__属性。
>>> s.__dict__
{'name': 'Emore'}
3.8.抽象基类
Python通过引入模块abc为所谓的抽象基类提供支持。一般而言,抽象类是不能(至少不应该)实例化的类,其职责是定义子类应实现的一组抽象方法。
from ab import ABC,adstractmethod
class Talker(ABC):
@adstractmethod
def talk(self):
pass
形如@this的是一种修饰器,@adstractmethod用来将方法标记为抽象的,必须在子类实现的方法。
抽象类不可以实例化,这是抽象类(包含抽象方法的类)最显著的特征。
>>> Talker()
Traceback (most recent call last):
File "<pyshell#71>", line 1, in <module>
Talker()
TypeError: Can't instantiate abstract class Talker with abstract method talk
如果派生出一个超类,但是不重写方法Talk,那这个子类也是抽象的,不能实例化。
这是用isinstance检查给定的实例确实是Talker对象,就知道一定要重写方法Talk。
另外isinstance的妙用。
>>> class Herry:
def talk(self):
print('ding')
>>> h=Herry()
>>> isinstance(h,Talker)
False
这时如果把Herry定义为Talker的子类,可能万事大吉,但是如果做不到,就需要将Herry注册为Talker,所有的Herry对象都被视为Talker对象,这样区别于派生子类,而理解为”挂名“。但是这么挂名,有一个缺点直接从抽象派生提供的保障没有了。
>>> class Clam:
pass
>>> Talker.register(Clam)
<class '__main__.Clam'>
>>> issubclass(Clam,Talker)
True
>>> c=Clam()
>>> isinstance(c,Talker)
True
>>> c.talk()
Traceback (most recent call last):
File "<pyshell#80>", line 1, in <module>
c.talk()
AttributeError: 'Clam' object has no attribute 'talk'
报错提示是没有指定的方法talk。
所以这是一种挂名的方式,书本称之为”意图“。
4.小结
要慎用继承,尤其是多重继承,会造成难以排除的bug。
不要让对象过于亲密,方法只关心其所属的实例的属性,别的实例的状态让他们自己管理。
方法要简短可读,篇幅尽量在一屏以内。
水平有限,仅供参考。如有错误,劳请指出。