Python学习笔记 - 面向对象编程

前言

面向对象是 Python 最重要的特性,在 Python 中一切数据类型都是面向对象的。

Python 支持面向对象的三大特性:封装性、继承性和多态性,子类继承父类同样可以继承到父类的变量和方法。

面向对象编程是最有效的软件编写方法之一。在面向对象编程中,你编写表示现实世界中的事物和情景的类,并基于这些类来创建对象。编写类时,你定义一大类对象都有的通用属性(类的成员变量)和通用行为(类的成员方法)。基于类创建对象时,每个对象都自动具备这种通用属性和通用行为,然后可根据需要赋予每个对象独特的个性。

根据类来创建对象被称为实例化,这让你能够使用类的实例。

一、面向对象编程概述

面向对象编程的思想是:按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建的软件系统就存在什么样的实体。

例如,在真实世界的学校里,会有学生和老师等实体,学生有学号、姓名、所在班级等属性(数据),学生还有学习、提问、吃饭和走路等操作(方法)。学生只是抽象的描述,这个抽象的描述称为“”。在学校里活动的是学生个体,即张同学、李同学等,这些具体的个体称为“对象(object)”,对象也称为“类的实例(instance)”。

在现实世界中有类和对象,软件世界中也有类和对象,只不过它们会以某种计算机编程语言编写的程序代码形式存在,这就是面向对象编程(Object Oriented Programming, OOP)。

二、面向对象的三大特性

面向对象编程思想有三个基本特性:封装性、继承性和多态性。

2.1 封装性

        在现实世界中封装的例子比比皆是。例如,一台计算机内部极其复杂,有主板、CPU、硬盘和内存等,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU主频、硬盘和内存的大小,于是计算机制造商用机箱把计算机封装起来,对外提供一些接口,如鼠标、键盘和显示器等,这样当用户使用计算机时就变得非常方便。

        面向对象的封装和真实世界的目的是一样的。封装能够使外部访问者不能随意存取对象的内部数据,隐藏了对象的内部实现细节,只保留有限的对外接口。外部访问者不用关心对象的内部细节,操作对象变得简单。

2.2 继承性

        在现实世界中继承也是无处不在。例如轮船与客轮之间的关系,客轮是一种特殊的轮船,拥有轮船的全部特征和行为,即数据和操作。在面向对象编程中,轮船是一般的类,客轮是特殊的类,特殊类拥有一般类的全部数据和操作,称为特殊类继承一般类。一般类称为“父类”或“基类”,特殊类称为“子类”或“派生类”。为了统一,本文中一般类统称为“父类”,特殊类统称为“子类”。

2.3 多态性

多态性是指在父类中成员被子类继承后,可以具有不同的状态或表现的行为。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)

三、类和对象

Python 中的数据类型都是类,类是组成 Python 程序的基本要素,它封装了一类对象的数据(成员变量)和操作(成员方法)。

3.1 定义类

Python 语言中一个类的实现包括类定义和类体。类定义的语法格式如下:

class 类名 [ (父类) ]:
    类体

        其中,class 是声明类的关键字,“类名” 是用户自定义的类名,自定义类名首先应该是合法的标识符,且应该遵循 Python 命名规范,采用大驼峰命名法。“父类” 声明当前类继承的父类,父类可以省略声明,表示直接继承 Python 根类 object 类。

        例如,定义一个动物(Animal)类代码如下:

class Animal(object):
    # 类体
    pass

解析:上述代码声明了动物类,它继承了 object 类,object 类是所有类的根类,在 Python 中任何一个动物类都直接或间接继承 object 类,所以 (object) 部分代码可以省略不写。

<提示> 代码的 pass 语句什么操作都不执行,用来维持程序结构的完整。有些不想编写的代码,又不想有语法错误,可以使用 pass 语句占位。

3.2 创建和使用对象

        上面章节讲过,类的实例化可生成对象,所以 “对象” 也称为 “实例”。一个对象的生命周期包括三个阶段:创建、使用和销毁。销毁对象时 Python 的垃圾回收机制会自动回收不再使用的对象的内存空间,不需要程序员负责。程序员只需要关心对象的创建和使用。

        Python 创建对象很简单,就是在类后面加上一对小括号,表示调用类的构造方法,这就创建了一个对象,示例代码如下:

# 创建一个Animal类的对象
animal = Animal()

Animal 类是上一节定义的动物类,Animal() 表达式创建了一个动物类对象,并把创建的对象赋值给 animal 变量,animal 是指向动物类对象的一个引用。通过 animal 变量可以使用刚刚创建的动物类对象。如下代码打印出动物类对象相关信息。

# coding=utf-8

# 定义一个Animal类
class Animal(object):
    # 类体
    pass

animal = Animal()
print(animal)

运行结果:

> python test.py
<__main__.Animal object at 0x000001DDEF44A4A0>

解析:print() 函数打印对象会输出一些很难懂的信息。事实上,print() 函数调用了对象的 __str__() 方法输出字符串信息,__str__() 是 object 类的一个方法,它会返回有关该对象的描述信息,由于本例中 Animal 类的 __str__() 方法是默认实现的,所以会返回这些难懂的信息,如果要打印出友好的信息,需要在 Animal 类体中重写 __str__() 方法。

<提示> __str__() 这种双下划线开始和结尾的方法是 Python 保留的,有着特殊的含义,称为魔法方法

3.3 实例变量

        在类体中可以包含类的成员,类成员如下图 1 所示,其中包括成员变量、成员方法和属性。成员变量又分为实例变量和类变量,成员方法又分为实例方法、类方法和静态方法。

Python学习笔记 - 面向对象编程_第1张图片 图1  类成员

<提示> 在 Python 类成员中有 attributeproperty,见上图1。attribute 是类中保存数据的变量,如果需要对 attribute 进行封装,那么在类的外部为了访问这些 attribute,往往会提供一些 setter 和 getter 访问器。setter 访问器是对 attribute 赋值的方法,getter 访问器是取 attribute 值的方法,这些方法在创建和调用时都比较麻烦,于是 Python 又提供了 property,property 本质上就是 setter 和 getter 访问器,是一种方法。一般情况下,attribute 和 property 中文都翻译为 “属性”,这样很难区分两者的含义,也有很多书将 attribute 翻译为 “特性”。“属性” 和 “特性” 在中文中区别也不大。其实很多编程语言都有 attribute 和 property 概念,例如 Objective-C 中 attribute 称为成员变量(或字段),property 称为属性。本文也将采用 Objective-C 提法,将 attribute 翻译为 “成员变量”,而 property 翻译为 “属性”

