深入了解Python的类与模块化

发布于 2022年 05月 06日 09:23

学习目标

Python 是简洁、易学、面向对象的编程语言。它不仅拥有强大的原生数据类型,也提供了简单易用的控制语句。本节的主要目标是介绍 Python 中的面向对象编程范式以及模块化思想,为接下来的学习奠定基础,本文会完整的介绍学习数据结构和算法所需的 Python 基础知识及基本思想,并给出相应的实战示例及解释。

掌握 Python 面向对象编程的基本概念,并会编写 Python 自定义类

掌握 Python 模块化的编程思想

1. 面向对象编程:类

1.1 面向对象编程的基本概念

一个完善的程序是由数据和指令组成的。过程式编程利用“分而治之”的思想,使用函数对数据进行处理,数据与函数之前的关系是松散的,即同样的数据可以被程序中的所有函数访问,而一个函数也可以访问程序中的不同数据。这导致了,如果出现异常,需要在整个系统中查找错误代码。

为了解决这一问题,面向对象编程 (Object Oriented Programming, OOP) 将系统划分为不同对象,每个对象包含自身的信息数据以及操作这些数据的方法。例如,每个字符串对象具有字符数据,同时还具有改变大小写、查找等方法。

面向对象编程使用类描述其所包含的所有对象的共同特性(属性),即数据属性(也称数据成员或成员变量)和功能属性(也称成员函数或方法)。

一个类的对象也称为这个类的一个实例。例如,32 就是一个整数类 int 的对象,可以使用函数 type() 来获取对象所属类:

1
2
>>> type(32)
<class 'int'>

可以利用内置函数 dir() 查询类的属性:

1
2
>>> dir(32)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

在继续讲解之前,我们首先来快速介绍下面向对象的三大特性——多态、封装和继承。

1.1.1 多态

多态:可对不同类型的对象执行相同的操作。

多态其实很常见,例如列表对象和元组对象都具有 count 方法,使用变量调用 count 方法时,我们无需知道它究竟是列表还是元组,这就是多态的作用。这不仅仅适用于方法,许多内置运算符和函数也使用了多态:

1
2
3
4
5
6
7
8
>>> [1,3,3,3].count(3)
3
>>> (1,3,3,3).count(3)
3
>>> [1,3]+[1,3]
[1, 3, 1, 3]
>>> 'hello' + ' world!'
'hello world!'

1.1.2 封装

封装:对外部隐藏有关对象具体操作的细节。

封装与多态类似,都属于抽象原则,都用于处理程序的组成部分而无需关心不必要的细节,但不同的是,多态使我们无需知道对象所属的类就能调用其方法,而封装使我们无需知道对象的内部构造就能使用它。例如我们将虚数的实部和虚部作为对象的数据属性,就是将对象的属性“封装”在对象中。

1.1.3 继承

继承:用于建立类的层次结构,基于上层的类创建出新类。

如果我们有了一些类,再创建新的类时发现与已存在的类十分相似,只需要添加一些新方法,那么我们可能不想复制旧类的代码至新类中,这时我们就要用到继承了。例如,我们有了一个 Fruit 类,具有描述外观的方法 show_shape,如果想要新建一个 Apple 类,除了描述外观外,我们还想知道如何计算总价,那么我们就可以让 Apple 类继承 Fruit 的方法,使得对 Apple 对象调用方法 show_shape 时,将自动调用 Fruit 类的这个方法。

1.2 自定义类

我们已经知道抽象数据类型就是一个由对象以及对象上的操作组成的集合,对象和操作被捆绑为一个整体,不但可以使用对象的数据属性,还可以使用对象上的操作。操作(在类中称为方法)定义了抽象数据类型和程序其他部分之间的接口。接口定义了操作要做什么,但没有说明如何做,因此我们可以说抽象的根本目标是隐藏操作的细节。而类就是为了实现数据抽象类型。

Python 用关键字 class 定义一个类,格式如下,其中方法定义与函数定义语法类似:

