zoukankan      html  css  js  c++  java
  • 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) 返回对象的类型
  • 相关阅读:
    error_reporting(“E_ALL”)和ini_set(“display_errors”, “on”)的区别?
    linux命令awk的详解
    Ubuntu 能PING IP但不能PING主机域名的解决方法
    从github checkout子文件夹
    zuul简单使用
    docker for windows 10 添加阿里云镜像仓库无效问题
    Spring Boot 进行Bean Validate和Method Validate
    JVM调优-GC参数
    Spring Aop: 关于继承和execution target this @annotation
    ReentrantLock原理
  • 原文地址:https://www.cnblogs.com/jiameng991010/p/11239711.html
Copyright © 2011-2022 走看看