“实例变量” 就是某个实例(或对象)特有的 “数据”,例如你家狗狗的年龄、性别和体重与邻居家狗狗的年龄、性别和体重是不同的。

Python 中定义实例变量的示例代码如下:

# coding=utf-8

# 定义一个Animal类
class Animal(object):        # --1
    """ 定义动物类 """
    def __init__(self, age, sex, weight): # --2
        self.age = age       # 初始化实例变量的年龄 --3
        self.sex = sex       # 初始化实例变量的性别
        self.weight = weight # 初始化实例变量的体重

animal = Animal(2, 'male', 10.0) # 创建一个实例变量animal,即一个Animal类对象
print('年龄: {0}'.format(animal.age)) # --4
print('性别: {0}'.format(animal.sex))
print('体重: {0}'.format(animal.weight))

解析:上述代码第 1 处是定义 Animal 动物类,代码第 2 处是构造方法,构造方法是用来创建和初始化实例变量的,有关构造方法下文会再详细介绍,这里不再赘述。构造方法中的 self 指向当前对象实例,在创建对象实例时会自动调用构造方法,并将当前对象实例的引用赋值给形参 self,从而使得 self 也指向当前对象实例,self 类似于C++中的 this 指针的作用。代码第 3 处是在创建和初始化实例变量 age,其中 self.age 表示当前对象实例的实例变量。代码第 4 处是访问 age 实例变量,实例变量需要通过 “实例名.实例变量” 的形式访问。

3.4 类变量

        “类变量”是所有实例(或对象)共有的变量。例如有一个 Account(银行账号)类,它有三个成员变量:amount(账户金额)、interest_rate(利率)和 owner(账户名)。在这三个成员变量中,amount 和 owner 会因人而异,对于不同的账户这些内容是不同的,而所有账户的 interest_rate 是相同的。amount 和 owner 成员变量与账户个体实例有关,称为 “实例变量”,interest_rate 成员变量与个体实例无关,或者说是所有账户实例共享的,这种成员变量称为 “类变量”。

        类变量示例代码如下:

# coding=utf-8

class Account:
    """ 定义银行账户类 """
    interest_rate = 0.0688  # 类变量利率 --1
    
    def __init__(self, owner, amount):
        self.owner = owner   # 定义实例变量账户名并初始化
        self.amount = amount # 定义实例变量账户金额

account = Account('Godlike', 2_000_000.0)

print('账户名: {0}'.format(account.owner))       # --2
print('账户金额: ${0}'.format(account.amount))
print('利率: {0}'.format(Account.interest_rate)) # --3

运行结果:

> python test.py
账户名: Godlike
账户金额: $2000000.0
利率: 0.0688

解析:代码第 1 处是创建并初始化类变量。创建类变量与实例变量不同,类变量要在方法之外定义。代码第 2 处是访问实例变量,通过 “实例名.实例变量” 的形式访问。代码第 3 处是访问类变量,通过 “类名.类变量” 的形式访问。“类名.类变量” 事实上是有别于包和模块的另一种形式的命名空间

<注意> 不要通过实例存取类变量数据。当通过实例读取变量时,Python 解释器会先从实例中找这个变量,如果没有找到再到类中去找;当通过实例为变量赋值时,无论类中是否有该同名变量,Python 解释器都会创建一个同名实例变量。

在上面的类变量示例中添加如下代码:

print('Account 利率: {0}'.format(Account.interest_rate))
print('account 利率: {0}'.format(account.interest_rate))  # --1

print('account 实例所有变量:{0}'.format(account.__dict__)) # --2
account.interest_rate = 0.01  # --3
account.interest_rate2 = 0.01 # --4
print('account 实例所有变量:{0}'.format(account.__dict__)) # --5

运行结果:

Account 利率: 0.0688
account 利率: 0.0688
account 实例所有变量:{'owner': 'Godlike', 'amount': 2000000.0}
account 实例所有变量:{'owner': 'Godlike', 'amount': 2000000.0, 'interest_rate': 0.01, 'interest_rate2': 0.01}

解析:(1)上述代码第 1 处通过实例 account 读取 interest_rate 变量,解释器发现 account 实例中没有该变量,然后会在 Account 类中找,如果类中也没有,会发生 AttributeError 错误。虽然通过实例读取 interest_rate 变量可以实现,但不符合 Python 设计规范。代码第 2 处打印出实例 account 所有的 “实例变量”,可以发现并不包括 “类变量”。

(2)代码第 3 处为 account.interest_rate 变量赋值,这样的操作下无论类中是否有同名类变量,都会创建一个新的实例变量。为了查看实例变量有哪些,可以通过 object 类提供的 __dict__ 变量查看,见代码第 2 处和第 5 处。从输出结果可见,代码第 3 处和代码第 4 处的赋值操作会导致新创建两个实例变量 interest_rate 和 interest_rate2。

<提示> 代码第 3 处和代码第 4 处能够在类之外创建实例变量,主要原因是 Python 的动态语言特性,Python 不能从语法层面禁止此事的发生。这样创建实例变量会引起很严重的问题,一方面,类的设计者无法控制一个类中有哪些成员变量;另一方面,这些实例变量无法通过类中的方法访问,因此,不建议这样做,最好是杜绝。

3.5 构造方法

        在 3.3 节和 3.4 节中都使用了 __init__() 方法,该方法用来创建和初始化实例变量,这种方法就是 “构造方法”。__init__() 方法也属于魔法方法。定义时它的第一个参数应该是 self,其后的参数才是用来初始化实例变量的。调用构造方法时不需要传入 self。

        构造方法示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0): # --1
        self.age = age       # 定义年龄实例变量
        self.sex = sex       # 定义性别实例变量
        self.weight = weight # 定义体重实例变量

a1 = Animal(8, 'male', 5.0)   # --2
a2 = Animal(16, 'male', 10.0)
a3 = Animal(32, 'female')     # --3

print('a1:[age:{0}, sex:{1}, weight:{2}]'.format(a1.age, a1.sex, a1.weight))
print('a2:[age:{0}, sex:{1}, weight:{2}]'.format(a2.age, a1.sex, a2.weight))
print('a3:[age:{0}, sex:{1}, weight:{2}]'.format(a3.age, a3.sex, a3.weight))

运行结果:

>python test.py
a1:[age:8, sex:male, weight:5.0]
a2:[age:16, sex:male, weight:10.0]
a3:[age:32, sex:female, weight:0.0]

解析:上述代码第 1 处是定义构造方法,其中除了第一个参数 self 外,其他的参数可以有默认值,这也提供了默认值的构造方法,能够给调用者提供多个不同形式的构造方法。代码第 2 处和第 3 处是调用构造方法创建 Animal 对象实例,其中不需要传入 self,只需要提供后面的三个实际参数即可。

3.6 实例方法

        实例方法与实例变量一样都是某个实例(或对象)个体特有的。方法是在类中定义的函数。而定义实例方法时它的第一个参数也应该是 self,这个过程是将当前实例与该方法绑定起来,使该方法称为实例方法。

        下面看一个定义实例方法示例。

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0):
        self.age = age       # 定义年龄实例变量
        self.sex = sex       # 定义性别实例变量
        self.weight = weight # 定义体重实例变量

    def eat(self):           # --1
        self.weight += 0.05
        print('eat...')

    def run(self):           # --2
        self.weight -= 0.01
        print('run...')

a1 = Animal(8, 'male', 10.0)
print('a1体重:{0:0.02f}'.format(a1.weight))
a1.eat()  # --3
print('a1体重:{0:0.02f}'.format(a1.weight))
a1.run()  # --4
print('a1体重:{0:0.02f}'.format(a1.weight))

运行结果:

>python test.py
a1体重:10.00
eat...
a1体重:10.05
run...
a1体重:10.04

解析:上述代码第 1 处和第 2 处声明了两个方法,其中第一个参数是 self。代码第 3 处和第 4 处是调用这些实例方法,注意在调用这两个方法时不需要传入 self 实参。

3.7 类方法

        “类方法” 与 “类变量” 类似也是属于类,不属于个体实例的方法,类方法不需要与实例绑定,但需要与类绑定,定义时它的第一个参数不是 self,而是类的 type 实例。type 是描述 Python 数据类型的类,Python 中所有数据类型都是 type 的一个实例

        定义类方法示例代码如下:

# coding=utf-8

class Account:
    """ 定义银行账户类 """
    interest_rate = 0.0688  # 类变量利率
    
    def __init__(self, owner, amount):
        self.owner = owner   # 定义实例变量账户名
        self.amount = amount # 定义实例变量账户金额

    # 定义类方法
    @classmethod
    def interest_by(cls, amt):            # --1
        return cls.interest_rate * amt    # --2

interest = Account.interest_by(200_000.0) # --3
print('计算利息: {0:0.04f}'.format(interest))

运行结果:

>python test.py
计算利息: 13760.0000

解析:(1)定义类方法有两个关键:第一,方法第一个参数 cls(见代码第1处)是 type 类型,是当前 Account 类型的实例;第二,方法使用装饰器 @classmethod 声明该方法是类方法。

<提示> 装饰器(Decorators) 是 Python 3.0 之后加入的新特性,以 @ 开头修饰函数、方法和类,用来修饰和约束它们,类似于 Java 中的注解。

(2)代码第2处是方法体,在类方法中可以访问其他的类变量和类方法,cls.interest_rate 是访问类变量 interest_rate。

<注意> 类方法可以访问类变量和其他类方法,但不能访问其他实例方法和实例变量。

(3)代码第3处是调用类方法 interest_by(),采用 “类名.类方法” 形式调用。从语法角度来说,也可以通过实例调用类方法,但这不符合编程规范,同样不建议这样做。

3.8 静态方法

如果定义的方法既不想与实例绑定,也不想与类绑定,只是想把类作为它的命名空间,那么可以定义静态方法。

定义静态方法示例代码如下:

# coding=utf-8

class Account:
    """ 定义银行账户类 """
    interest_rate = 0.0688  # 类变量利率
    
    def __init__(self, owner, amount):
        self.owner = owner   # 定义实例变量账户名
        self.amount = amount # 定义实例变量账户金额

    # 定义类方法
    @classmethod
    def interest_by(cls, amt):
        return cls.interest_rate * amt

    # 定义静态方法
    @staticmethod
    def interest_with(amt):             # --1
        return Account.interest_by(amt) # --2

interest1 = Account.interest_by(200_000.0)
print('计算利息1: {0:0.04f}'.format(interest1))
interest2 = Account.interest_with(200_000.0)      # --3
print('计算利息2: {0:0.04f}'.format(interest2))

account = Account('Godlike', 500_000.0)
interest3 = account.interest_with(account.amount) # --4
print('计算利息3: {0:0.04f}'.format(interest3))

运行结果:

>python test.py
计算利息1: 13760.0000
计算利息2: 13760.0000
计算利息3: 34400.0000

解析:上述代码第1处是定义静态方法,使用了 @staticmethod 装饰器,声明方法是静态方法,方法参数不指定 self 和 cls。代码第2处定义了类方法。定义静态方法与调用类方法类似,都是通过类名实现(见代码第3处),但也可以通过对象实例调用静态方法(见代码第4处)。

        类方法与静态方法在很多场景下是类似的,只是在定义时有一些区别。类方法需要绑定类,静态方法不需要绑定类,静态方法与类的耦合度更加松散。在一个类中定义静态方法只是为了提供一个基于类名的命名空间

四、封装性

封装性是面向对象编程的三大特性之一,Python 语言没有与封装性相关的关键字,它通过特定的名称实现变量和方法的封装。

4.1 私有变量

默认情况下 Python 中的变量是公有的,可以在类的外部访问它们。如果想让它们成为私有变量,可以在变量前加上双下划线 "__"。

示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0):
        self.age = age         # 定义年龄实例变量
        self.sex = sex         # 定义性别实例变量
        self.__weight = weight # 定义体重实例变量 --1

    def eat(self):
        self.__weight += 0.05
        print('eat...')

    def run(self):
        self.__weight -= 0.01
        print('run...')

a1 = Animal(2, 'male', 10.0)
print('a1体重: {0:0.02f}'.format(a1.weight)) # --2
a1.eat()
a1.run()

运行结果:

>python test.py
Traceback (most recent call last):
  File "F:\python_work\面向对象编程\test.py", line 19, in 
    print('a1体重: {0:0.02f}'.format(a1.weight))
AttributeError: 'Animal' object has no attribute 'weight'

错误信息: Animal类没有成员变量'weight'