class 类名:

方法定义

接下来构建实现抽象数据类型 Imaginary (虚数)的类,用于展示如何实现自定义类。

定义类时首先需要提供构造方法,构造方法定义了数据对象的创建方式。要创建一个 Imaginary 对象,需要提供实部和虚部两部分数据,Python 中,__init__() 作为构造方法名:

1
2
3
4
class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag

形式参数列表的第一项是一个指向对象本身的特殊参数(习惯上通常使用 self ),在调用时不需要提供相应的实际参数,而构造方法中的剩余参数必须提供相应的实参,使得新创建的对象能够知道其初始值,与函数定义一样,可以通过默认值为形参提供默认实参。如在 Imaginary 类中,self.real 定义了 Imaginary 对象有一个名为 real 的内部数据对象作为其实部数据属性,而self.imag 则定义了虚部。

创建 Imaginary 类的实例时,会调用类中定义的构造方法,使用类名并且传入数据属性的实际值完成调用:

1
2
3
>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a
<__main__.Imaginary object at 0x0000020CF1B80160>

以上代码创建了一个对象,名为 imaginary_a,值为 6+6i,这就是封装的示例,将数据属性和操作数据属性的方法打包在对象中。

除了实例化外,类还支持另一操作:属性引用(包括数据属性和功能属性),通过点标记法访问与类关联的属性:

1
2
3
4
>>> imaginary_a.real
6
>>> imaginary_a.imag
6

除了数据属性外,还需要实现抽象数据类型所需要的方法(功能属性),需要牢记的是,方法的第一个参数 self 是必不可少的,例如要实现打印实例化的虚数对象,编写类方法 display()

1
2
3
4
5
6
7
class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
     
    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))

调用类方法打印实例化的虚数对象:

1
2
3
4
5
>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a.display()
6+6i
>>> print(imaginary_a)
<__main__.Imaginary object at 0x0000020CF1B72D90>

可以看到,如果使用 print() 函数只能打印存储在变量中的地址,这是由于将对象转换成字符串的方法 __str__() 的默认实现是返回实例的地址字符串,如果想要使用 print 函数打印对象,需要重写默认的 __str__() 方法,或者说重载该方法:

1
2
3
4
5
>>> imaginary_a = Imaginary(6, 6)
>>> imaginary_a.display()
6+6i
>>> print(imaginary_a)
<__main__.Imaginary object at 0x0000020CF1B72D90>

此时如果再次使用 print() 函数,就可以直接打印对象了:

1
2
3
>>> imaginary_a = Imaginary(6, 6)
>>> print(imaginary_a)
6+6i

可以重载类中的很多方法,最常见的是重载运算符,这是由于人们习惯使用熟悉的运算符对数据进行运算,这要比使用函数对数据进行运算更加直观且易于理解,如表达式:8 + 6 / 3,如果用函数则为:add(8, div(6, 3)),显然前者比后者更加符合习惯。 如果某种类型的对象要使用常见运算符,就必须对这种类型重新定义相应的运算符函数,例如,Python 对于 int 整型、float 浮点型、str 字符串类型等都重新定义了乘法运算符函数,对一个类型重新定义运算符函数的也称“运算符重载”。我们可以编写 Imaginary 类的 __mul__() 方法重载乘法运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
 
    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))
         
    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
     
    def __mul__(self, other):
        new_real = self.real * other.real - self.imag * other.imag
        new_imag = self.real * other.imag + self.imag * other.real
        return Imaginary(new_real, new_imag)

还可以重载其他运算符,如比较运算符 ==,即 __eq__() 方法,重载 Imaginary 类的 __eq__() 方法允许两个虚数进行比较,查看它们的值是否相等,这也称为深相等;而根据引用进行判断的浅相等,只有两个变量是同一个对象的引用时才相等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# shallow_and_deep_equal.py
class ImaginaryFirst:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
 
    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))
 
    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
         
