Python学习6——再谈抽象(面对对象编程)
1、对象魔法
在面对对象编程中,术语对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。
使用对象而非全局变量以及函数的原因有多个,而最重要的好处不过以下几点:
多态:可对不同类型的对象执行相同的操作,而这些操作全部能够正常运行。
封装:对外部隐藏有关对象工作的具体细节。
继承:可基于通用类创建专用类。
1.1多态
术语多态源于希腊语,意思是有多种形态,这大致意味着即使你不知道变量指向的是哪个对象,也能够对其执行操作,且操作的行为随着对象所属的类型(类)而异。
1.2多态和方法
>>> \'abc\'.count(\'a\') 1 >>> [1,2,3,\'a\',\'a\'].count(\'a\') 2
下面我们做一个实验:
模块random中有一个函数random,它从序列中随机选择一个元素。
>>> from random import choice >>> x=choice([\'Hello\',\'Python\',[1,2,3,4,\'e\',\'e\',]]) >>> x.count(\'e\') 1
执行这些代码后,你不知道x到底包含什么,你只关心x包含了多少个\’e\’。从结果看,x应该包含的是 \’Hello\’ 。但关键是你无需执行相关的检查,只要x有一个名为count的方法,他将单个字符作为参数并返回一个整数就行了。如果有人创建了包含这个方法的对象,你也可以像使用字符串一样使用这个对象。
好吧!上面这句话并不太好理解!!!!
多态形式多样:
多态不仅仅适用于方法,我们还通过内置运算符和函数大量使用多态。
>>> def add(x,y): return x+y >>> add(1,2) 3 >>> add(\'jiamemg \',\'is so cool!\') \'jiamemg is so cool!\' >>> 1+2 3 >>> \'jiameng \'+\'is so cool!\' \'jiameng is so cool!\' #加法运算符不仅可以用于数,也可以用于字符串
>>> def length_message(x): print(\'the length of\',repr(x),\'is\',len(x)) #如你所见,这里使用了repr函数,repr函数是多态的集大成者之一,可用于任何对象 >>> length_message(\'jiameng\') the length of \'jiameng\' is 7 >>> length_message([1,2,3,4,5,6,7,8,9]) the length of [1, 2, 3, 4, 5, 6, 7, 8, 9] is 9 #如你所见,这个函数也是支持多态的,虽然你编写的时候可能不没有意识到这一点
很多函数和运算符都是多态的,你编写的大部分函数也可能如此,即使你不是有意为之。每当你使用多态的函数和运算符时,多态都将发挥作用。事实上,要想破坏多态,除非使用诸如type、issubclass等函数显式地执行类型检查,但你应该尽可能避免这样破坏多态。本章后面将会学到抽象基类和模块abc后,函数issubclass本身也是多态的。
1.3封装
封装指的是向外部隐藏不必要的细节。这听起来有点像多态。这两个概念很像,因为它们都是抽象的原则。
但是封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它。下面看一下一个使用多态而没有使用封装的示例。假装你有一个名为OpenObject 的类
1.4继承
继承是另一种(偷懒)的方式。因为已经学过JAVA有着相关基础,此处简介。
你已经有了一个类,现在你要创建一个新类,这两个类功能很相似,甚至需要部分的相同代码,你总不至于去copy代码,你只需要继承即可。你只需要让新类去继承老类方法即可,当你用新类对象去调用这个继承来的方法时,将自动调用老类的这个方法。
具体如何继承,将在后续实例中展示。
2、类
2.1什么是类
这一章节总是在提到类,并将其用作类型的同义词,那么到底什么是类哪?从很多方面来说,这正是类的定义——一种对象。每个对象都属于特定的类,并被称为这个类的实例。
举个例子:鸟类、云雀。云雀是鸟类的子类,鸟类是云雀的超类。
在面向对象编程中,子类关系意味深长,因为类是由其支持的方法定义的。类的所有实例都有该类的所有的方法,因此子类的所有实例都能实现超类的所有方法。因此,要定义子类,只需要定义多出来的方法(或者重写的一些方法)即可。
2.2创建自定义类
终于,终于要自定义类了!!!!!太高兴了!!!!
>>> class person: def set_name(self ,name ): self.name=name; def get_name(self): return self.name def grett(self): print("Hello,Python!I\'m {}.".format(self.name)) #关键来了,关键来了,虽然这个示例很简单,但是它说清了self是什么! >>> foo=person() >>> bar=person() >>> foo.set_name(\'jiameng\') >>> bar.set_name(\'wangweili\') >>> foo.grett() Hello,Python!I\'m jiameng. >>> bar.grett() Hello,Python!I\'m wangweili. #对foo调用set_name和grett时,foo都会作为第一个参数自动传递给它们。我们将这个参数命名为self。 #显然self很有用,如果没有它,所有的方法都无法访问对象本身——要操作的属性所属的对象 #与以前一样,也可以从外部访问他们 >>> foo.name \'jiameng\' >>> bar.name \'wangweili\'
2.3属性、函数和方法
实际上,方法和函数的区别体现在上边提到的参数 self 上,方法(更准确地说是关联的方法)将其第一个参数关联到它所属的实例,因此无需提供这个参数。
无疑可以将属性关联到一个普通函数,但这样就没有什么特殊的self参数了。
>>> class Class: def method(self): print(\'I have a self\') >>> def function(): print("I don\'t have") >>> instance=Class() >>> instance.method() I have a self >>> instance.method=function >>> instance.method() I don\'t have
请注意,有没有参数self并不取决是否以刚才使用的方法(如instance.method())调用方法。
实际上,你也可以让另一个参数指向同一个方法:
>>> bird=Bird() >>> bird.sing() Squaawk! >>> birdsong=bird.sing >>> birdsong() Squaawk! #虽然最后一种方法调用看起来像是函数调用,单变量birdsong指向的是关联的方法bird.sing,这意味着它也能够访问参数self(即它也能够被关联到类的实例)
2.4再谈隐藏
默认情况下,可以从外部访问对象的属性。
有人认为违反了封装原则,认为应该对外部完全隐藏对象的状态(即不能从外部访问它们)。
Python没有为私有属性提供直接的支持,而是要求程序员知道在什么情况下从外部修改属性是安全的。毕竟,你只有知道如何使用对象之后才能使用它。
要想方法或者属性成为私有的(无法从外部访问),只需要让其名称以两个下划线打头即可:
>>> class Secretive: def __inaccessible(self): print("Bet you can\'t see me ...") def accessible(self): print("The secret message is ...") self.__inaccessible() >>> s=Secretive() >>> s.__inaccessible() Traceback (most recent call last): File "<pyshell#128>", line 1, in <module> s.__inaccessible() AttributeError: \'Secretive\' object has no attribute \'__inaccessible\' >>> s.accessible() The secret message is ... Bet you can\'t see me ... #虽然以两个下划线打头有点怪异,但这样的方法类似于其它语言中的标准私有方法。
然而,幕后的处理方法却并不标准:在类定义中,对所有以两个下划线打头的名称都进行替换,即在开头加上一个下划线和类名。
>>> Secretive._Secretive__inaccessible <function Secretive.__inaccessible at 0x000002BC8D06E620> #只要知道这种幕后处理方式之后,你就可以从外部继续访问了,但你不应该这样做。 >>> s._Secretive__inaccessible() Bet you can\'t see me ... #总之,你无法阻止别人访问对象的私有方法和属性,你只是以这种方式告诉他们不要这么做.....
2.5类的命名空间
在class 语句中定义的代码都是在一个特殊的命名空间(类的命名空间)中执行的,而类的所有成员都可以访问这个命名空间。类的定义其实就是要执行的代码段。
>>> class MemberCounter: members=0 def init(self): MemberCounter.members+=1 >>> m1=MemberCounter() >>> m1.init() >>> MemberCounter.members 1 >>> m2=MemberCounter() >>> m2.init() >>> MemberCounter.members 2
每个实例都可以访问这个类作用域内的变量,就像方法一样。
但如果在实例中给属性赋值
>>> m1.members=\'AAA\' >>> m1.members \'AAA\' >>> m2.members 2
新值被写入m1的一个属性中,这个属性遮住了类级变量。这类似于前边讨论的局部变量与全局变量的关系。
2.6指定超类
子类扩展了超类的定义。要指定超类,可在class语句中的类名后面加上超类名,并将其用圆括号括起来。
>>> class Filter: def init(self): self.blocked=[] def filiter(self,sequence): return [x for x in sequence if x not in self.blocked ] >>> class Filter: def init(self): self.blocked=[] def filter(self,sequence): return [x for x in sequence if x not in self.blocked ] >>> class SPAMFilter(Filter): def init(self):#重写方法 self.blocked=[\'SPAM\'] >>> f=Filter() >>> f.init() >>> f.filter([1,2,3,4,5,6]) [1, 2, 3, 4, 5, 6] >>> s=SPAMFilter() >>> s.init() >>> s.filter([\'SPAM\',\'SPAM\',\'jiameng\',\'wangweili\']) [\'jiameng\', \'wangweili\']
2.7深入探讨继承
要确定一个类是不是另一个类的子类,可以使用内置方法:
>>> issubclass(SPAMFilter,Filter) True >>> issubclass(Filter,SPAMFilter) False
如果以有一个类,想知道它的基类可以访问它的特殊属性:__bases__
>>> SPAMFilter.__bases__ (<class \'__main__.Filter\'>,)
同样要确定对象是不是特定类的实例,可以使用isinstance
>>> isinstance(s,SPAMFilter) True >>> isinstance(s,Filter) True #如你所见,s也是Filter的实例
如果你要获悉对象属于哪个类,可以使用属性__class__
>>> s.__class__ <class \'__main__.SPAMFilter\'> >>> type(s) <class \'__main__.SPAMFilter\'> #对于新式类(无论是通过使用 __mataclass__=type还是通过object继承创建的)的实例,还可以使用type(s)来获悉其所属的类。对于旧式类的实例,type都是返回instance
2.8多个超类
一个类的基类可能有很多。这里被称为多重继承,但这里要注意的是,如果多个超类以不同的方式实现了同一个方法(即有多个同名方法),必须在class语句中小心排列这些超类,因为位于前面的类将会覆盖后面类的方法(这好像有点不太好理解,难道不应该从上到下执行的吗?但事实上前面的方法的确会覆盖后面的方法)。
多个超类同时使用时,查找特定方法或者属性时访问的顺序被称为方法解析顺序(MRO),它使用的算法很复杂,但很有效,你根本无需担心。
2.9接口和内省
接口这个概念与多态相关。处理多态对象时,你只关心接口(协议)——对外暴露的方法和属性。
Python中,不显式地指定对象必须包含哪些方法才能做参数。例如,你不会像在JAVA中那样显式地编写接口,而是假定对象能够完成你要求它完成的任务。如果不能完成,程序失败!!!
通常,你要求对象遵守特定的接口(即实现特定的方法),但如果需要,你可以非常灵活的提出要求:不是直接调用方法并期待一切顺利,而是检查所需的方法是否存在,如果不存在,就不再使用,避免程序失败。
>>> hasattr(tc,\'talk\')#tc 包含属性talk >>>Ture >>> callable(getattr(tc,\'talk\',None))#检查属性talk是否可以被调用,请注意,这里没有在if语句中使用hasattr来直接访问属性,而是使用了getattr(它让我能够指定属性不存在时使用的默认值,这里为None),然后对返回的对象调用callable >>>Ture
setattr与getattr功能相反,可用于设置对象的属性:
>>> setattr(tc,\'name\',\'Mr.Jia\') >>> tc.name \'Mr.Jia\'
要查看这个对象中存储的所有值,可检查其__dict__属性。
如果要确定这个对象由什么组成,应该研究模块inspect,
2.10抽象基类
但是,有比手工检查(使用hasattr等)更好的方法!!!!
Python最终引进了abc模块提供了官方解决方法。这个模块为所谓的抽象基类提供了支持。一般而言,抽象类是不能被实例化的类,其职责是定义子类应该实现的一组抽象方法。,下面是一个简单示例:
>>> from abc import ABC,abstractmethod >>> class Talker(ABC): @abstractmethod#装饰器,这里的作用是将方法标记为抽象的 def talk(self): pass >>> Talker() Traceback (most recent call last): File "<pyshell#214>", line 1, in <module> Talker() TypeError: Can\'t instantiate abstract class Talker with abstract methods talk #上边报错,是因为抽象类不能被实例化 #派生一个子类 >>> class knigget(Talker): pass #上边实例化也会报错,下面重写方法talk >>> class Knigget(Talker): def talk(self): print("Hi!") #实例化完全没有问题。这是抽象类的主要用途。只有在这种情况下,使用isinstance 才是安全的:如果先检查给定的实例确实是Talker对象,就能相信这个实例在有需要的时候才有方法talk >>> k=Knigget() >>> isinstance(k,Talker) True >>> k.talk() Hi!
然而,还缺少一个重要的部分——让isinstance多态程度更高的部分。正如我们不关心对象是什么,只关心对象能做什么。这样只要实现了talk方法,即使不是Talker的子类,都可以通过类型检查。下面新建一个类:
>>> class Herring: def talk(self): print("Blub.") #这个类可以通过是否是Talker对象的检查,但它并不是Talker的对象 >>> h=Herring() >>> isinstance(h,Talker) False #诚然,你可以直接从Talker派生出herring但这样Herring也可能是从其他人的模块中导入的。在这种情况下,就无法采用这种方法。 #为了解决这个问题,你可以将Herring注册为Talker,这样所有的Herring对象都会被看做talker对象。 >>> Talker.register(Herring) <class \'__main__.Herring\'> >>> isinstance(h,Talker) True >>> issubclass(Herring,Talker) True
然而,这样做存在一个缺点,就是直接从抽象类派生提供的保障没了…….
>>> 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#243>", line 1, in <module> c.talk() AttributeError: \'Clam\' object has no attribute \'talk\'
换而言之,应将isinatance返回True视为一种意图表达。在这里,Clam有称为Talker的意图。本着鸭子类型的精神,我们相信它能承担Talker的职责,不行的是失败了。’
3、面向对象的思考
3.1将相关的东西放在一起。如果一个函数只操作一个全局变量,最好将它作为一个类的属性和方法。
3.2不要让对象之间过于亲密。方法应只关心其所属实例的属性,对其他实例的状态,让它们自己去管理。
3.3慎用继承,尤其是多重继承。
3.4保持简单。让方法短小紧凑。
小结:
本节学到的新函数
函数 | 描述 |
callable(object) | 判断对象是否可调用(如是否是函数或方法) |
getaeet(obiect,name[ , default]) | 获取属性的值,还可以提供默认值 |
hasattr(object,name) | 确定对象是否有指定的属性 |
isinstance(object,class) | 确定对象是否是指定类的实例 |
issubclass(A,B) | 确定A是否是B的子类 |
random.choice(sequence) | 从一个非空列表中随机地选取一个元素 |
setattr(object,name,value) | 将对象的指定属性设置为指定值 |
type(object) | 返回对象的类型 |