请使用最新版本浏览器访问此演示文稿以获得更好体验。
在本章以前,我们在编程时都将程序看作是一系列函数的集合,通过调用函数来进行流程控制,或者直接对电脑下达指令,这种传统的编程范式被称为面向过程编程。
面向对象程序设计(Object-oriented programming,OOP)是另一种具有对象(object)概念的编程范式,同时也是一种程序开发的抽象方针。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。对象指的是类(class)的实例,它可能包含数据、属性、代码与方法。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。
面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。面向对象程序设计提高了程序的灵活性和可维护性,在大型项目设计中被广为应用。
用史书来类比程序设计的范式:面向过程程序设计就像是编年体史书,而面向对象程序设计则像是纪传体史书。前者按事情的发展过程叙述,后者围绕特定的主角来叙述。
问题:洗衣机里有脏衣服,怎么洗干净?
(1)面向过程的解决方法:
(2)面向对象的解决方法:
执行:
例如,整数可以看作是一个类,而 33 则可以被看作是一个对象。
再如,一只名字叫阿黄的小狗就可以看作是一个对象,它是犬类的一个实例。
同函数定义(def
语句)类似,类的定义通过 class
语句实现:
class ClassName:
"""类的文档字符串""" # 类的帮助信息
statement # 类体
ClassName
用于指定类名,一般使用“驼峰式命名法”;"""类的文档字符串"""
可以通过 ClassName.__doc__
查看;statement
主要由类变量(或称类成员)、方法和属性等定义语句组成。示例:
class Student:
"""所有学生的基类"""
count = 0 # 类变量,表示学生总数
def __init__(self, id, name): # 特殊的构造方法
self.id = id
self.name = name
self.scores = {}
Student.count += 1
def set_score(self, course, score): # 方法
self.scores[course] = score
上页代码中:
count
变量是一个类变量,又称类属性,类变量定义在类中且在函数体之外,它的值将在这个类的所有实例之间共享。类属性可以通过类名称(如 Student.count
)或实例名访问。__init__
和 set_score
这些在类中定义的函数,称为方法,方法是从属于对象的函数。self
是方法的一个特殊参数,代表类的实例,它在方法的参数列表中首先给出,但在调用时不必传入相应的参数。id
、name
和 scores
这种使用 self.name
形式赋值的变量称为实例变量,又称实例属性,一般应该在 __init__()
函数中定义所有实例变量。可以通过属性引用的方式(obj.name
)访问类变量,如下所示:
print(Student.count) # 0
定义好的类相当于一栋房屋的设计图,它告诉我们房屋的模型,但本身不是房屋。要创建真实的房屋,需要把类实例化。
类的实例化使用函数表示法。如下所示:
class MyClass: # 定义 MyClass 类
pass
obj1 = MyClass() # MyClass 类的实例化
obj2 = MyClass() # MyClass 类的实例化
obj1 = MyClass()
创建类 MyClass
的新实例并将其赋值给变量 obj1
。
__init__()
方法是一种特殊的方法,称为构造函数或构造器。每当创建一个类的新实例时,都会自动执行该方法。该方法必须包含一个 self
参数,且其必须是第一个参数。示例如下:
class Dog:
def __init__(self):
print('汪!汪!')
my_dog = Dog() # 汪!汪!
说明:在 __init__()
方法的名称中,开头和结尾处是两个下划线(中间没有空格),这是一种约定,旨在区分 Python 的默认方法和普通方法。
当 __init__()
方法除了包含 self
参数外,还有额外的参数时,在类实例化时也应该提供这些参数,他们将被传递给__init__()
方法。如前面定义的 Student
类,可以按如下方式进行实例化和使用:
zhangsan = Student(2001, '张三')
lisi = Student(2002, '李四')
print(Student.count)
在创建了类的实例后,可以通过属性引用访问实例的属性(实例变量和方法)。例如:
zhangsan = Student(2001, '张三')
zhangsan.set_score('数学', 79.0) # 调用实例方法时不必给定 self 参数
print(zhangsan.scores) # 访问其 scores 数据属性,{'数学': 79.0}
print(zhangsan.count) # 甚至可以通过实例访问类变量,1
lisi = Student(2002, '李四')
lisi.set_score('数学', 88.5)
print(lisi.name) # 李四
print(lisi.count) # 2
print(Student.count) # 2
类属性、实例属性都可以自由地添加、修改和删除:
class Dog:
bark_mode = '汪!汪!'
def __init__(self):
print(Dog.bark_mode)
my_dog1 = Dog() # 汪!汪!
Dog.bark_mode = '嗷...呜...'
my_dog2 = Dog() # 嗷...呜...
Dog.name = '狗狗'
print(Dog.name) # 狗狗
my_dog1.name = '阿黄'
print(my_dog1.name) # 阿黄,同样的属性名称同时出现在实例和类中,则属性查找会优先选择实例
del Dog.bark_mode
del my_dog1.name
一般来说,实例变量用于每个实例的唯一数据,而类变量用于类的所有实例共享的属性和方法。
也可以使用以下函数的方式来访问属性:
getattr(object, name, /)
或 getattr(object, name, default, /)
: 返回 object
对象的 name
属性,等价于 object.name
。hasattr(object, name, /)
:检查是否存在一个属性,如果字符串 name
是对象 object
的属性之一的名称,则返回 True
,否则返回 False
。setattr(object, name, value, /)
:设置一个属性。如果属性不存在,会创建一个新属性,等价于 object.name = value
。delattr(object, name, /)
:删除属性,等价于 del object.name
。所有类具有一些内置属性:
__dict__
: 类的属性(包含一个字典,由类的数据属性组成)。__doc__
:类的文档字符串。__name__
:类名。__module__
:类定义所在的模块(类的全名是 __main__.ClassName
,如果类位于一个导入模块 mymod
中,那么 ClassName.__module__
等于 mymod
)。__bases__
:类的所有父类构成元素(包含了一个由所有父类组成的元组)。为了保证类内部的某些属性或方法不被外部所访问,可以在属性或方法名前面添加双下划线 __private_attrs
声明该属性为私有,只允许定义这个类本身进行访问。如下所示:
class Student:
"""所有学生的基类"""
__count = 0 # 私有类变量,表示学生总数
def __init__(self, id, name): # 特殊的构造方法
self.__avg_score = None # 私有示例变量
self.id = id
self.name = name
self.scores = {}
Student.__count += 1
def set_score(self, course, score): # 方法
self.scores[course] = score
self.__avg_score = sum(self.scores.values()) / len(self.scores)
def avg_score(self):
return self.__avg_score
zhangsan = Student(2001, '张三')
zhangsan.set_score('数学', 79.0)
print(zhangsan.avg_score())
单下划线、双下划线、头尾双下划线说明:
_foo
:以单下划线开头的表示的是保护(protected)类型的变量,即保护类型只能允许其本身与子类进行访问,不能用于 from module import *
。__foo
:以双下划线开头的表示的是私有类型(private)类型的变量,只能是允许这个类本身进行访问。__foo__
:头尾双下划线定义的是特殊属性,一般是系统定义名字,类似 __init__()
之类的。Python 通过不同的下划线命名方式在一定程度上实现了封装(Encapsulation)。封装是面向对象编程的三大基本特征之一。封装思想保证了类内部数据结构的完整性,使用该类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。
在某种情况下,一个类会有子类(或称派生类)。子类比原本的类(称为父类或基类)要更加具体化。例如,Student
这个类可能会有它的子类 MiddleSchoolStudent
和 CollegeStudent
。子类会继承父类的属性(变量和方法),并且也可包含它们自己的。我们假设 Dog
这个类有一个方法叫做 bark()
和一个属性叫做 color
。它的子类(如 ChineseRuralDog
或 GermanShepherdDog
)会继承这些成员。这意味着只需要将相同的代码写一次。
这种子类具有父类的属性和方法的机制,称为继承(Inheritance)。
继承可避免相同代码的重复书写,减少代码的数量。继承是面向对象编程的三大基本特征之二。
子类定义的语法如下:
class DerivedClassName(BaseClassName):
...
即需要将父类 BaseClassName
列在子类 DerivedClassName
头部的括号内。
在使用子类及其实例化对象时,如果引用了其属性,并且在子类中找不到此属性,会转往基类中查找此属性,如果基类也继承自其它某个类,此搜索将被递归地应用。
示例:
class Dog:
def __init__(self):
self.name = None
self.breed = None
self.bark_mode = '汪…汪…'
def bark(self):
print(f'一只名为{self.name}的{self.breed}在{self.bark_mode}地叫着。')
class ChineseRuralDog(Dog): pass
my_dog = ChineseRuralDog()
my_dog.name = '大黄'
my_dog.breed = '中华田园犬'
my_dog.bark() # 一只名为大黄的中华田园犬在汪…汪…地叫着。
以下示例无法运行,请分析原因:
class Dog:
def __init__(self):
self.name = None
self.breed = None
self.bark_mode = '汪…汪…'
def bark(self):
print(f'一只名为{self.name}的{self.breed}在{self.bark_mode}地叫着。')
class ChineseRuralDog(Dog):
def __init__(self):
self.breed = '中华田园犬'
my_dog = ChineseRuralDog()
my_dog.name = '大黄'
my_dog.bark() # AttributeError: 'ChineseRuralDog' object has no attribute 'bark_mode'
又一个示例:
class Shape:
def draw(self): ...
def edge_length(self): ...
class Polygon(Shape):
def area(self): ...
class Rectangle(Polygon):
def width(self): ...
def height(self): ...
rect = Rectangle()
rect.draw()
rect.area()
rect.width()
一个子类可以有多个父类,此种继承关系称为多重继承。如下所示:
class DerivedClassName(Base1, Base2, Base3):
...
在搜索从父类所继承的属性属性时,会按照深度优先、从左至右的顺序进行,当层次结构中存在重叠时不会在同一个类中搜索两次。因此,如果某一属性在 DerivedClassName
中未找到,则会到 Base1
中搜索它,然后(递归地)到 Base1
的基类中搜索,如果还未找到,再到 Base2
中搜索,依此类推。
示例:
class HeadingMachine:
"""掘进机"""
def driving(self): ...
class RockDrill:
"""凿岩机"""
def drill(self): ...
class BolterMiner(HeadingMachine, RockDrill):
"""掘锚机"""
def other(self): ...
另一个示例:
class Reader:
def read(self): ...
class Writer:
def write(self): ...
class File(Reader, Writer):
def other(self): ...
Python 有两个内置函数可被用于继承机制:
isinstance()
检查一个实例的类型,isinstance(obj, int)
仅会在 obj.__class__
为 int
或某个派生自 int
的类时为 True
。issubclass()
检查类的继承关系,issubclass(bool, int)
为 True
,因为 bool
是 int
的子类。 但是,issubclass(float, int)
为 False
,因为 float
不是 int
的子类。子类可以包含与父类相同签名的方法,即子类中的方法将重写(Override)父类的方法,从而表现出与父类不同的行为,而其他方法仍然继承父类。
由继承而产生的相关的不同的类,其对象对同一方法会做出不同的响应,这种机制被称为多态(Polymorphism)。多态是面向对象编程的三大基本特征之三。
在下面示例中,子类 Car
、Train
和 Bicycle
都重写了父类的 whistle
方法:
class Vehicle:
def whistle(self):
print('鸣笛声')
class Car(Vehicle):
def whistle(self):
print('嘀……嘀……')
class Train(Vehicle):
def whistle(self):
print('呜……呜……')
class Bicycle(Vehicle):
def whistle(self):
print('叮铃铃……')
上例中三个子类的实例对象可以嵌入到一个更大的容器中,通过循环语句进行遍历,并调用他们同名的方法:
vehicles = [Car(), Train(), Bicycle()]
for v in vehicles:
v.whistle()
# 嘀……嘀……
# 呜……呜……
# 叮铃铃……
这些同名的方法表现出不同的行为,这就是多态。
有时候,我们需要对数据属性给出更精确的控制。按照其他语言的的实现方法,可以编写 “getter”、“setter” 或 “deleter” 一类的方法。如下示例:
class Student:
def __init__(self, name):
self.name = name
self.__is_male = None # 为了演示目的,特别通过私有的布尔类型变量存储性别
def get_gender(self):
return '男' if self.__is_male else '女'
def set_gender(self, gender):
match gender.lower():
case '男' | '男性' | 'male' | 'm' | 'man':
self.__is_male = True
case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
self.__is_male = False
case _:
self.__is_male = None
def del_gender(self):
del self.__is_male
xiaoming = Student('小明')
xiaoming.set_gender('男')
print(xiaoming.get_gender())
xiaoming.del_gender
Python 有一个内置的 property
函数,用于创建一个特征属性 property
对象。其签名为:
class property(fget=None, fset=None, fdel=None, doc=None)
property
对象有三个方法:getter()
、setter()
和 delete()
,可以分别通过 fget
、fset
和 fdel
参数指定。这里 fget
是获取属性值的函数,fset
是用于设置属性值的函数,fdel
是用于删除属性值的函数,doc
用于为属性对象创建文档字符串。
可以通过为所定义的类创建一个 property
类型的类变量来定义一个托管属性,见下页示例。
使用 property()
函数:
class Student:
def __init__(self, name):
self.name = name
self.__is_male = None # 为了演示目的,特别通过私有的布尔类型变量存储性别
def get_gender(self):
return '男' if self.__is_male else '女'
def set_gender(self, gender):
match gender.lower():
case '男' | '男性' | 'male' | 'm' | 'man':
self.__is_male = True
case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
self.__is_male = False
case _:
self.__is_male = None
def del_gender(self):
del self.__is_male
# gender 是一个托管的属性
gender = property(get_gender, set_gender, del_gender, "I'm the 'gender' property.")
xiaoming = Student('小明')
xiaoming.gender = '男'
print(xiaoming.gender) # 男
del xiaoming.gender
这里 xiaoming.gender
将调用其“getter”,xiaoming.gender = value
将调用其“setter”,del xiaoming.gender
将调用 “deleter”。
以上代码可进一步用 Python 的装饰器来实现,见下页代码。
装饰器为一种语法糖,它是返回值为另一个函数的函数。通常使用 @wrapper
形式的语法来进行函数变换,从而修改其他函数功能,使代码更简洁。
下页 @property
装饰器会创建一个与 gender()
方法同名的特征属性 gender
,将该方法指定给其 “getter”,将 gender
的文档字符串设置为 "I'm the 'gender' property."
,然后将同名的 gender()
方法分别指定给 gender
特征属性的 “setter” 和 “deleter” 方法。
使用 @property
装饰器:
class Student:
def __init__(self, name):
self.name = name
self.__is_male = None
@property
def gender(self):
"""I'm the 'gender' property."""
return '男' if self.__is_male else '女'
@gender.setter
def gender(self, gender):
match gender.lower():
case '男' | '男性' | 'male' | 'm' | 'man':
self.__is_male = True
case '女' | '女性' | 'female' | 'f' | 'woman' | 'w':
self.__is_male = False
case _:
self.__is_male = None
@gender.deleter
def gender(self):
del self.__is_male
xiaoming = Student('小明')
xiaoming.gender = '男'
print(xiaoming.gender) # 男
del xiaoming.gender
编写一个存储网站注册用户信息的 User
类,要求:
name
只能使用 A~Z
、a~z
、0~9
、-
和 _
这些字符,只能以字母开头,长度在 5~20 字符之间;password
只能是编号范围为 32~126 之间的 ASCII 可显示字符,长度在 10~50 之间,必须至少各包含一个小写字母、大写字母和数字,以及至少包含一个除了字母和数字之外的特殊字符。User(name, password)
的形式创建用户,后期也可以进一步通过 user.name = new_value
的形式修改用户名和密码。提示:最好使用 @property
装饰器将用户名和密码设置为特征属性,并将实际的用户名和密码实例变量设置为私有的或保护的。另外请注意这里是以明文存储密码的,实际网站的密码都应该是不可逆加密存储的。
复习矿井通风课程中所学习的关于井巷通风阻力一章的知识,编写一个名称为 Roadway
的类,可以表示在矿井通风设计时井巷通风阻力计算的一些基本操作。以下已经搭建出该类的框架,请将其中标注为 TODO
的部分补充完整:
class Roadway:
"""表示一段巷道,该段巷道对应通风网络图中的一个边,所有数值单位都为国际标准单位"""
def __init__(self, length, area, form, alpha, volume_flux):
self.length = length # 巷道的长度
self.area = area # 巷道的断面积
self.form = form # 巷道断面形状,参见 SectionalForm 类的定义
self.alpha = alpha # 巷道的摩擦阻力系数,可在通风教材最后的附录中通过查表得到
self.volume_flux = volume_flux # 巷道的风量(体积通量)
self.air_density = 1.25 # 巷道内空气的平均密度
# 总阻力与摩擦阻力的比值,也是总风阻与摩擦风阻的比值,取值范围为 1.1~1.15,
# 一般新建矿井宜取 1.10,改扩建矿井宜取 1.15
self.t2f_ratio = 1.1
@property
def perimeter(self):
"""根据巷道的断面面积、断面形状估算巷道的断面周长"""
match self.form:
case SectionalForm.Trapezoid:
return 4.16 * (self.area ** 0.5)
case SectionalForm.ThreeCenteredArch:
return 3.85 * (self.area ** 0.5)
case SectionalForm.SemiCircularArch:
return 3.90 * (self.area ** 0.5)
case _:
return None
def frictional_resistance(self):
"""计算该段巷道的摩擦风阻 Rf"""
pass # TODO: 请进一步完成该函数
def local_resistance(self):
"""计算该段巷道的局部风阻 Rl"""
return self.frictional_resistance() * (self.t2f_ratio - 1.0)
def total_resistance(self):
"""计算该段巷道的总风阻 R"""
return self.frictional_resistance() * self.t2f_ratio
def velocity(self):
"""计算该段巷道的平均风速"""
pass # TODO: 请进一步完成该函数
def frictional_loss(self):
"""计算该段巷道的摩擦阻力 hf"""
pass # TODO: 请进一步完成该函数
def local_loss(self):
"""计算该段巷道的局部阻力 hl"""
return self.frictional_loss() * (self.t2f_ratio - 1.0)
def total_loss(self):
"""计算该段巷道的总阻力 h"""
return self.frictional_loss() * self.t2f_ratio
class SectionalForm:
"""这里实际上是用类变量模拟其他语言的枚举变量,即三个类变量分别代表三种断面形状。
Python 的标准库是有枚举(enum)模块的,不过这里为了展示类的用法而没有用该模块。"""
Trapezoid = 1 # 梯形
ThreeCenteredArch = 2 # 三心拱
SemiCircularArch = 3 # 半圆拱
# 给定一段通风路线
vent_path = [
Roadway(123.0, 16.0, SectionalForm.ThreeCenteredArch, 33.2e4, 90.0),
Roadway(37.4, 13.6, SectionalForm.SemiCircularArch, 82.9e4, 74.5),
Roadway(409.1, 13.2, SectionalForm.Trapezoid, 167.8e4, 64.5),
]
path_total_loss = 0.0 # 给定的通风路线的总阻力初值
# TODO: 请进一步编写程序,计算以上给定的通风路线的总阻力 path_total_loss
要求:
安模作业02-06-学号-姓名.py
的源文件中,通过电子邮件以附件形式发给任课教师。安模作业02-06-学号-姓名
的形式。