class Imaginary:
    def __init__(self, real, imag):
        self.real = real
        self.imag = imag
 
    def display(self):
        print('{}{:+}i'.format(self.real, self.imag))
 
    def __str__(self):
        print('{}{:+}i'.format(self.real, self.imag))
     
    def __eq__(self, other):
        return self.real == other.real and self.imag == self.imag
 
print('浅相等:只有两个变量是同一个对象的引用时才相等。')
imag_1 = imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)
imag_1 = ImaginaryFirst(6, 6)
imag_2 = ImaginaryFirst(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)
 
print('深相等:两个变量的值相等即表示对象相等。')
imag_1 = imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)
imag_1 = Imaginary(6, 6)
imag_2 = Imaginary(6, 6)
print('imag_1 == imag_2  ', imag_1 == imag_2)

程序运行结果如下所示:

浅相等:只有两个变量是同一个对象的引用时才相等。

imag_1 == imag_2   True

imag_1 == imag_2   False

深相等:两个变量的值相等即表示对象相等。

imag_1 == imag_2   True

imag_1 == imag_2   True

1.3 再谈继承

1.3.1 继承实例

继承可以建立一组彼此相关的抽象,能够建立一个类的层次结构,这样的关系结构也称为继承层次结构,每个类都可以从上层的类继承属性。在 Python 中,object 类位于最顶层。

下层的新类会继承已有类的属性,同时添加自己特有的一些属性,这个新的类就称为“派生类”或“子类”,而原有的类称为“基类”、“父类”或“超类”。利用父类定义子类,需要在定义的类名后添加圆括号,圆括号内写入父类名。如果没有显式地说明一个类的父类,则默认其父类为 object

例如三角形,包括锐角三角形、直角三角形和钝角三角形,因此定义类时,除了定义一般三角形的类 Triangle 外,还可以定义锐角三角形的类 AcuteTriangle 等,这时我们就可以令 AcuteTriangle 类继承 Triangle 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Triangle:
    def __init__(self, edge_1, edge_2, edge_3):
        self.edge_1 = edge_1
        self.edge_2 = edge_2
        self.edge_3 = edge_3
     
    def __str__(self):
        return str((self.edge_1, self.edge_2, self.edge_3))
     
    def print_info(self):
        print('The three sides of a triangle are {}, {} and {}'.format(self.edge_1, self.edge_2, self.edge_3))
     
    def perimeter(self):
        return self.edge_1 + self.edge_2 + self.edge_3
         
class AcuteTriangle(Triangle):
    def __init__(self, edge_1, edge_2, edge_3, max_angle):
        # 使用父类构造函数进行初始化
        Triangle.__init__(self, edge_1, edge_2, edge_3)
        self.max_angle = max_angle
     
    def print_info(self):
        Triangle.print_info(self)
        print('The max angle is {}'.format(self.max_angle))
     
    def get_max_angle(self):
        return self.max_angle

可以看到子类除了继承外,还可以:

  • 添加新属性,例如子类 AcuteTriangle 中新增了数据属性 max_angle 以及方法属性 get_max_angle
  • 替换(覆盖)父类中的属性,例如AcuteTriangle覆盖了父类的__init__()和 print_info() 方法。以 AcuteTriangle.__init__() 方法为例,首先调用 Triangle.__init__() 初始化被继承的实例变量 self.edge_1,self.edge_2,self.edge_3, 然后初始化 self.max_angle,这个实例变量只在 AcuteTriangle 实例中才有,而Triangle 实例中没有。
1
2
3
4
5
6
7
>>> triangle_a = Triangle(3, 4, 6)
>>> triangle_a.print_info()
The three sides of a triangle are 3, 4 and 6
>>> triangle_b = AcuteTriangle(3, 3, 3, 60)
>>> triangle_b.print_info()
The three sides of a triangle are 3, 3 and 3
The max angle is 60

在子类中可以通过 super() 方法来调用父类的方法,这种方法可以省略父类名:

