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) 返回对象的类型

 

版权声明:本文为jiameng991010原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/jiameng991010/p/11239711.html