解析:上述代码第1处在 weight 变量前加上双下划线,这会定义私有变量 __weight。__weight 变量在类内部访问没有问题,但是如果在类外部访问则会发生错误,见代码第2处。

<提示> Python 中并没有严格意义上的封装,所谓的私有变量只是形式上的限制。如果想在外部访问这些私有变量也是可以的,这些双下划线开头的私有变量只是换了一个名字,它们的命名规律为 "_类名__变量名",所以将上述代码 a1.weight 改成 a1._Animal__weight 就可以访问了,但这种访问方式并不符合规范,会破坏封装。可见,Python 的封装性靠的是程序员的自律,而非强制性的语法。

# print('a1体重: {0:0.02f}'.format(a1.weight)) # --2
print('a1体重: {0:0.02f}'.format(a1._Animal__weight)) # --3

将上述第2处的代码改成第3处的代码,就可以正确运行程序了,运行结果为:

>python test.py
a1体重: 10.00
eat...
run...

4.2 私有方法

私有方法与私有变量的封装是类似的,只要在方法前加上双下划线就是私有方法了。

示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0):
        self.age = age         # 定义年龄实例变量
        self.sex = sex         # 定义性别实例变量
        self.__weight = weight # 定义体重实例变量

    def eat(self):
        self.__weight += 0.05
        print('eat...')

    def __run(self):           # --1
        self.__weight -= 0.01
        print('run...')

a1 = Animal(2, 'male', 10.0)
a1.eat()
a1.run()  # --2

运行结果:

>python test.py
eat...
Traceback (most recent call last):
  File "F:\python_work\面向对象编程\test.py", line 20, in 
    a1.run()  # --1
AttributeError: 'Animal' object has no attribute 'run'

错误信息: Animal类没有成员方法'run'

解析:上述代码第1处中 __run() 方法是私有方法。__run() 方法可以在类的内部访问,不能在类的外部访问,否则会发生错误,见代码第2处。

<提示> 如果一定要在类的外部访问私有方法也是可以的。与私有变量访问类似,命名规律为 "_类名__方法名"。这也不符合规范,也会破坏类的封装性。

4.3 定义属性

        封装通常是对成员变量进行封装。在严格意义上的面向对象设计中,一个类是不应该有公有的实例成员变量的,这些实例成员变量应该被设计为私有的,然后通过公有的 setter 和 getter 访问器访问。

        私有 setter 和 getter 访问器的示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0):
        self.age = age         # 定义年龄实例变量
        self.sex = sex         # 定义性别实例变量
        self.__weight = weight # 定义体重实例变量

    def set_weight(self, weight):  # --1
        self.__weight = weight

    def get_weight(self):  # --2
        return self.__weight

a1 = Animal(2, 'male', 10.0)
print('a1体重: {0:0.2f}'.format(a1.get_weight())) # --3
a1.set_weight(123.45)
print('a1体重: {0:0.2f}'.format(a1.get_weight())) # --4

运行结果:

>python test.py
a1体重: 10.00
a1体重: 123.45

解析:上述代码第1处中的 set_weight() 方法是 setter 访问器,它有一个参数 weight 用来给私有成员变量 __weight 传递值。代码第2处的 get_weight() 方法是 getter 访问器。代码第3处是调用 getter 访问器。代码第4处是调用 setter 访问器。

        访问器形式的封装需要一个私有变量,需要提供 getter 访问器和一个 setter 访问器,只读变量不用提供 setter 访问器。总之,访问器形式的封装在编写代码时比较麻烦。为了解决这个问题,Python 中提供了属性(property),定义属性可以使用 @property @属性名.setter 装饰器,@property 用来修饰 getter 访问器,@属性名.setter 用来修饰 setter 访问器

        使用属性修改前面的示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex, weight = 0.0):
        self.age = age         # 定义年龄实例变量
        self.sex = sex         # 定义性别实例变量
        self.__weight = weight # 定义体重实例变量

    @property
    def weight(self):          # 替代 get_weight(self): --1
        return self.__weight

    @weight.setter
    def weight(self, weight):  # 替代 set_weight(self, weight): --2
        self.__weight = weight

a1 = Animal(2, 'male', 10.0)
print('a1体重: {0:0.2f}'.format(a1.weight)) # --3
a1.weight = 123.45  # a1.set_weight(123.45) # --4
print('a1体重: {0:0.2f}'.format(a1.weight))

运行结果:

程>python test.py
a1体重: 10.00
a1体重: 123.45

解析:(1)上述代码第1处是定义属性 getter 访问器,使用了 @property 装饰器进行修饰,方法名就是属性名,这样就可以通过属性取值了,见代码第3处。

(2)代码第2处是定义属性 setter 访问器,使用了 @weight.setter 装饰器进行修饰,weight 是属性名,与 getter 和 setter 访问器方法名保持一致,可以通过 a1.weight = 123.45 赋值,见代码第4处。

        从上述示例可见,属性本质上就是两个方法,在方法前加上装饰器使得方法成为属性(property)。属性使用起来类似于公有变量,可以在赋值符号 “=” 左边或右边,左边是被赋值,右边是取值。

<提示> 定义属性时应该先定义 getter 访问器,再定义 setter 访问器,即上述代码第1处和第2处不能颠倒,否则会出现错误。这是因为 @property 修饰 getter 访问器时,定义了 weight 属性,这样在后面使用 @weight.setter 装饰器才是合法的。

五、继承性

类的继承是面向对象编程语言的基本特性,多态性的前提是继承。

5.1 继承的概念

为了了解继承性,先看这样一个场景:一位面向对象的程序员小张,在编程过程中需要描述和处理个人信息,于是定义了类 Person,如下所示:

class Person:

    def __init__(self, name, age):
        self.name = name  # 名字
        self.age = age    # 年龄

    def info(self):
        template = 'Person [name={0}, age={1}]'
        s = template.format(self.name, self.age)
        return s

一周之后,小张又遇到了新的需求,需要描述和处理学生信息,于是他又定义了一个新的类 Student,如下所示:

class Student:

    def __init__(self, name, age, school):
        self.name = name     # 名字
        self.age = age       # 年龄
        self.school = school # 所在学校

    def info(self):
        template = 'Person [name={0}, age={1}, school={2}]'
        s = template.format(self.name, self.age, self.school)
        return s

        我们可能会认为小张的做法能够被理解并认为这是可行的,但问题在于 Student 和 Person 两个类的结构太接近了,后者只比前者多了一个 school 实例变量,却要重复定义其他所有的内容,实在让人 “不甘心”。Python 提供了解决类似问题的机制,那就是类的继承,代码如下所示:

class Student(Person):                     # --1

    def __init__(self, name, age, school): # --2
        super.__init__(name, age)          # --3
        self.school = school  # 所在学校      --4

解析:上述代码第 1 处是声明 Student 类继承自 Person 类,其中小括号中的是父类,如果没有指明父类(一对空的小括号或省略小括号),则默认父类为 object 类,object 类是 Python 的根类。代码第 2 处定义构造方法,子类中定义构造方法时首先要调用父类的构造方法,初始化父类实例变量。代码第 3 处 super.__init__(name, age) 语句是调用父类的构造方法,super() 函数是返回父类引用,通过它可以调用父类中的实例变量和方法。代码第 4 处是定义 school 实例变量,这个是子类新增的成员变量。

<提示> 子类继承父类时只是继承父类中公有的成员变量和方法,不能继承私有的成员变量和方法。

5.2 重写方法

如果子类方法名与父类方法名相同,而且参数列表也相同,只是方法体不同,那么子类重写(Override)了父类的方法。

示例代码如下:

# coding=utf-8

class Animal(object):
    """ 定义动物类 """
    def __init__(self, age, sex = 'male', weight = 0.0):
        self.age = age         # 定义年龄实例变量
        self.sex = sex         # 定义性别实例变量
        self.weight = weight   # 定义体重实例变量

    def eat(self):  # --1
        self.weight += 0.05
        print('Animal eat...')

class Dog(Animal):
    def eat(self):  # --2
        self.weight += 0.02
        print('Dog eat...')

a1 = Dog(2, 'female', 10.0)
a1.eat()

运行结果:

>python test.py
Dog eat...

解析:上述代码第 1 处是父类中定义的 eat() 实例方法,子类继承父类并重写了 eat() 方法,见代码第 2 处。那么通过子类实例调用 eat() 方法时,会调用子类重写的 eat() 方法。

5.3 多继承

        所谓多继承,就是一个子类有多个父类。大部分计算机编程语言如 Java、Swift 等,只支持单继承,不支持多继承。主要是多继承会发生方法冲突。例如,客轮是轮船也是交通工具,客轮的父类是轮船和交通工具,如果两个父类都定义了 run() 方法,子类客轮继承哪一个 run() 方法呢?

        Python 支持多继承,但 Python 给出了解决方法名冲突的方案。这个方案是:当子类实例调用一个方法时,先从子类中查找,如果没有找到则查找父类。父类的查找顺序是按照子类声明的父类列表从左到右查找,如果没有找到再找父类的父类,依次查找下去

        多继承示例代码如下:

# coding=utf-8

class ParentClass1:
    def run(self):
        print('ParentClass1 run...')

class ParentClass2:
    def run(self):
        print('ParentClass2 run...')

class SubClass1(ParentClass1, ParentClass2):
    pass

class SubClass2(ParentClass2, ParentClass1):
    pass

class SubClass3(ParentClass1, ParentClass2):
    def run(self):
        print('SubClass3 run...')

sub1 = SubClass1()
sub1.run()
sub2 = SubClass2()
sub2.run()
sub3 = SubClass3()
sub3.run()

运行结果:

>python test.py
ParentClass1 run...
ParentClass2 run...
SubClass3 run...

解析:上述代码中定义了两个父类 ParentClass1 和 ParentClass2,以及三个子类 SubClass1、SubClass2 和 SubClass3,这三个子类都继承了 ParentClass1 和 ParentClass2 两个父类。当子类 SubClass1 的实例 sub1 调用 run() 方法时,Python 解释器会先查找当前子类是否有 run() 方法,如果没有则到父类中查找,按照父类列表从左至右的顺序,找到 ParentClass1 中的 run() 方法,所以最后调用的是 ParentClass1  中的 run() 方法。按照这个规律,其他两个实例 sub2 和 sub3 调用哪一个 run() 就很容易知道了。

六、多态性

在面向对象程序设计中,多态是一个非常重要的特性,理解多态有利于进行面向对象的分析与设计。

6.1 多态概念

发生多态要有两个前提条件:第一、继承——多态发生一定是子类和父类之间;第二、重写——子类重写父类同名的方法

下面通过一个示例解释什么是多态。如下图 2 所示,父类 Figure (几何图形)有一个 draw(绘图)函数,Figure(几何图形)有两个子类 Ellipse(椭圆形)和 Triangle(三角形),Ellipse 和 Triangle 重写 draw() 方法。Ellipse 和 Triangle 都有 draw() 方法,但具体实现的方式不同。

Python学习笔记 - 面向对象编程_第2张图片 图2  几何图形类图

具体代码如下:

# coding=utf-8

# 几何图形类
class Figue:
    def draw(self):
        print('draw Figue...')

# 椭圆形类
class Ellipse(Figue):
    def draw(self):
        print('draw Ellipse...')

# 三角形类
class Triangle(Figue):
    def draw(self):
        print('draw Triangle...')

f1 = Figue()    # --1
f1.draw()

f2 = Ellipse()  # --2
f2.draw()

f3 = Triangle() # --3
f3.draw()

运行结果:

程>python test.py
draw Figue...
draw Ellipse...
draw Triangle...

解析:上述代码第2处和第3处符合多态的两个前提,因此会发生多态。而代码第1处不符合,没有发生多态。多态发生时,Python 解释器根据引用指向的实例调用它的方法。

<提示> 与 Java 等静态语言相比,多态性对于动态语言 Python 意义不大。多态性优势在于运行期动态特性。例如在 Java 中多态性是指,编译期声明变量是父类的类型,在运行期确定变量所引用的子类实例。而 Python 不需要声明变量的类型,没有编译,直接由解释器运行,运行期确定变量所引用的实例。

6.2 类型检查

无论多态性对 Python 影响多大,Python 作为面向对象的编程语言多态性是存在的,这一点可以通过运行期检查类型证实,运行期类型检查使用 isinstance(object, classinfo) 函数,它可以检查 object 实例是否由 classinfo 类或 classinfo 子类所创建的

在 6.1 节示例的基础上修改代码如下:

# coding=utf-8

# 集合图形类
class Figue:
    def draw(self):
        print('draw Figue...')

# 椭圆形类
class Ellipse(Figue):
    def draw(self):
        print('draw Ellipse...')