1
2
3
4
5
6
7
8
9
10
11
12
class AcuteTriangle(Triangle):
    def __init__(self, edge_1, edge_2, edge_3, max_angle):
        # 使用父类构造函数进行初始化
        super().__init__(self, edge_1, edge_2, edge_3)
        self.max_angle = max_angle
     
    def print_info(self):
        super().print_info(self)
        print('The max angle is {}'.format(self.max_angle))
     
    def get_max_angle(self):
        return self.max_angle

使用内置函数 isinstance() 可以检查一个对象是否是某个类的实例(对象),而要确定一个类是否是另一个类的子类,则可以使用内置方法 issubclass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> triangle_a = Triangle(3, 4, 6)
>>> triangle_b = AcuteTriangle(3, 3, 3, 60)
>>> print(isinstance(triangle_a, Triangle))
True
>>> print(isinstance(triangle_a, AcuteTriangle))
False
>>> print(isinstance(triangle_b, AcuteTriangle))
True
>>> print(isinstance(triangle_b, Triangle))
True
>>> print(issubclass(AcuteTriangle, Triangle))
True
>>> print(issubclass(Triangle, AcuteTriangle))
False

因为类 AcuteTriangle 是从类 Triangle 派生出来的,所以一个类 AcuteTriangle 对象当然也是一个类 Triangle 对象,正如“一个锐角三角形也是一个三角形”。

1.3.2 多继承

一个类可以继承多个类的特性,这也称为多继承,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RightTriangle(Triangle):
    def area(self):
        return self.edge_1 * self.edge_2 * 0.5
 
    def print_name(self):
        print('This is a right triangle!')
 
class IsoscelesTriangle(Triangle):
    def print_name(self):
        print('This is an isosceles triangle!')
 
class IsoscelesRightTriangle(RightTriangle, IsoscelesTriangle):
    pass

以上示例中,pass 语句不做任何事,其作用相当于占位符,以等待后续补充代码;也可以用于语法上需要语句而实际不需要做任何工作的地方。

2. 模块

我们已经知道,函数和类都是可以重复调用的代码块。在程序中使用位于不同文件的代码块的方法是:导入 (import) 该对象所在的模块 (mudule)。

在之前的示例中,我们总是使用 shell,或假设整个程序保存在一个文件中,这在程序比较小时可能没有什么问题。但程序变得越来越大时,将程序的不同部分根据不同分类方法保存在不同文件中通常会更加方便。

2.1 导入模块

Python 模块允许我们方便地使用多个文件中的代码来构建程序。模块就是一个包含 Python 定义和语句的 .py 文件。

例如我们创建一个 hello_world.py 文件,就可以理解为创建了一个名为 hello_world 的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# hello_world.py
def print_hello():
    print('Hello World!')
 
class Triangle:
    def __init__(self, edge_1, edge_2, edge_3):
        self.edge_1 = edge_1
        self.edge_2 = edge_2
        self.edge_3 = edge_3
     
    def __str__(self):
        return str((self.edge_1, self.edge_2, self.edge_3))
     
    def print_info(self):
        print('The three sides of a triangle are {}, {} and {}'.format(self.edge_1, self.edge_2, self.edge_3))
     
    def perimeter(self):
        return self.edge_1 + self.edge_2 + self.edge_3

可将模块视为扩展,要导入模块,需要使用关键字 import,导入模块的一般格式如下:

1
import module_1[, module_2....]  # 可以同时导入多个模块

例如在 test.py 文件要导入 hello_world 模块:

1
import hello_world

导入的模块只要说明模块名即可,不需要也不能带有文件扩展名 .py。如果要使用模块中的对象,如函数、类等,需要用使用句点运算符 (.),即使用“模块名.对象”进行访问。例如,使用 hello_worl.Triangle 访问模块 hello_world 中的类 Triangle:

1
2
3
4
5
# test_1.py
import hello_world
hello_world.print_hello()
tri_a = hello_world.Triangle(3, 4, 5)
print(tri_a)

程序输出如下所示:

Hello World!

(3, 4, 5)

