美文网首页
15 Python面向对象编程

15 Python面向对象编程

作者: 泷汰泱 | 来源:发表于2019-09-26 10:52 被阅读0次

    Python从设计之初就是一门面向对象语言,它提供一些语言特性支持面向对象编程。
    创建对象是Python的核心概念,本章将介绍如何创建对象,以及多态、封装、方法和继承等概念。

    理解面向对象

    什么是面向对象编程

    Python是一门面向对象编程语言,对面向对象语言编码的过程叫作面向对象编程。
    面向对象编程(Object Oriented Programming, OOP)是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含数据和操作数据的函数。
    面向对象程序设计把计算机程序视为一组对象的集合,每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。
    在Python中,所有数据类型都被视为对象,也可以自定义对象。自定义对象数据类型就是面向对象中的类(Class)的概念。

    面向对象术语简介

    在开始具体介绍面向对象技术之前,我们先了解一些面向对象的术语,以便在后续内容中碰到对应词时能明白这些术语的意思。

    • 类:用来描述具有相同属性和方法的对象的集合。类定义了集合中每个对象共有的属性和方法。对象是类的实例。
    • 类变量(属性):类变量在整个实例化的对象中是公用的。类变量定义在类中,且在方法之外。类变量通常不作为实例变量使用。类变量也称作属性。
    • 数据成员:类变量或实例变量用于处理类及其实例对象的相关数据。
    • 方法重写:如果从父类继承的方法不能满足子类的需求,就可以对其进行改写,这个过程称为方法的覆盖(Override),也称为方法的重写。
    • 实例变量:定义在方法中的变量只作用于当前实例的类。
    • 多态(Polymorphism):对不同类的对象使用同样的操作。
    • 封装(Encapsulation):对外部世界隐藏对象的工作细节。
    • 继承(Inheritance):即一个派生类(derived class)继承基类(base class)的字段和方法。继承允许把一个派生类的对象作为一个基类对象对待,以普通类为基础建立专门的类对象。
    • 实例化(Instance):创建一个类的实例、类的具体对象。
    • 方法:类中定义的函数。
    • 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。

    和其他编程语言相比,Python在尽可能不增加新语法和语义的情况下加入了类机制。
    Python中的类提供了面向对象编程的所有基本功能:类的继承机制允许多个基类、派生类可以覆盖基类中的任何方法、方法中可以调用基类中的同名方法。
    对象可以包含任意数量和类型的数据。

    类的定义与使用

    1 类的定义

    开始介绍前先看一个类的示例:

    class MyClass(object):
        i = 123
        def f(self):
             return 'hello world'
    

    由上面的代码可以得知,类定义的语法格式如下:

    class ClassName(object):
     <statement-1>
     .
     .
     .
     <statement-N>

    由代码片段和类定义我们看到,Python中定义类使用class关键字,class后面紧接着类名,如示例中的MyClass,类名通常是大写开头的单词;紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。类包含属性(相当于函数中的语句)和方法(类中的方法大体可以理解成函数)。
    提示:在类中定义方法的形式和函数差不多,但不称为函数,而称为方法。方法的调用需要绑定到特定对象上,而函数不需要。我们后面会逐步接触方法的调用方式。

    2 类的使用

    本节简单讲述类的使用。以8.2.1小节的示例为例(别忘了写开头两行),保存并执行(程序编写完成后,需要将文件保存为后缀为.py的文件,在cmd命令窗口下执行.py文件):

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class MyClass(object):
        i = 123
        def f(self):
             return 'hello world'
    
    use_class = MyClass()
    print('调用类的属性:',use_class.i)
    print('调用类的方法:',use_class.f())
    

    执行结果如下:

    调用类的属性: 123
    调用类的方法: hello world

    由输入代码中的调用方式可知,类的使用比函数调用多了几个操作,调用类时需要执行如下操作:

    use_class = MyClass()

    这步叫作类的实例化,即创建一个类的实例。此处得到的use_class变量称为类的具体对象。再看后面两行的调用:

    print('调用类的属性:',use_class.i)
    print('调用类的方法:',use_class.f())

    这里第一行后的use_class.i用于调用类的属性,也就是我们前面所说的类变量。第二行后的use_class.f()用于调用类的方法。
    在上面的示例中,在类中定义f()方法时带了一个self参数,该参数在方法中并没有被调用,是否可以不要呢?调用f()方法时没有传递参数,是否表示参数可以传递也可以不传递?
    对于在类中定义方法的要求:在类中定义方法时,第一个参数必须是self。除第一个参数外,类的方法和普通函数没什么区别,如可以用默认参数、可变参数、关键字参数和命名关键字参数等。
    对于在类中调用方法的要求:要调用一个方法,在实例变量上直接调用即可。除了self不用传递,其他参数正常传入。
    类对象支持两种操作,即属性引用和实例化。属性引用的标准语法如下:

    obj.name

    语法中obj代表类对象,name代表属性。

    深入类

    将深入介绍类的相关内容,如类的构造方法和访问权限。

    1 类的构造方法

    在开始介绍前,我们对前面的示例做一些改动,代码如下:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class MyClass(object):
        i = 123
        def __init__(self, name):
             self.name = name
    
        def f(self):
             return 'hello,'+ self.name
    
    use_class = MyClass('xiaoming')
    print('调用类的属性:',use_class.i)
    print('调用类的方法:',use_class.f())
    

    程序执行结果如下:

    调用类的属性: 123
    调用类的方法: hello,xiaoming

    若类的实例化语句写法和之前一样,即:

    use_class = MyClass()

    程序执行结果如下:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 21, in <module>
        use_class = MyClass()
    TypeError: __init__() missing 1 required positional argument: 'name'
    

    从代码和输出结果看到,实例化MyClass类时调用了__init__()方法。这里就奇怪了,我们在代码中并没有指定调用__init__()方法,怎么会报__init__()方法错误呢?
    在Python中,__init__()方法是一个特殊方法,在对象实例化时会被调用。__init__()的意思是初始化,是initialization的简写。这个方法的书写方式是:先输入两个下划线,后面接着init,再接着两个下划线。这个方法也叫构造方法。在定义类时,若不显式地定义一个__init__()方法,则程序默认调用一个无参的__init__()方法。比如以下两段代码的使用效果是一样的:
    代码一:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class DefaultInit(object):
        def __init__(self):
             print('类实例化时执行我,我是__init__方法。')
        def show(self):
             print ('我是类中定义的方法,需要通过实例化对象调用。')
    
    test = DefaultInit()
    print('类实例化结束。')
    test.show()
    

    程序执行结果如下:

    类实例化时执行我,我是__init__方法。
    类实例化结束。
    我是类中定义的方法,需要通过实例化对象调用。

    代码二:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class DefaultInit(object):
        def show(self):
             print ('我是类中定义的方法,需要通过实例化对象调用。')
    
    test = DefaultInit()
    print('类实例化结束。')
    test.show()
    

    程序执行结果如下:

    类实例化结束。
    我是类中定义的方法,需要通过实例化对象调用。

    由上面两段代码的输出结果看到,当代码中定义了__init__()方法时,实例化类时会调用该方法;若没有定义__init__()方法,实例化类时也不会报错,此时调用默认的__init__()方法。
    在Python中定义类时若没有定义构造方法(__init__()方法),则在类的实例化时系统调用默认的构造方法。另外,__init__()方法可以有参数,参数通过init()传递到类的实例化操作上。

    既然__init__()方法是Python中的构造方法,那么是否可以在一个类中定义多个构造方法呢?我们先看如下3段代码:

    代码一:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class DefaultInit(object):
          def __init__(self):
                print('我是不带参数的__init__方法。')
    
    DefaultInit()
    print('类实例化结束。')
    

    程序执行结果如下:

    我是不带参数的__init__方法。
    类实例化结束。

    在只有一个__init__()方法时,实例化类没有什么顾虑。
    代码二:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class DefaultInit(object):
        def __init__(self):
             print('我是不带参数的__init__方法。')
    
        def __init__(self, param):
             print('我是带一个参数的__init__方法,参数值为:',param)
    
    DefaultInit('hello')
    print('类实例化结束。')
    

    程序执行结果如下:

    我是带一个参数的__init__方法,参数值为: hello
    类实例化结束。

    由执行结果看到,调用的是带了一个param参数的构造方法,若把类的实例化语句更改为:

    DefaultInit()
    执行结果为:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 59, in <module>
        DefaultInit()
    TypeError: __init__() missing 1 required positional argument: 'param'
    

    或更改为:

    DefaultInit('hello', 'world')

    执行结果为:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 61, in <module>
        DefaultInit('hello', 'world')
    TypeError: __init__() takes 2 positional arguments but 3 were given
    

    由执行结果看到,实例化类时只能调用带两个占位参数的构造方法,调用其他构造方法都会报错。
    代码三:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class DefaultInit(object):
        def __init__(self, param):
             print('我是带一个参数的__init__方法,参数值为:',param)
    
        def __init__(self):
             print('我是不带参数的__init__方法。')
    
    DefaultInit()
    print('类实例化结束。')
    

    程序执行结果如下:

    我是不带参数的__init__方法。
    类实例化结束。

    由执行结果看到,调用的构造方法除了self外,没有其他参数。若把类的实例化语句更改为如下:

    DefaultInit('hello')

    执行结果为:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 60, in <module>
        DefaultInit('hello')
    TypeError: __init__() takes 1 positional argument but 2 were given
    

    或更改为:

    DefaultInit('hello', 'world')

    执行结果为:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 61, in <module>
        DefaultInit('hello', 'world')
    TypeError: __init__() takes 2 positional arguments but 3 were given
    

    由执行结果看到,实例化类时只能调用带一个占位参数的构造方法,调用其他构造方法都会报错。
    由以上几个示例我们得知:一个类中可定义多个构造方法,但实例化类时只实例化最后的构造方法,即后面的构造方法会覆盖前面的构造方法,并且需要根据最后一个构造方法的形式进行实例化。建议一个类中只定义一个构造函数。

    2 类的访问权限

    在类内部有属性和方法,外部代码可以通过直接调用实例变量的方法操作数据,这样就隐藏了内部的复杂逻辑,例如:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class Student(object):
        def __init__(self, name, score):
             self.name = name
             self.score = score
    
        def info(self):
             print('学生:%s;分数: %s' % (self.name, self.score))
    
    stu = Student('xiaomeng',95)
    print ('修改前分数:', stu.score)
    stu.info()
    stu.score=0
    print ('修改后分数:', stu.score)
    stu.info()
    

    程序执行结果如下:

    修改前分数:95
    学生:xiaomeng;分数: 95
    修改后分数:0
    学生:xiaomeng;分数: 0

    由代码和输出结果看到,在类中定义的非构造方法可以调用类中构造方法实例变量的属性,调用的方式为self.实例变量属性名,如代码中的self.name和self.score。可以在类的外部修改类的内部属性。如果要让内部属性不被外部访问,该怎么办呢?
    要让内部属性不被外部访问,可以在属性名称前加两个下划线__。在Python中,实例的变量名如果以__开头,就会变成私有变量(private),只有内部可以访问,外部不能访问。据此,我们把Student类改一改:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class Student(object):
        def __init__(self, name, score):
             self.__name = name
             self.__score = score
    
        def info(self):
             print('学生:%s;分数: %s' % (self.__name, self.__score))
    
    stu = Student('xiaomeng',95)
    print('修改前分数:', stu.__score)
    stu.info()
    stu.__score = 0
    print('修改后分数:',stu.__score)
    stu.info()
    

    程序执行结果如下:

    Traceback (most recent call last):
      File "D:/python/workspace/classdef.py", line 81, in <module>
        print('修改前分数:', stu.__score)
    AttributeError: 'Student' object has no attribute '__score'
    

    由执行结果看到,我们已经无法从外部访问实例变量的属性__score了。这样有什么作用呢?
    这样可以确保外部代码不能随意修改对象内部的状态,通过访问限制的保护,代码更加安全。比如上面的分数对象是一个比较重要的内部对象,如果外部可以随便更改这个值,大家都随便更改自己成绩表单中的分数,岂不是很混乱。
    如果外部代码要获取类中的namescore怎么办呢?
    在Python中,可以为类增加get_attrs方法,获取类中的私有变量,例如在上面的示例中添加get_score(name的使用方式类同)方法,代码如下:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class Student(object):
        def __init__(self, name, score):
             self.__name = name
             self.__score = score
    
        def info(self):
             print('学生:%s;分数: %s' % (self.__name, self.__score))
    
        def get_score(self):
             return self.__score
    
    stu = Student('xiaomeng',95)
    print('修改前分数:', stu.get_score())
    stu.info()
    print('修改后分数:',stu.get_score())
    stu.info()
    

    执行结果如下:

    修改前分数: 95
    学生:xiaomeng;分数: 95
    修改后分数: 95
    学生:xiaomeng;分数: 95

    由执行结果看到,通过get_score方法已经可以正确得到类内部的属性值。
    是否可以通过外部更改内部私有变量的值呢?
    在Python中,可以为类增加set_attrs方法,修改类中的私有变量,如更改上面示例中的score属性值,可以添加set_score(name使用方式类同)方法,代码如下:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class Student(object):
        def __init__(self, name, score):
             self.__name = name
             self.__score = score
    
        def info(self):
             print('学生:%s;分数: %s' % (self.__name, self.__score))
    
        def get_score(self):
             return self.__score
    
        def set_score(self, score):
             self.__score = score
    
    stu = Student('xiaomeng',95)
    print('修改前分数:', stu.get_score())
    stu.info()
    stu.set_score(0)
    print('修改后分数:',stu.get_score())
    stu.info()
    

    程序执行结果如下:

    修改前分数: 95
    学生:xiaomeng;分数: 95
    修改后分数: 0
    学生:xiaomeng;分数: 0

    由程序执行结果看到,通过set_score方法正确更改了私有变量score的值。这里有个问题,原先stu.score=0这种方式也可以修改score变量,为什么要费这么大周折定义私有变量,还定义set_score方法呢?
    在Python中,通过定义私有变量和对应的set方法可以帮助我们做参数检查,避免传入无效的参数,如对上面的示例更改如下:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class Student(object):
        def __init__(self, name, score):
             self.__name = name
             self.__score = score
    
        def info(self):
             print('学生:%s;分数: %s' % (self.__name, self.__score))
    
        def get_score(self):
             return self.__score
    
        if 0<=score<=100:
                 self.__score = score
             else:
                 print('请输入0 到100 的数字。')
    
    stu = Student('xiaomeng',95)
    print('修改前分数:', stu.get_score())
    stu.info()
    stu.set_score(-10)
    print('修改后分数:',stu.get_score())
    stu.info()
    

    程序执行结果如下:

    修改前分数: 95
    学生:xiaomeng;分数: 95
    请输入0 到100 的数字。
    修改后分数: 95
    学生:xiaomeng;分数: 95

    由输出结果看到,调用set_score方法时,如果传入的参数不满足条件,就按照不满足条件的程序逻辑执行。
    既然类有私有变量的说法,那么类是否有私有方法呢?
    答案是肯定的,类也有私有方法。类的私有方法也是以两个下划线开头,声明该方法为私有方法,且不能在类外使用。私有方法的调用方式如下:

    self.__private_methods

    我们通过下面的示例进一步了解私有方法的使用:

    #! /usr/bin/python3
    # -*-coding:UTF-8-*-
    
    class PrivatePublicMethod(object):
        def __init__(self):
             pass
    
        def __foo(self):          # 私有方法
             print('这是私有方法')
    
        def foo(self):            # 公共方法
             print('这是公共方法')
             print('公共方法中调用私有方法')
             self.__foo()
             print('公共方法调用私有方法结束')
    
    
    pri_pub = PrivatePublicMethod()
    print('开始调用公共方法:')
    pri_pub.foo()
    print('开始调用私有方法:')
    pri_pub.__foo()
    

    程序执行结果如下:

    开始调用公共方法:
    这是公共方法
    公共方法中调用私有方法
    这是私有方法
    公共方法调用私有方法结束
    开始调用私有方法:
    Traceback (most recent call last):
    File "D:/python/workspace/classdef.py", line 114, in <module>
    pri_pub.__foo()
    AttributeError: 'PrivatePublicMethod' object has no attribute '__foo'

    由输出结果看到,私有方法和私有变量类似,不能通过外部调用。

    继承

    面向对象编程带来的好处之一是代码的重用,实现重用的方法之一是通过继承机制。继承完全可以理解成类之间类型和子类型的关系。
    在面向对象程序设计中,当我们定义一个class时,可以从某个现有的class继承,定义的新class称为子类(Subclass),而被继承的class称为****基类、父类或超类(Base class、Super class)**。
    继承的定义如下:

    class DerivedClassName(BaseClassName):
        <statement-1>
        .
        .
        <statement-N>
    

    需要注意:继承语法class子类名(基类名)时,//基类名写在括号里,基本类是在定义类时,在元组中指明的。
    在Python中,继承有以下特点:
    (1)在继承中,基类的构造方法(__init__()方法)不会被自动调用,需要在子类的构造方法中专门调用。
    (2)在调用基类的方法时需要加上基类的类名前缀,并带上self参数变量。区别于在类中调用普通函数时不需要带self参数。
    (3)在Python中,首先查找对应类型的方法,如果在子类中找不到对应的方法,才到基类中逐个查找。
    例如:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Animal(object):
        def run(self):
             print('Animal is running...')
    

    上面定义了一个名为Animal的类,类中定义了一个run()方法直接输出(没有显式定义__init__()方法,会调用默认的构造方法)。在编写Dog和Cat类时,可以直接从Animal类继承,定义如下:

    class Dog(Animal):
        pass
    
    class Cat(Animal):
        pass
    

    在这段代码片段中,对于Dog来说,Animal就是它的父类;对于Animal来说,Dog就是它的子类。Cat和Dog类似。
    继承有什么好处?
    继承最大的好处是子类获得了父类全部非私有的功能。由于在Animial中定义了非私有的run()方法,因此作为Animial的子类,Dog和Cat什么方法都没有定义,自动拥有父类中的run()方法。
    执行以上代码:

    dog = Dog()
    dog.run()
    
    cat = Cat()
    cat.run()
    

    程序执行结果如下:

    Animal is running...
    Animal is running...

    由执行结果看到,子类中没有定义任何方法,但都成功执行了run()方法。当然,子类可以拥有一些自己的方法,比如在Dog类中增加一个eat方法:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Dog(Animal):
        def eat(self):
             print('Eating ...')
    
    dog = Dog()
    dog.run()
    dog.eat()
    

    以上代码执行结果如下:

    Animal is running...
    Eating ...

    由执行结果看到,既执行了父类的方法,又执行了自己定义的方法。
    子类不能继承父类中的私有方法,也不能调用父类的私有方法。父类的定义如下:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Animal(object):
        def run(self):
             print('Animal is running...')
    
        def __run(self):
             print('I am a private method.')
    

    子类定义不变,执行如下调用语句:

    dog = Dog()
    dog.__run()
    执行结果如下:

    Traceback (most recent call last):
      File "D:/python/workspace/classextend.py", line 25, in <module>
        dog.__run()
    AttributeError: 'Dog' object has no attribute '__run'
    

    由执行结果看到,子类不能调用父类的私有方法,子类虽然继承了父类,但是调用父类的私有方法相当于从外部调用类中的方法,因而调用不成功。
    对于父类中扩展的非私有方法,子类可以拿来即用,如在父类Animal中增加一个jump方法:

    class Animal(object):
        def run(self):
             print('Animal is running...')
    
        def jump(self):
             print('Animal is jumpping....')
    
        def __run(self):
             print('I am a private method.')
    

    上面我们增加了一个非私有的jump()方法,子类Dog和Cat保持原样,执行如下调用:

    dog = Dog()
    dog.run()
    dog.jump()
    
    cat = Cat()
    cat.run()
    cat.jump()
    

    执行结果如下:

    Animal is running...
    Animal is jumpping....
    Animal is running...
    Animal is jumpping....
    

    由执行结果看到,子类可以立即获取父类增加的非私有方法。

    继承可以一级一级继承下来,就好比从爷爷到爸爸再到儿子的关系。所有类最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树,如图所示。 继承树

    多态

    继承可以帮助我们重复使用代码。但对于继承中的示例,无论是Dog还是Cat,调用父类的run()方法时显示的都是Animal is running…,如果想让结果显示为Dog is running…和Cat is running…,该怎么处理呢?
    我们对Dog和Cat类做如下改进:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Dog(Animal):
        def run(self):
             print('Dog is running...')
    
    class Cat(Animal):
    def run(self):
        print('Cat is running...')
    

    执行如下语句:

    dog = Dog()
    print('实例化Dog 类')
    dog.run()
    
    cat = Cat()
    print('实例化Cat类')
    cat.run()
    

    执行结果如下:

    实例化Dog 类
    Dog is running...
    实例化Cat类
    Cat is running...
    由执行结果看到,分别得到了Dog和Cat各自的running结果。
    当子类和父类存在相同的run()方法时,子类的run()方法会覆盖父类的run()方法,在代码运行时总是会调用子类的run()方法,称之为多态。
    多态来自于希腊语,意思是有多种形式。多态意味着即使不知道变量所引用的对象类型是什么,也能对对象进行操作,多态会根据对象(或类)的不同而表现出不同的行为。例如,我们在上面的Animal类中定义了run方法,Dog和Cat类分别继承Animal类,并且分别定义了自己的run方法,最后Dog和Cat调用的是自己定义的run方法。
    为了更好地理解什么是多态,我们对数据类型再做一点说明。当我们定义一个类时,实际上就定义了一种数据类型。定义的数据类型和Python自带的数据类型(如str、list、dict)没什么两样。

    a = list() # a 是list 类型
    b = Animal() # b 是Animal类型
    c = Dog() # c 是Dog 类型
    

    下面用isinstance()方法判断一个变量是否是某个类型。

    print('a 是否为list类型:', isinstance(a, list))
    print('b 是否为Animal 类型:', isinstance(b, Animal))
    print('c 是否为Dog 类型:', isinstance(c, Dog))
    

    执行结果如下:

    a 是否为list 类型: True
    b 是否为Animal类型: True
    c 是否为Dog 类型: True

    由执行结果看到,a、b、c确实分别为list、Animal、Dog三种类型。我们再执行如下语句:

    print('c 是否为Dog 类型:', isinstance(c, Dog))
    print('c 是否为Animal 类型:',isinstance(c, Animal))
    

    执行结果如下:

    c 是否为Dog 类型: True
    c 是否为Animal类型: True

    由执行结果看到,c既是Dog类型又是Animal类型。这怎么理解呢?
    因为Dog是从Animal继承下来的,当我们创建Dog的实例c时,我们认为c的数据类型是Dog,但c同时也是Animal, Dog本来就是Animal`的一种。
    在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以看作是父类。但是反过来就不行,例如以下语句:

    b = Animal()
    print('b 是否为Dog 类型:', isinstance(b, Dog))
    

    执行结果如下:

    b 是否为Dog 类型:False

    由输出结果看到,变量b是Animal的实例化对象,是Animal类型,但不是Dog类型,也就是Dog可以看成Animal,但Animal不可以看成Dog。
    我们再看一个示例。编写一个函数,这个函数接收一个Animal类型的变量,定义并执行如下函数,执行时传入Animal的实例:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    def run_two_times(animal):
        animal.run()
        animal.run()
    
    run_two_times(Animal())
    

    执行结果如下:

    Animal is running...
    Animal is running...

    若执行函数时传入Dog的实例,操作如下:

    run_two_times(Dog())
    

    得到执行结果如下:

    Dog is running...
    Dog is running...

    若传入Cat的实例,操作如下:

    run_two_times(Cat())
    

    得到执行结果如下:

    Cat is running...
    Cat is running...

    看上去没有什么特殊的地方,已经正确输出预期结果了,但是仔细想想,如果再定义一个Bird类型,也继承Animal类,定义如下:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Bird(Animal):
        def run(self):
             print('Bird is flying the sky...')
    
    run_two_times(Bird())
    

    程序执行结果如下:

    Bird is flying the sky...
    Bird is flying the sky...

    由执行结果我们发现,新增的Animal子类不必对run_two_times()方法做任何修改。实际上,任何依赖Animal作为参数的函数或方法都可以不加修改地正常运行,原因就在于多态。
    多态的好处是:当我们需要传入Dog、Cat、Bird等对象时,只需要接收Animal类型就可以了,因为Dog、Cat、Bird等都是Animal类型,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此传入的类型只要是Animal类或继承自Animal类,都会自动调用实际类型的run()方法。
    多态的意思是:对于一个变量,我们只需要知道它是Animal类型,无须确切知道它的子类型,就可以放心调用run()方法。具体调用的run()方法作用于Animal、Dog、Cat或Bird对象,由运行时该对象的确切类型决定。
    多态真正的威力在于:调用方只管调用,不管细节。当我们新增一种Animal的子类时,只要确保run()方法编写正确即可,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:对于扩展开放,允许新增Animal子类;对于修改封闭,不需要修改依赖Animal类型的run_two_times()等函数。
    很多函数和运算符都是多态的,你写的绝大多数程序也可能是,即便你并非有意这样。只要使用多态函数和运算符,多态就会消除。唯一能够毁掉多态的是使用函数显式地检查类型,如type、isinstance函数等。如果有可能,就尽量避免使用这些毁掉多态的方式,重要的是如何让对象按照我们希望的方式工作,无论它是否是正确类型或类。

    封装

    前面我们讲述了Python对象中两个重点——继承和多态,这里将讲述第3个重点——封装
    封装是全局作用域中其他区域隐藏多余信息的原则。听起来有些像多态,使用对象而不用知道其内部细节。它们都是抽象原则,都会帮忙处理程序组件而不用过多关心细节,就像函数一样。
    封装并不等同于多态。多态可以让用户对不知道类(或对象类型)的对象进行方法调用,而封装可以不用关心对象是如何构建的,直接使用即可。
    前面几节的示例基本都用到封装的思想,如前面定义的Student类中,每个实例都拥有各自的name和score数据。我们可以通过函数访问这些数据,如输出学生的成绩,可以如下定义并执行:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Student(object):
        def __init__(self, name, score):
             self.name = name
             self.score = score
    
    std = Student('xiaozhi',90)
    def info(std):
        print('学生:%s;分数: %s' % (std.name, std.score))
    info(std)
    

    执行结果为:

    学生:xiaozhi;分数: 90

    由输出结果看到,可以通过函数调用类并得到结果。
    既然Student实例本身就拥有这些数据,要访问这些数据就没有必要从外面的函数访问,可以直接在Student类内部定义访问数据的函数,这样就把“数据”封装起来了。这些封装数据的函数和Student类本身是相关联的,我们称之为类的方法。于是就有了前面所写类的形式:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Student0(object):
        def __init__(self, name, score):
             self.name = name
        self.score = score
    
    def info(self):
        print('学生:%s;分数: %s' % (self.name, self.score))
    

    要定义一个方法,除了第一个参数是self外,其他参数和普通函数一样。要调用一个方法,在实例变量上直接调用即可。除了self不用传递,其他参数正常传入,执行如下语句:

    stu = Student0('xiaomeng',95)
    

    执行结果为:

    学生:xiaomeng;分数: 95

    这样一来,我们从外部看Student类,只需要知道创建实例需要给出的name和score,如何输出是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节。
    封装的另一个好处是可以给Student类增加新方法,比如我们在类的访问权限中所讲述的get_score()方法和set_score()方法。使用这些方法时,我们无须知道内部实现细节,直接调用即可。

    多重继承

    上面讲述的是单继承,Python还支持多重继承。多重继承的类定义如下:

    class DerivedClassName(Base1, Base2, Base3):
        <statement-1>
        .
        .
        <statement-N>
    

    可以看到,多重继承就是有多个基类(父类或超类)。
    需要注意圆括号中父类的顺序,若父类中有相同的方法名,在子类使用时未指定,Python会从左到右搜索。若方法在子类中未找到,则从左到右查找父类中是否包含方法。
    继续以前面的Animal类为例,假设要实现4种动物:Dog(狗)、Bat(蝙蝠)、Parrot(鹦鹉)、Ostrich(鸵鸟)。
    如果按照哺乳动物和鸟类分类,我们可以设计按哺乳动物分类的类层次图,如图8-2所示。如果按照“能跑”和“能飞”分类,我们可以设计按行为功能分类的类层次图,如图所示。
    如果要把上面的两种分类都包含进来,就得设计更多层次:
    哺乳类:包括能跑的哺乳类和能飞的哺乳类。

    按哺乳动物分类的类层次图
    按行为功能分类的类层次图
    鸟类:包括能跑的鸟类和能飞的鸟类。这么一来,类的层次就复杂了。下图所示为更复杂的类层次图。
    更复杂的类层次图
    如果还要增加“宠物类”和“非宠物类”,类的数量就会呈指数增长,很明显这样设计是不行的。
    正确的做法是采用多重继承。首先,主要的类层次仍按照哺乳类和鸟类设计,设计代码如下:
    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Animal(object):
        pass
    
    # 大类:
    class Mammal(Animal):
        pass
    
    class Bird(Animal):
        pass
    
    # 各种动物:
    class Dog(Mammal):
        pass
    
    class Bat(Mammal):
        pass
    
    class Parrot(Bird):
        pass
    
    class Ostrich(Bird):
        pass
    

    接下来,给动物加上Runnable和Flyable功能。我们先定义好Runnable和Flyable类:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Runnable(object):
        def run(self):
             print('Running...')
    
    class Flyable(object):
        def fly(self):
             print('Flying...')
    

    大类定义好后,对需要Runnable功能的动物添加对Runnable的继承,如Dog:

    class Dog(Mammal, Runnable):
        pass
    

    对需要Flyable功能的动物添加对Flyable的继承,如Bat:

    class Bat(Mammal, Flyable):
        pass
    

    这样,通过上面的多重继承,一个子类就可以继承多个父类,同时获得多个父类所有非私有功能。

    获取对象信息

    当我们调用方法时可能需要传递一个参数,这个参数类型我们知道,但是对于接收参数的方法,就不一定知道是什么参数类型了。我们该怎么得知参数的类型呢?
    Python为我们提供了以下3种获取对象类型的方法。

    1. 使用type()函数

    我们前面已经学习过type()函数的使用,基本类型都可以用type()判断,例如:

    >>> type(123)
    <class 'int'>
    >>> type('abc')
    <class 'str'>
    >>> type(None)
    <class 'NoneType'>
    

    如果一个变量指向函数或类,用type()函数返回的是什么类型?在交互模式下输入:

    >>> type(abs)
    <class 'builtin_function_or_method'>
    >>> type(pri_pub)   #上面定义的PrivatePublicMethod 类
    <class '__main__.PrivatePublicMethod'>
    

    由输出结果看到,返回的是对应的Class类型。
    如果我们要在if语句中判断并比较两个变量的type类型是否相同,应如下操作:

    >>> type(123)==type(456)
    True
    >>> type(123)==int
    True
    >>> type('abc')==type('123')
    True
    >>> type('abc')==str
    True
    >>> type('abc')==type(123)
    False
    

    通过操作我们看到,判断基本数据类型可以直接写int、str等。怎么判断一个对象是否是函数呢?
    可以使用types模块中定义的常量,在交互模式下输入:

    >>> import types
    >>> def func():
    ...     pass
    ...
    >>> type(fn)==types.FunctionType
    True
    >>> type(abs)==types.BuiltinFunctionType
    True
    >>> type(lambda x: x)==types.LambdaType
    True
    >>> type((x for x in range(10)))==types.GeneratorType
    True
    

    由执行结果看到,函数的判断方式需要借助types模块的帮助。

    2. 使用isinstance()函数

    要明确class的继承关系,使用type()很不方便,通过判断class的数据类型确定class的继承关系要方便得多,这个时候可以使用isinstance()函数。
    例如,继承关系是如下形式:
    object -> Animal -> Dog
    即Animal继承object、Dog继承Animal。使用isinstance()可以告诉我们一个对象是否是某种类型。
    例如,创建如下两种类型的对象:

    >>> animal = Animal()
    >>> dog = Dog()
    对上面两种类型的对象,使用isinstance进行判断:
    >>> isinstance(dog, Dog)
    True
    

    根据输出结果看到,dog是Dog类型,这个没有任何疑问,因为dog变量指向的就是Dog对象。接下来判断Animal类型,使用isinstance判断如下:

    >>> isinstance(dog, Animal)
    True
    

    根据输出结果看到,dog也是Animal类型。
    由此我们得知:尽管dog是Dog类型,不过由于Dog是从Animal继承下来的,因此dog也是Animal类型。换句话说,isinstance()判断的是一个对象是否为该类型本身,或者是否为该类型继承类的类型。
    我们可以确信,dog还是object类型:

    >>> isinstance(dog, object)
    True
    同时确信,实际类型是Dog类型的dog,同时也是Animal类型:
    >>> isinstance(dog, Dog) and isinstance(dog, Animal)
    True
    不过animal不是Dog类型,这个我们在8.5节已经讲述过:
    >>> isinstance(animal,Dog )
    False
    

    提醒一点,能用type()判断的基本类型也可以用isinstance()判断。这个可以自己进行验证。
    isinstance()可以判断一个变量是否为某些类型中的一种,判断变量是否为listtuple的方式如下:

    >>> isinstance([1, 2, 3], (list, tuple))
    True
    >>> isinstance((1, 2, 3), (list, tuple))
    True
    

    3. 使用dir()

    如果要获得一个对象的所有属性和方法,就可以使用dir()函数。dir()函数返回一个字符串的list。例如,获得一个str对象的所有属性和方法的方式如下:

         >>> dir('abc')
         ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
    '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__',
    '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__',
    '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode',
    'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier',
    'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition',
    'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title',
    'translate', 'upper', 'zfill']
    

    由输出结果看到,str对象包含许多属性和方法。

    类的专有方法

    我们前面讲述了类的访问权限、私有变量和私有方法,除了自定义私有变量和方法外,Python类还可以定义专有方法。专有方法是在特殊情况下或使用特别语法时由Python调用的,而不是像普通方法一样在代码中直接调用。本节讲述几个Python常用的专有方法。
    看到形如__xxx__的变量或函数名就要注意,这在Python中是有特殊用途的。
    __init__我们已经知道怎么用了,Python的class中有许多这种有特殊用途的函数,可以帮助我们定制类。下面介绍这种特殊类型的函数定制类的方法。

    1.__str__

    开始介绍之前,我们先定义一个Student类,定义如下:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Student(object):
        def __init__(self, name):
             self.name = name
    
    print(Student('xiaozhi'))
    

    执行结果如下:
    <__main__.Student object at 0x0000000000D64198>
    执行结果输出一堆字符串,一般人看不懂,没有什么可用性,也不好看。怎样才能输出得好看呢?
    只需要我们定义好__str__()方法,返回一个好看的字符串就可以了。重新定义上面的示例:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Student(object):
        def __init__(self, name):
             self.name = name
    
        def __str__(self):
             return '学生名称: %s' % self.name
    
    print(Student('xiaozhi'))
    

    执行结果为:

    学生名称: xiaozhi

    由执行结果看到,这样输出的实例不但好看,而且是我们想要的。
    如果在交互模式下输入如下:

    >>> s = Student('xiaozhi')
    >>> s
    <__main__.Student object at 0x00000000030EC550>
    

    由执行结果看到,输出的实例还跟之前一样,不容易识别。
    这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别在于__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串。也就是说,__repr__()是为调试服务的。
    解决办法是再定义一个__repr__()。通常,__str__()__repr__()代码是一样的,所以有一个偷懒的写法:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Student(object):
        def __init__(self, name):
             self.name = name
    
        def __str__(self):
             return '学生名称: %s' % self.name
        __repr__ = __str__
    

    在交互模式下执行:

    >>> s = Student('xiaozhi')
    >>> s
    学生名称: xiaozhi
    

    可以看到,已经得到满意的结果了。

    2.__iter__

    如果想将一个类用于for ... in循环,类似list或tuple一样,就必须实现一个__iter__()方法。该方法返回一个迭代对象,Python的for循环会不断调用该迭代对象的__next__()方法,获得循环的下一个值,直到遇到StopIteration错误时退出循环。
    我们以斐波那契数列为例,写一个可以作用于for循环的Fib类:

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class Fib(object):
        def __init__(self):
             self.a, self.b = 0, 1 # 初始化两个计数器a、b
    
        def __iter__(self):
             return self # 实例本身就是迭代对象,故返回自己
    
        def __next__(self):
             self.a, self.b = self.b, self.a + self.b # 计算下一个值
             if self.a > 100000: # 退出循环的条件
                 raise StopIteration();
             return self.a # 返回下一个值
    

    下面我们把Fib实例作用于for循环。

    >>> for n in Fib():
    ...    print(n)
    ...
    1
    1
    2
    3
    5
    . . .
    89
    

    3. __getitem__

    Fib实例虽然能够作用于for循环,和list有点像,但是不能将它当成list使用。比如取第3个元素:

    >>> Fib()[3]
    Traceback (most recent call last):
      File "<pyshell#35>", line 1, in <module>
        Fib()[3]
    TypeError: 'Fib' object does not support indexing
    

    由执行结果看到,取元素时报错了。怎么办呢?
    要像list一样按照下标取出元素,需要实现__getitem__()方法,代码如下:

    class Fib(object):
        def __getitem__(self, n):
             a, b = 1, 1
             for x in range(n):
                 a, b = b, a + b
             return a
    

    下面尝试取得数列的值:

    >>> fib = Fib()
    >>> fib[3]
    3
    >>> fib[10]
    89
    

    由执行结果看到,可以成功获取对应数列的值了。

    4. __getattr__

    正常情况下,调用类的方法或属性时,如果类的方法或属性不存在就会报错。比如定义Student类:

    class Student(object):
        def __init__(self, name):
             self.name = 'xiaozhi'
    

    对于上面的代码,调用name属性不会有任何问题,但是调用不存在的score属性就会报错。
    执行以下代码:

    >>> stu = Student()
    >>> print(stu.name)
    Xiaozhi
    >>> print(stu.score)
    Traceback (most recent call last):
      File "<pyshell#50>", line 1, in <module>
        print(stu.score)
    AttributeError: 'Student' object has no attribute 'score'
    

    由输出结果看到,错误信息告诉我们没有找到score属性。对于这种情况,有什么解决方法吗?
    要避免这个错误,除了可以添加一个score属性外,Python还提供了另一种机制,就是写一个__getattr__()方法,动态返回一个属性。上面的代码修改如下:

    class Student(object):
    
        def __init__(self):
             self.name = 'xiaozhi'
    
        def __getattr__(self, attr):
             if attr=='score':
                 return 95
    

    当调用不存在的属性时(如score),Python解释器会调用__getattr__(self, 'score')尝试获得属性,这样就有机会返回score的值。在交互模式下输入如下:

    >>> stu = Student()
    >>> stu.name
    xiaozhi
    >>> stu.score
    95
    

    由输出结果看到,可以正确输出不存在的属性的值了。
    注意,只有在没有找到属性的情况下才调用__getattr__,已有的属性(如name),不会在__getattr__中查找。此外,如果所有调用都会返回None(如stu.abc),就是定义的__getattr__默认返回None。

    5. __call__

    一个对象实例可以有自己的属性和方法,调用实例的方法时使用instance.method()调用。能不能直接在实例本身调用呢?答案是可以。
    任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用,例如:

    class Student(object):
        def __init__(self, name):
             self.name = name
    
        def __call__(self):
             print('名称:%s' % self.name)
    

    在交互模式下输入如下:

    >>> stu = Student('xiaomeng')
    >>> stu()
    名称:xiaomeng
    

    由输出结果看到,可以直接对实例进行调用并得到结果。
    __call__()还可以定义参数。对实例进行直接调用就像对一个函数调用一样,完全可以把对象看成函数,把函数看成对象,因为这两者本来就有根本区别。
    如果把对象看成函数,函数本身就可以在运行期间动态创建出来,因为类的实例都是运行期间创建出来的。这样一来,就模糊了对象和函数的界限。
    怎么判断一个变量是对象还是函数呢?
    很多时候判断一个对象是否能被调用,可以使用Callable()函数,比如函数和上面定义的带有__call__()的类实例。输入如下:

    >>> callable(Student('xiaozhi'))
    True
    >>> callable(max)
    True
    >>> callable([1, 2, 3])
    False
    >>> callable(None)
    False
    >>> callable('a')
    False
    

    由操作结果看到,通过callable()函数可以判断一个对象是否为“可调用”对象。

    牛刀小试——出行建议

    小智今天想出去,但不清楚今天的天气是否适宜出行,需要一个帮他提供建议的程序,程序要求输入daytime和night,根据可见度和温度给出出行建议和使用的交通工具,需要考虑需求变更的可能。
    需求分析:使用本章所学的封装、继承、多态比较容易实现,由父类封装查看可见度和查看温度的方法,子类继承父类。若有需要,子类可以覆盖父类的方法,做自己的实现。子类也可以自定义方法。
    定义天气查找类,类中定义两个方法,一个方法根据传入的input_daytime值返回对应的可见度;另一个方法根据传入的input_daytime值返回对应的温度。

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class WeatherSearch(object):
        def __init__(self, input_daytime):
             self.input_daytime = input_daytime
    
        def seach_visibility(self):
             visible_leave = 0
             if self.input_daytime == 'daytime':
                 visible_leave = 2
             if self.input_daytime == 'night':
                 visible_leave = 9
             return visible_leave
    
        def seach_temperature(self):
             temperature = 0
             if self.input_daytime == 'daytime':
                 temperature = 26
             if self.input_daytime == 'night':
                 temperature = 16
             return temperature
    

    定义建议类,该类继承WeatherSearch类。类中定义两个方法,一个覆盖父类的温度查找方法,具有传入的input_daytime的值,返回建议使用的交通工具;另一个方法返回整体的建议。

    #! /usr/bin/python3
    # -*- coding:UTF-8 -*-
    
    class OutAdvice(WeatherSearch):
        def __init__(self, input_daytime):
             WeatherSearch.__init__(self, input_daytime)
    
        def seach_temperature(self):
             vehicle = ''
             if self.input_daytime == 'daytime':
                 vehicle = 'bike'
             if self.input_daytime == 'night':
                 vehicle = 'taxi'
             return vehicle
    
    def out_advice(self):
        visible_leave = self.seach_visibility()
        if visible_leave == 2:
             print('The weather is good,suitable for use %s.' % self.seach_temperature())
        elif visible_leave == 9:
             print('The weather is bad,you should use %s.' % self.seach_temperature())
        else:
             print('The weather is beyond my scope,I can not give you any advice')
    

    程序调用如下:

    check = OutAdvice('daytime')
    check.out_advice()
    

    结果如下:

    The weather is good,suitable for use bike.

    调试

    在程序运行的任何时刻为对象添加属性都是合法的,不过应当避免让对象拥有相同的类型却有不同的属性组。
    在init方法中初始化对象的全部属性是一个好习惯,可以帮助你用户更好地管理类中的属性和对属性值的更改。
    继承会给调试带来新挑战,因为当你调用对象的方法时,可能无法知道调用的是哪一个方法。一旦无法确认程序的运行流程,最简单的解决办法是在适当位置添加一个输出语句,如在相关方法的开头或方法调用开始处等。

    相关文章

      网友评论

          本文标题:15 Python面向对象编程

          本文链接:https://www.haomeiwen.com/subject/qybhyctx.html