# 三角形类
class Triangle(Figue):
    def draw(self):
        print('draw Triangle...')

f1 = Figue()    # 没有发生多态
f1.draw()

f2 = Ellipse()  # 发生多态
f2.draw()

f3 = Triangle() # 发生多态
f3.draw()

print(isinstance(f1, Triangle)) # False --1
print(isinstance(f2, Triangle)) # False
print(isinstance(f3, Triangle)) # True
print(isinstance(f2, Ellipse))  # True  --2
print(isinstance(f2, Figue))    # True  --3

解析:上述代码第1处和第2、3处添加的代码,需要注意代码第3处的 isinstance(f2, Figue) 表达式是 True,f2 是 Ellipse 类创建的实例,而 Ellipse 是 Figure 类的子类,所以这个表达式返回 True,通过这样的表达式可以判断是否发生了多态。另外,还有一个类似于 isinstance(object, classinfo) 函数的 issubclass(class, classinfo) 函数,issubclass(class, classinfo) 函数用来检查 class 是否是 classinfo 的子类。示例代码如下:

print(issubclass(Ellipse, Triangle)) # False
print(issubclass(Ellipse, Figue))    # True
print(issubclass(Triangle, Ellipse)) # False

6.3 鸭子类型

        多态性对于动态语言意义不同,在动态语言中有一种类型检查称为 “鸭子类型”,即一只鸟走起来像鸭子,游起来像鸭子,叫起来也像鸭子,那它就可以被当做 “鸭子类型”。鸭子类型不关注变量的类型,而是关注变量具有的方法。鸭子类型像多态一样工作,但是没有继承,只是像 “鸭子” 一样的行为(方法)就可以了。

        鸭子类型示例代码如下:

# coding=utf-8

class Animal(object):
    def run(self):
        print('Animal run...')

class Dog(Animal):
    def run(self):
        print('Dog run...')

class Car:
    def run(self):
        print('Car run...')

def go(animal):  # 接收参数是Animal --1
    animal.run()

go(Animal()) # 传入一个Animal类实例
go(Dog())    # 传入一个Dog类实例
go(Car())    # 传入一个Car类实例 --2

运行结果:

>python test.py
Animal run...
Dog run...
Car run...
Python学习笔记 - 面向对象编程_第3张图片 图3  鸭子类型类图

解析:上述代码定义了三个类 Animal、Dog 和 Car,从代码和上图 3 所示可见,Dog 类继承了 Animal 类,而 Car 和 Animal 和 Dog 没有任何的关系,只是它们都有 run() 方法。代码第 1 处定义的 go() 函数设计时考虑接收 Animal 类型参数,但是由于 Python 解释器不做任何的类型检查,所以可以传入任何的实际参数。当代码第 2 处给 go() 函数传入 Car 类的实例时,它可以正常执行。这就是 “鸭子类型”。

        在 Python 这样的动态语言中使用 “鸭子类型” 替代多态性设计,能够充分地发挥 Python 动态语言特点,但是也给软件设计者带来了困难,对程序员的要求也非常高。

七、Python 根类 —— object 类

Python 所有类都直接或间接继承自 object 类,它是所有类的 “祖先”。object 类有很多方法,本节重点介绍如下两个方法:

  • __str__():返回该对象的字符串表示。
  • __eq__():指示其他某个对象是否与此对象 “相等”。

这些方法都是需要在子类中重写的,下面就详细解释一下它们的用法。

7.1 __str__() 方法

        为了日志输出等处理方便,所有的对象都可以输出自己的描述信息。为此,可以重写 __str__() 方法。如果没有重写 __str__() 方法,则默认返回对象的类名,以及内存地址等信息。

        下面看一个示例,在前面 5.1 节介绍过的 Person 类,重写它的 __str__() 方法代码如下:

# coding=utf-8

class Person:

    def __init__(self, name, age):
        self.name = name  # 名字
        self.age = age    # 年龄

    def __str__(self): # --1
        template = 'Person [name={0}, age={1}]'
        s = template.format(self.name, self.age)
        return s

person = Person('Godlike', 18)
print(person)          # --2

运行结果:

>python test.py
Person [name=Godlike, age=18]

解析:上述代码第 1 处覆盖 __str__() 方法,返回什么样的字符串完全是自定义的,只要能够表示当前类和当前对象即可,本例是将 Person 成员变量拼接成为一个字符串。代码第 2 处是打印 person 对象,print() 函数会将对象的 __str__() 方法返回字符串,并打印输出。

7.2 对象比较方法

        在《Python学习笔记 - Python运算符》这篇博文中介绍同一性测试运算符时,曾经介绍过内容相等比较运算符 “==”,== 用来比较两个对象的内容是否相等。当使用运算符 == 比较两个对象时,在对象的内部是通过 __eq__() 方法进行比较的。

        两个人(Person 对象)相等是指什么?是名字?是年龄?问题的关键是需要指定相等的规则,就是要指定比较的是哪些实例变量相等。所以为了比较两个 Person 对象是否相等,则需要重写 __eq__() 方法,在该方法中指定比较规则。

        修改 Person 代码如下:

# coding=utf-8

class Person:

    def __init__(self, name, age):
        self.name = name  # 名字
        self.age = age    # 年龄

    def __str__(self): # --1
        template = 'Person [name={0}, age={1}]'
        s = template.format(self.name, self.age)
        return s

    def __eq__(self, other):                                  # --1
        if self.name == other.name and self.age == other.age: # --2
            return True
        else:
            return False

person1 = Person('Godlike', 18)
person2 = Person('Godlike', 18)
person3 = Person('Godlike', 20)

print(person1 == person2) # True
print(person1 == person3) # False

解析:上述代码第 1 处重写了 Person 类 __eq__() 方法,代码第 2 处是提供比较规则,即只有是 name 和 age 都相等才认为两个对象相等。代码中创建了三个 Person 对象 person1、person2 和 person3,person1 和 person2 这两个对象具体相同的 name 和 age,所以 person1 == person2 为 True。而 person1 和 person3 这两个对象,成员变量 age 的值不同,所以 person1 == person3 为 False。

八、枚举类

        枚举是用来管理一组相关的有限个数常量的集合,使用枚举可以提高程序的可读性,使代码更清晰且更易于维护。在 Python 中提供枚举类型,本质上是一种类。

8.1 定义枚举类

在 Python 中定义枚举类的语法格式如下:

class 枚举类名 (enum.Enum):
    枚举常量列表