需要注意的是,导入的模块要位于相同的目录层次下,否则需要添加目录结构,例如,如果 hello_world 位于子目录 module 下,则需要使用如下方式:

1
2
3
# test_2.py
import module.hello_world
module.test.print_hello()

程序输出如下所示:

Hello World!

2.2 导入Python标准模块

Python 提供了许多标准模块,这些模块文件位于 Python 安装目录的 lib 文件夹中。可以像导入自己编写的模块一样导入标准模块,例如导入 math 模块,使用其中的对象:

1
2
3
4
# test_3.py
import math
print('sqrt(4) = ', math.sqrt(4))
print('sin(π/6) = ', math.sin(math.pi /6))

程序输出如下所示:

sqrt(4) = 2.0

sin(π/6) = 0.49999999999999994

这里可能大家会有一个疑问,这里导入的模块和当前文件并不在同一目录下,为什么不需要使用模块路径?这个问题也可以转换为——当我们使用 import 语句的时候,Python 解释器是怎样找到对应的文件的呢?

这就涉及到 Python 的搜索路径,搜索路径是由一系列目录名组成的,Python 解释器就依次从这些目录中去寻找所引入的模块。搜索路径被存储在 sys 模块中的 path 变量中:

1
2
3
>>> import sys
>>> sys.path
['', 'D:\\Program Files\\Python39\\python39.zip', 'D:\\Program Files\\Python39\\DLLs', 'D:\\Program Files\\Python39\\lib', 'D:\\Program Files\\Python39', 'D:\\Program Files\\Python39\\lib\\site-packages'

2.3 单独导入模块中所需对象

我们可能不想每次调用模块中的对象时都指定模块名,这时,我们可以使用 from module import object,从模块中单独导入所需对象,同时使用这个单独导入的对象时就不需要在前面添加“模块名.”前缀了:

1
2
3
4
# test_4.py
from math import pi, sin
print('sqrt(4) = ', sqrt(4))
print('sin(π/6) = ', sin(math.pi /6))

2.4 导入模块中的所有对象

可以通过 from module import *导入模块中的所有对象,同样不再需要模块名前缀:

1
2
3
4
# test_5.py
from math import *
print('sqrt(4) = ', sqrt(4))
print('sin(π/6) = ', sin(math.pi /6))

不同程序代码中不可避免地可能会使用了同一个名字来命名不同对象,这时就会引起冲突,但如果这些名字属于不同的模块,就可以通过模块名来区分它们,因此为了避免名字冲突,应尽量避免使用 from module import object 或 from module import * 导入对象。

2.5 重命名导入模块或对象

另一种避免名字冲突的方法是重命名导入模块或对象:

1
2
3
4
5
6
# test_6.py
import math as m
from datetime import date as d
print(d.today())
print('sqrt(4) = ', m.sqrt(4))
print('sin(π/6) = ', m.sin(math.pi /6))

程序输出如下所示:

datetime.date(2021, 12, 3)

sqrt(4) = 2.0

sin(π/6) = 0.49999999999999994

可以看到附加的好处是可以使用简写,减少编码工作量。

2.6 导入第三方模块

除了标准库外,Python 也具有规模庞大的第三方库,覆盖了信息技术几乎所有领域,这也是 Python 的其中一个巨大优势。下面以常用可视化库 matplotlib 为例介绍第三方库的使用。和标准库不同,使用第三方库首先要进行安装,在 shell 命令中使用 pip 命令可以快速安装所需库:

1
pip install matplotlib

安装完成后,使用第三方库就和标准库没有任何差别了:

1
2
3
4
5
6
7
8
# cos_1.py
import math
from matplotlib import pyplot as plt
scale = range(100)
x = [(2 * math.pi * i) / len(scale) for i in scale]
y = [math.cos(i) for i in x]
plt.plot(x, y)
plt.show()

以上就是深入了解Python的类与模块化的详细内容,更多关于Python 类 模块化的资料请关注脚本之家其它相关文章!

推荐文章