枚举类继承自 enum.Enum 类。枚举中会定义多个常量成员。枚举类 WeekDays 具体代码如下:

# coding=utf-8

import enum

class WeekDays(enum.Enum): # --1
    # 枚举常量列表
    MONDAY = 1             # --2
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 10            # --3

day = WeekDays.FRIDAY      # --4

print(day)       # WeekDays.FRIDAY
print(day.value) # 10
print(day.name)  # FRIDAY

运行结果:

>python test.py
WeekDays.FRIDAY
10
FRIDAY

解析:上述代码第 1 处是定义 WeekDays 枚举类吗,它有 5 个常量成员,每一个常量成员值都需要进行初始化,见代码第 2 处和第 3 处。代码第 4 处是实例化枚举类 WeekDays,该实例为 FRIDAY。注意枚举类实例化与一般类不同,枚举类不能调用构造方法。枚举实例 value 属性是返回枚举值, name 属性返回枚举名

<提示> 常量成员值可以是任意类型,多个成员的值也可以相同。

8.2 限制枚举类

        为了存储和使用方便,枚举类中的常量成员取值应该是整数,而且每一枚举常量应该有不同的取值。为了使枚举常量成员只能使用整数类型,可以使用 enum.IntEnum 作为枚举父类。为了防止常量成员值重复,可以为枚举类加上 @enum.unique 装饰器。具体示例代码如下:

# coding=utf-8

import enum

@enum.unique                  # --1
class WeekDays(enum.IntEnum): # --2
    # 枚举常量列表
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    # WEDNESDAY = 'Wed.'
    THURSDAY = 4
    FRIDAY = 5
    # FRIDAY = 1

day = WeekDays.FRIDAY

print(day)       # WeekDays.FRIDAY
print(day.value) # 5
print(day.name)  # FRIDAY

解析:上述代码第 1 处是 WeekDays 枚举类的装饰器。代码第 2 处是定义枚举类型 WeekDays,其父类是 enum.IntEnum。如果尝试将其中的成员值修改为其他数据类型值,或修改为相同值(见注释代码),则会发生异常。

ValueError: invalid literal for int() with base 10: 'Wed.'

8.3 使用枚举类

定义枚举类的主要目的是提高程序可读性,特别是在比较时,枚举类非常实用。示例代码如下:

# coding=utf-8

import enum

@enum.unique
class WeekDays(enum.IntEnum):
    # 枚举常量列表
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

day = WeekDays.SATURDAY

if day == WeekDays.MONDAY:     # --1
    print('Working')
elif day == WeekDays.SATURDAY: # --2
    print("Resting")

运行结果:

>python test.py
Resting

解析:上述代码第 1 处比较 day 是否为 星期一,代码第 2 处比较 day 是否为 星期六。从中可见,使用枚举成员要好于使用 1 和 5 这种无意义的值(这种无意义的值也称为魔鬼数字)。

九、总结

        本文主要介绍了 Python 面向对象编程基础知识,包括如何定义类,如何为类定义变量、方法,以及如何创建类的对象。Python 的类(class)其实就相当于类命名空间,在类中定义的方法就位于类命名空间内,因此使用类定义方法非常灵活,类不仅可以调用类方法、静态方法,也可以使用未绑定的方式调用实例方法。

        本文还详细介绍了 Python 面向对象的三大特征:封装、继承 和 多态,Python 通过双下划线的方式来隐藏类中的成员。本文也讲解了 Python 的多继承机制,需要注意多继承中可能容易出错的问题。Python 的多态则大大提高了 Python 编程的灵活性。

十、编程练习题

1、编写一个学生类,提供 name、age、gender、phone、address、email 等属性,为学生类提供带所有变量的构造器,为学生类提供方法,用于描述吃、喝、玩、睡等行为。

# coding=utf-8

class Student:
    '''定义学生类'''
    def __init__(self, name, age, gender, phone, address, email):
        '''定义构造方法'''
        self.name = name
        self.age = age
        self.gender = gender
        self.phone = phone
        self.address = address
        self.email = email

    def eat(self, food):
        '''定义吃的实例方法'''
        print('%s 正在吃 %s' %(self.name, food))

    def drink(self, drink):
        '''定义喝的实例方法'''
        print('%s 正在喝 %s' %(self.name, drink))

    def play(self, sport, other):
        '''定义玩的实例方法'''
        print('%s 今年 %d 岁, 正和 %s 玩 %s' %(self.name, self.age, other, sport))

    def sleep(self):
        '''定义睡的实例方法'''
        print('%s 正在 %s 睡觉' %(self.name, self.address))

    def __repr__(self): # 重写object类的 __repr__() 方法,该方法主要实现"自我描述"功能
        return ('Student(name=%s, age=%d, phone=%s, address=%s, email=%s)' %(self.name, self.age, self.phone, self.address, self.email))

if __name__ == '__main__':
    stu = Student('小明', 18, '男', '18812345678', 'Beijing`s home', '[email protected]')
    stu.eat('Bread')
    stu.drink('Milk')
    stu.play('Honor of Kings', '小李子')
    stu.sleep()

运行结果:

>python student.py
小明 正在吃 Bread
小明 正在喝 Milk
小明 今年 18 岁, 正和 小李子 玩 Honor of Kings
小明 正在 Beijing`s home 睡觉

2、利用第 1 题定义的 Student 类,定义一个列表保存多个 Student 对象作为通讯录数据。程序可通过 name、email、address 查询,如果找不到数据,则进行友好提示。

# coding=utf-8

from student import Student

contacts = [
    Student('Zhangsan', 18, 'Male', '18812345671', 'Beijing', '[email protected]'),
    Student('Lisi', 18, 'Male', '18812345672', 'Wuhan', '[email protected]'),
    Student('Wangwu', 18, 'Male', '18812345673', 'Shanghai', '[email protected]'),
    Student('Lihong', 18, 'Female', '18812345674', 'Shenzhen', '[email protected]')
]

def find_by_name(name):
    return [x for x in contacts if name in x.name] # 列表推导式

def find_by_email(email):
    return [x for x in contacts if email in x.email]

def find_by_address(address):
    return [x for x in contacts if address in x.address]

if __name__ == '__main__':
    while True:
        t = input('请输入查找的方式,名字(n), Email(e),地址(a): ')
        k = input('请输入查找的关键字: ')
        if t == 'n':
            print(find_by_name(k))
        elif t == 'e':
            print(find_by_email(k))
        elif t == 'a':
            print(find_by_address(k))
        else:
            print('输入有误!')
        print() # 换行

3、定义代表二维坐标系上某个点的 Point 类(包括 x、y 两个属性),为该类提供一个方法用于计算两个 Point 之间的距离,再提供一个方法用于判断三个 Point 组成的三角形是钝角、锐角还是直角三角形。

# coding=utf-8

import math

class Point(object):
    '''定义Point类'''
    def __init__(self, x, y):
        '''定义构造方法'''
        self.x = x
        self.y = y

    def distance(self, other):
        '''计算坐标系两点之间的距离'''
        return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)

    def judge_triangle(self, p1, p2):
        '''判断3个坐标点组成的三角形'''
        self_p1 = self.distance(p1)
        self_p2 = self.distance(p2)
        p1_p2 = p1.distance(p2)
        # 如果self_p1长度最大
        if self_p1 > self_p2 and self_p1 > p1_p2:
            if self_p1 > (self_p2 + p1_p2):
                print('不是三角形')
            else:
                if (self_p1 ** 2) > (self_p2 ** 2 + p1_p2 ** 2):
                    print('钝角三角形')
                elif (self_p1 ** 2) == (self_p2 ** 2 + p1_p2 ** 2):
                    print('直角三角形')
                else:
                    print('锐角三角形')
        # 如果self_p2长度最大
        if self_p2 > self_p1 and self_p2 > p1_p2:
            if self_p2 > (self_p1 + p1_p2):
                print('不是三角形')
            else:
                if (self_p2 ** 2) > (self_p1 ** 2 + p1_p2 ** 2):
                    print('钝角三角形')
                elif (self_p2 ** 2) == (self_p1 ** 2 + p1_p2 ** 2):
                    print('直角三角形')
                else:
                    print('锐角三角形')
        # 如果p1_p2长度最大
        if p1_p2 > self_p1 and p1_p2 > self_p2:
            if p1_p2 > (self_p2 + p1_p2):
                print('不是三角形')
            else:
                if (p1_p2 ** 2) > (self_p1 ** 2 + self_p2 ** 2):
                    print('钝角三角形')
                elif (p1_p2 ** 2) == (self_p1 ** 2 + self_p2 ** 2):
                    print('直角三角形')
                else:
                    print('锐角三角形')

    def __repr__(self):
        return ('Point(x=%s, y=%s)' %(self.x, self.y))

if __name__ == '__main__':
    pt = Point(1, 1)
    print(pt.distance(Point(2, 3)))
    pt.judge_triangle(Point(4, 1), Point(5, 5))

运行结果:

2.23606797749979
钝角三角形

4、定义代表三维笛卡尔坐标系上某个点 Points 类(包括 x、y、z 三个属性),为该类定义一个方法,可接收 b、c、d 三个参数,用于计算当前点a、b、c 组成的面与 b、c、d 组成的面之间的夹角。

提示:cos(夹角) = (X▪Y) / |X||Y|,其中 X = AB × BC,Y = BC × CD,X▪Y 代表 X 与 Y 的点积,AB × BC 代表 AB 与 BC 的叉乘。

Python学习笔记 - 面向对象编程_第4张图片 图4  向量的点积与叉积
# coding=utf-8

import math

class Points(object):
    '''定义Point类'''
    def __init__(self, x, y, z):
        '''定义构造方法'''
        self.x = x
        self.y = y
        self.z = z

    def __sub__(self, p):
        '''为减法提供支持的方法'''
        return Points((self.x - p.x), (self.y - p.y), (self.z - p.z))

    def dot(self, p):
        '''点积'''
        return (self.x + p.x) + (self.y + p.y) + (self.z + p.z)

    def cross(self, p):
        '''叉积'''
        return Points((self.y * p.z - self.z * p.y), (self.z * p.x - self.x * p.z), (self.x * p.y - self.y * p.x))

    def absolute(self):
        '''绝对值'''
        return math.sqrt((self.x ** 2 + self.y ** 2 + self.z ** 2))

if __name__ == '__main__':
    points = list()
    print('请依次输入4个点的x y z(中间以空格隔开):')
    for i in range(4):
        a = list(map(float, input().split())) # 将输入的数字字符串转换成浮点数,map函数返回的是迭代器
                                              # 最后使用list()函数转换为列表
        points.append(a)                      # 将列表a追加到列表points中

    a, b, c, d = Points(*points[0]), Points(*points[1]), Points(*points[2]), Points(*points[3])
    X = (b - a).cross(c - b)
    Y = (c - b).cross(d - c)
    angle = math.acos((X.dot(Y)) / (X.absolute() * Y.absolute()))

    print("degrees=%.2f" %math.degrees(angle)) # 将角度angle从弧度转换为度数

示例运行结果:

>python test.py
请依次输入4个点的x y z(中间以空格隔开):
1 0 0
0 1 0
0 0 1
2 2 2
degrees=55.94

5、定义交通工具、汽车、火车、飞机这些类,注意它们的继承关系,为这些类提供构造方法。

# coding=utf-8

class Transport:
    def move(self, distance):
        print('我移动了 %s 千米' % distance)

class Car(Transport):
    def __init__(self, name, speed):
        self.name = name
        self.speed = speed

    def move(self, distance):
        print('%s 汽车以时速 %skm/h 在公路上开了 %s 千米' % (self.name, self.speed, distance))

class Train(Transport):
    def __init__(self, speed):
        self.speed = speed

    def move(self, distance):
        print('高铁以时速 %skm/h 在铁轨上走了 %s 千米' %(self.speed, distance))

class Plain(Transport):
    def __init__(self, speed):
        self.speed = speed

    def fly(self, distance):
        print('飞机以时速 %skm/h 在天空飞了 %s 千米' % (self.speed, distance))

if __name__ == '__main__':
    car = Car('BYD', 80)
    car.move(50)

    train = Train(350)
    train.move(1080)

    plain = Plain(950)
    plain.fly(3000)

运行结果:

BYD 汽车以时速 80km/h 在公路上开了 50 千米
高铁以时速 350km/h 在铁轨上走了 1080 千米
飞机以时速 950km/h 在天空飞了 3000 千米

参考

《Python从小白到大牛(第1版-2018).pdf》第11章 - 面向对象编程

《疯狂Python讲义(2018.12).pdf》第3章 - 类和对象

你可能感兴趣的:(#,Python学习笔记,Python,面向对象编程,类和对象)