01. 面向过程与面向对象
1.1 面向过程基本概念(POP)
- 之前写的所有代码都是面向过程(Procedure Oriented Programming,简称POP)的,POP是一种符合计算机思维的程序设计思想。
POP主要考虑一个功能需要由些步骤完成,并且先执行哪些步骤,再执行哪些步骤。如用POP实现把大象塞进冰箱的功能大致思路如下:
首先把冰箱打开
然后把大象塞进去
最后把冰箱关上
1.2 面向对象基本概念(OOP)
面向对象编程(Object Oriented Programming,简称OOP),是一种比较符合现实、符合人类思维的编程设计思想。
- OOP主要考虑一个功能由谁来做,如用OOP实现把大象塞进冰箱的功能大致思路如下:
```python
定义类
定义人类: 塞东西进冰箱的功能(物品)
定义冰箱类: 打开冰箱门的功能 关闭冰箱门的功能
定义大象类
实例化类的对象
人对象 = 人类() 冰箱对象 = 冰箱() 大象对象 = 大象()
由具体的对象完成具体的功能
冰箱对象.打开冰箱 人类对象.塞进冰箱(大象对象) 冰箱对象.关上冰箱
<a name="m5W0V"></a>
## 02. 类与对象
<a name="bcWtW"></a>
### 2.1 类与对象的基本概念
- 类是封装同一类对象的属性和行为的载体,也就是具有相同属性和行为的一类实体的抽象描述称为类。比如:人类、动物类、汽车、飞机等。
- 对象是相对于类而言的,是一个真实存在的事物实体。比图张继科是人类的一个对象、小明家的小黑狗汪汪是动物类的一个对象、我家的那辆雪佛兰轿车是汽车类的一个对象。在显示生活中,“万物皆对象”,即生活中随处可见的一个东西都是一个对象。
- 通常我们是通过两个方向来描述对象的,就是静态部分和动态部分:
- 静态部分称为对象的“属性”,任何对象都具备自身的属性,这些属性不仅是客观存在的,也是不能忽视的,比如人的姓名、性别、年龄等。
- 动态部分描述的是对象的行为,称之为对象的“方法”,就是对象要执行的动作,比如人要吃饭,人要走路等等。
<a name="o1TW7"></a>
### 2.2 类的定义与对象的实例化
- 具体的对象是某一个类的实例化结果,因此一定是现有类,再有类的对象。
<a name="Qnebg"></a>
#### 2.2.1 类的定义
- 类实际上分为系统类型和自定义类型两大类:
- 系统类型:所谓系统类型就是Python为我们提供好了的数据类型结构,如字符串、列表、元组、字典等。这些类型不需要开发者手动声明,是可以直接拿来用的,比如`nums = [12, 34, 56, 78]`就创建了一个列表对象。
- 自定义类型:所谓自定义类型就是当系统类型无法满足开发的需要时,开发者可以自己定义一些类型,并且也可以自己决定这个类中有哪些结构。
- 类的定义主要是针对于自定义类型而言的。
- 类定义之前需要明确的点:
- 对象有静态部分信息(属性)和动态部分信息(方法),这就要求我们在定义类之前要直到如何描述这个类,如有哪些属性、有哪些功能等。
- 描述静态部分的时候我们采用成员变量的方式(通过`属性名 = 指`的形式指定)。
- 描述动态部分的时候我们采用是定义函数的形式来表示对象的动作(对象中的函数称为方法),需要这个动作的时候,就来调用函数即可。
- 自定义类型的语法格式:
- `class`:是一个关键字,用于定义类。
- `类名`:是自定义类型的类型名,如学生类的类名可以是`Student`,动物类的类名可以是`Animal`。
- 注意,Python中变量使用的小驼峰方式命名,即变量中所有字符小写,每个单词之间用下划线分割,如`pet_dog`。
- 但是,类的命名采用大驼峰方式命名,即多个单词连接在一起,但每个单词首字母大写,如`PetDog`。
- `__init__`:为该类对象添加静态属性,并赋予属性初始值。(类似于Java的构造器)
- `self`:表示当前对象。
- 一个类是会有很多的对象的,描述信息的时候到底给哪个对象描述的,这个self就是用来指定当前对象的。
- 如self.name = "Tom",就是用于给当前对象的名字属性赋值Tom,self.upper()就是调用当前对象的upper()方法。
- 行为是来描述对象的动态信息的,其格式上与函数的定义没有什么差别。
```python
class 类名:
def __init__(self, 属性名1, 属性名2, ……, 属性名N):
self.属性名1 = 属性值1
self.属性名2 = 属性值2
……
self.属性名N = 属性值N
# 下面还可以写在实例化对象时要做的其他操作
def 行为名(self, 形参名):
行为操作实现
2.2.2 对象的实例化与结构的调用
- 对象的实例化:
- 人类有吃饭、走路、说明等能力,但空空一个人类的概念是无法做到这些的,必须要有一个现实中实际存在的人,才能让这个人完成这些事情。
- 在Python中也是一个道理,定义完类后,若要使用类中定义的结构,则必须通过类创建出一个实实在在的对象,而通过类创建对象的过程称之为实例化。
- 实例化的语法结构:
对象名 = 类名(对象的实际属性值)
- 在对象实例化时,会自动调用类中声明的
__init__()
函数,并将当前实例化对象的地址赋值给self
。 - 因此
self
参数并不需要手动传值,并且由此也可以看出,__init__()
中的self.属性名 = 属性值
实际上就是在给当前对象的属性赋值。
通过对象调用类中声明的结构:
示例一:定义一个学生类。
- 学生有姓名、年龄、性别、居住地址、体重、身高等属性。
- 学生有吃饭、睡觉、学习等功能。
- 定义完成类后进行实例化。要求学生对象在实例化的同时,与外界打招呼,如实例化学生Tom时,输出
Hello, I'm Tom.
,预示着该对象的诞生。 实例化完成后,让该学生对象进行学习的行为。 ```python class Student: def init(self, name, age, gender, address, weight, height): self.name = name self.age = age self.gender = gender self.address = address self.weight = weight self.height = height print(f”Hello, I’m {self.name}.”)
def eat(self): print(f”{self.name} is eating something.”)
def sleep(self): print(f”{self.name} is going to bed.”)
def study(self): print(f”{self.name} is going to learn new knowledge.”)
jack = Student(“Jack”, 18, “male”, “China”, 65.3, 175) jack.study() “”” 运行结果: Hello, I’m Jack. Jack is going to learn new knowledge. “””
- 示例二:定义一个宠物猫类。有品种、名字、年龄、性别等属性;有猫叫、吃饭、睡觉、玩耍等行为。
```python
class PetCat:
def __init__(self, varieties, name, age, gender):
self.varieties = varieties
self.name = name
self.age = age
self.gender = gender
def cat_barking(self):
print(f"喵~我叫{self.name},是一只{self.varieties}小{self.gender}猫,已经{self.age}岁了。")
def eat(self, food):
print(f"{self.name}在吃{food}。")
def play(self):
print(f"{self.name}出去玩了。")
def sleep(self):
print(f"{self.name}玩累了,回家睡觉了。")
daodan = PetCat("布偶", "捣蛋", 4, "公")
daodan.cat_barking() # 喵~我叫捣蛋,是一只布偶小公猫,已经4岁了。
daodan.eat("小鱼干") # 捣蛋在吃小鱼干。
daodan.play() # 捣蛋出去玩了。
daodan.sleep() # 捣蛋玩累了,回家睡觉了。
2.2.4 常见错误—通过类调用结构
正常情况下是要通过
对象名.结构
来调用类中声明的结构的,若用类名.结构
则会报错。- 通过报错信息可以看出,在调用sleep()方法是缺失了一个self参数的传值。
PetCat.sleep()
# TypeError: sleep() missing 1 required positional argument: 'self'
- 通过报错信息可以看出,在调用sleep()方法是缺失了一个self参数的传值。
2.2.2中有讲到,在实例化对象时Python会自动调用类中的
__init__()
方法,并将当前实例化对象的地址赋值给self
,因此通过对象名.结构
调用类结构时self
是有值的,可以直接调用;而若通过类名.结构
调用结构,则并没有实例化对象,此时既没有调用__init__()
方法,self
也没有获取到值,所以报错。但是这种语法Python是支持的,只需要稍加改动:
类名.结构(self=类的对象)
。daodan = PetCat("布偶", "捣蛋", 4, "公")
PetCat.sleep(self=daodan) # 捣蛋玩累了,回家睡觉了。
但是从逻辑上讲,猫类在睡觉和那只叫捣蛋的猫在睡觉是有区别的,正常而言应该是某个具体的对象在做什么事情。
- 并且两种方式都是要实例化对象的,那为什么不直接通过对象调用结构呢?
所以还是建议用
对象名.结构
而非类名.结构(self=对象)
。03. 类的结构
3.1 成员变量
3.1.1 成员变量的基本概念
在之前的函数中已经介绍过了全局变量和局部变量两个概念,而在类中,还存在一个成员变量的概念。
- 全局变量的作用域为整个应用程序,存储在方法区的静态池中;局部变量的作用域为函数体内,存储在函数压栈后的域场中;成员变量实际上就是属性,它是随着对象的创建存储在堆中的。
以2.2.3中的PetCat类为例,
__init__(self, varieties, name, age, gender)
中声明的varieties、name、age、gender四个变量是__init__()
函数的局部变量,通过self.varieties = varieties
这类语句的执行,局部变量varieties的值会被赋值给成员变量varieties。3.1.2 从内存角度理解init()中的局部变量和成员变量
还是以2.2.3中的PetCat类为例,可以发现有一个局部变量varieties,同时也存在一个成员变量varieties,这两个变量的变量名是完全相同的,但为什么不冲突呢?实际上是因为这两个变量存储在不同的区域中。
- PetCat类的定义与实例化这个过程在内存中的变化:
- 当PetCat的类结构被定义出来时,这部分结构就会存储在方法区的方法池中。
- 当类定义完成后,就是实例化的过程,以
daodan = PetCat("布偶", "捣蛋", 4, "公")
为例。 - 首先,PetCat类中的
__init__()
会进行压栈,这个函数中有四个局部变量varieties, name, age, gender也会在栈中函数的域场里被创建出来。 - 接着,传入的实参的4个数据会被加载到方法区的常量池中,并且这四个数据会被赋值给域场中的形参变量。到此为止,
__init__()
的准备工作就做好了,接下去就是运行这个方法。 - 此时,Python会在堆中开辟一块区域,用于存储PetCat对象daodan中各种各样的数据,并将这块内存区域的地址(以0x1101为例)赋值给self。
- 随着
self.varieties = varieties
这类语句的执行,Python会到self指向的内存地址0x1101中创建成员变量varieties,并把__init__()
中局部变量varieties的值(数据的内存地址)赋值给成员变量varieties。(另外的name、age、gender同理)。在这个过程中,可以明显开出局部变量varieties、name、age、gender是存储在域场中的,成员变量varieties、name、age、gender是存储在堆中的,因此就算这个8个变量两两重名,因为他们是存储在不同的内存结构中的,因此相互直接也不会冲突。 - 到此为止,
__init__()
就运行完成并出栈了,而"布偶", "捣蛋", 4, "公"
这四个值将作为成员变量的值继续被堆中的变量所引用。此时对象就在内存中实例化完成中,将对象的内存地址0x1101赋值给静态池中的全局变量daodan,一个全局中的对象就创建完成了。 - 成员变量和对象的其他数据会一直保存在内存中,直到对象完全失去引用后,整个对象的所有数据都会从堆中被删除。
总结:成员变量实际上就是对象的属性,随着对象的构建存储在堆区。当对象没有变量在使用其地址时,对象消失,对象的特征(即成员变量)也随着消失。
3.2 魔术方法
3.2.1 魔术方法的基本概念
魔术方法是指由官方命名并定义的,具有特殊含义的方法。
- 魔术方法有一个明显的标志,即方法名前后都有两个下划线:
__方法名__
。 - 魔术方法一般情况下不需要手动调用,而是满足特定时机时自动调用。
如
__init__()
构造方法就会在构造对象,给对象添加属性特征赋予初始值时会自行调用。3.2.2 del()析构方法
堆中的数据释放时遵循引用计数原则,当对象计数器为0时,就会释放该对象。
但在对象释放之前,系统会自动调用该对象的
__del__()
方法,因此改方法也被称之为析构方法。 ```python class Student: def init(self, name, age, gender):print(f"{self}被创建了")
self.name = name
self.age = age
self.gender = gender
def del(self):
print(f"{self}被释放了")
stu = Student(“乐乐”, 18, “男”) # 引用计数为1 stu1 = stu # 引用计数为2
for i in range(5): print(i) if i == 1: stu = None # 引用计数为1 elif i == 3: stu1 = None # 引用计数为0,对象被释放,会调用析构方法
- 运行结果:
```python
<__main__.Student object at 0x000002045566DFD0>被创建了
0
1
2
3
<__main__.Student object at 0x000002045566DFD0>被释放了
4
3.2.3 str()/repr()打印对象
像字符串、列表等系统类型的对象在print()时会直接打印对象的内容。
s = "ABCDEF"
l = ["AB", "CD", "EF"]
print(s) # ABCDEF
print(l) # ['AB', 'CD', 'EF']
但是在print()自定义对象时,打印的确是对象在内存中的地址。 ```python class Student: def init(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
stu1 = Student(“乐乐”, 18, “男”) stu2 = Student(“敏敏”, 21, “女”)
print(stu1) # <__main__.Student object at 0x0000023FAE0CDFD0> print(stu2) # <__main__.Student object at 0x0000023FAE0CDF10>
- 一般来说,print()是用来看数据的,但若打印出来的是一堆内存地址,那实际上看不出来什么东西。
- 此时若要能够根据自己的需求查看对象数据,则可以重写一下`__str__()`方法,在其中定义打印的结构。
- `__str__()`方法的返回值是字符串格式的,这个方法返回了什么,那么print()时就打印什么。
- 但是当对象存在于容器中时,打印的还是对象的内存地址。
```python
class Student:
def __init__(self, name, age, gender):
self.name = name
self.age = age
self.gender = gender
def __str__(self):
return f"Student(name={self.name}, age={self.age}, gender={self.gender})"
stu1 = Student("乐乐", 18, "男")
stu2 = Student("敏敏", 21, "女")
print(stu1) # Student(name=乐乐, age=18, gender=男)
print(stu2) # Student(name=敏敏, age=21, gender=女)
students = [stu1, stu2]
print(students) # [<__main__.Student object at 0x0000020F3F9CDFD0>, <__main__.Student object at 0x0000020F3F9CDF10>]
若想要打印对象集合时也是可读的对象数据,则可以重写一下
__repr__()
方法。- 重写
__repr__()
方法后就不用重写__str__()
方法了。 因为
__repr__()
方法在实现其特殊功能的同时,也实现了__str__()
的那部分功能。 ```python class Student: def init(self, name, age, gender): self.name = name self.age = age self.gender = genderdef repr(self): return f”Student(name={self.name}, age={self.age}, gender={self.gender})”
- 重写
stu1 = Student(“乐乐”, 18, “男”) stu2 = Student(“敏敏”, 21, “女”)
print(stu1) # Student(name=乐乐, age=18, gender=男) print(stu2) # Student(name=敏敏, age=21, gender=女)
students = [stu1, stu2] print(students) # [Student(name=乐乐, age=18, gender=男), Student(name=敏敏, age=21, gender=女)]
- 总结:若要根据自己的需求打印打印的数据,则可以在类中重写`__str__()`和`__repr__()`方法,但`__repr__()`方法功能更强,因此建议直接重写`__repr__()`方法。
<a name="c7f9k"></a>
#### 3.2.4 运算符魔术方法
- 对象支持运算的本质:
- 一些类实例化出来的对象,其对象之间是支持一些运算的,但对象之间的运算不是说支持就能支持的。
- 如果想要让对象支持指定的运算,就需要在对应的类中定义出来对应的魔术方法,在魔术方法中实现运算的规则才可以。
- 常见的运算相关的魔术方法:`__add__()`:加法、`__sub__()`:减法、`__mul__()`:乘法、`__truediv__()`:除法、`__floordiv__()`:整除、`__mod__()`:取模、`__and__()`:按位与、`__or__()`:按位或、`__xor__()`:按位异或、`__lshift__()`:按位左移、`__rshift__()`:按位右移、`__eq__`:==相等、`__pow__()`:求次幂、……。
- 示例:定义一个Numer类,一个Student类,并实现加法运算。
- Number类用于实例化一个数字,其加法运算就是数学中的加法运算。
- Student类用于实例化学生信息,其加法就是将两个Student类的对象封装到一个列表中。
```python
class Numer:
def __init__(self, num):
self.num = num
def __repr__(self):
return f"{self.num}"
def __add__(self, other):
# self是运算符前面的对象,other是运算符后面的对象
# 如:针对于1 + 2而言,1就是self,2就是other
return Numer(self.num + other.num)
class Student:
def __init__(self, name, age, student_id):
self.name = name
self.age = age
self.student_id = student_id
def __repr__(self):
return f"Student(name={self.name}, age={self.age}, student_id={self.student_id})"
def __add__(self, other):
return [self, other]
num1 = Numer(10)
num2 = Numer(20)
new_num = num1 + num2
print(new_num) # 30
stu1 = Student("明明", 19, 22001)
stu2 = Student("凯凯", 20, 22002)
students = stu1 + stu2
print(students) # [Student(name=明明, age=19, student_id=22001), Student(name=凯凯, age=20, student_id=22002)]
3.2.5 eq()判断对象是否相等
借用3.2.4中的Student类实例化两个字面量完全相同的对象,然后用==判断二者是否相等。
stu1 = Student("明明", 19, 22001)
stu2 = Student("明明", 19, 22001)
print(stu1 == stu2) # False
可以看到就算是字面量完全一样,但是这两个对象还是不一样的。这是因为
__eq__()
默认会比较两个对象在内存中的地址是否一样,但根据堆中的知识我们可以直到,自定义对象实例化一次就会在堆中创建一个新的对象,因此就算两个对象的字面量一样,但是由于地址不可能一样,所以==的结果一定是False。print(id(stu1), id(stu2)) # 2552006041552 2552006041360
print(id(stu1) == id(stu2)) # False
print(stu1 == stu2) # False
但是有时候比较两个对象的地址实际上没有什么意义,更多的时候需要开发者自己决定两个对象根据什么规则判断是否相等(多数情况下是对象字面量一致就认为两个对象相等)。
num1 = [1, 2, 3]
num2 = [1, 2, 3]
print(id(num1) == id(num2)) # False
print(num1 == num2) # True
# 地址不一样,但字面量一样,所以==的结果为True
这种时候就可以重写
__eq__()
方法,在其中定义判断两个对象是否相等的规则。示例:若两个Student对象的字面量完全一样,就认为这两个对象相等;只要存在一点不同,就认为这两个对象不相等。 ```python class Student: def init(self, name, age, student_id):
self.name = name
self.age = age
self.student_id = student_id
def eq(self, other):
return self.name == other.name and \
self.age == other.age and \
self.student_id == other.student_id
stu1 = Student(“明明”, 19, 22001) stu2 = Student(“明明”, 19, 22001) stu3 = Student(“明明”, 19, 22002) print(stu1 == stu2) # True print(stu2 == stu3) # False
- 为了方便对象之间的计算,而不是进行无意义的内存地址比较,建议在定义类的时候就重写`__eq__()`方法。
<a name="Te01v"></a>
## 04. 类与类的关系
<a name="XDqIL"></a>
### 4.1 类与类之间的关系概述
- 两个类之间可能毫无关联,但也有可能存在一定的联系。
- 常见的联系关系有如下三种:
- has-a:在一个类的对象中,存在着某个属性的值是另一个类的对象。
- use-a:在一个类的对象中,存在着一个或多个方法,在这些方法中会使用到另一个类的对象的信息。
- is-a:两个类之间能形成谁是谁的这种概念体系,这个就是所谓的继承。(具体在02. 面向对象的三大特性 -- 03. 继承中介绍)
<a name="kI5hv"></a>
### 4.2 use-a应用举例
- 应用需求:
- 定义一个猫类(属性:昵称name、年龄age,方法:抓老鼠的方法)。
- 定义一个鼠类(属性:昵称name)。
- 创建猫对象信息为5岁的Tom猫;创建老鼠对象信息为名为Jerry。
- 执行猫抓老鼠的行为,输出:一只5岁的Tom猫抓到一只名叫Jerry的老鼠。
- 代码实现:
```python
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def catch_mouse(self, mouse):
print(f"一只{self.age}岁的{self.name}猫抓到一只名叫{mouse.name}的老鼠。")
class Mouse:
def __init__(self, name):
self.name = name
tom = Cat("Tom", 5)
jerry = Mouse("Jerry")
tom.catch_mouse(jerry)
use-a的体现:可以发现,Cat类的catch_mouse()方法在指向时需要传入一个Mouse类的对象,并调用Mouse类对象的name属性。
4.3 has-a应用举例
4.3.1 点与圆的关系(一对一的关系)
应用需求:
- 定义一个点类,用于表述一个平面直角坐标系上的点,要有x和y两个属性。
- 定义一个圆类,需要有圆心(平面上一个点)和半径两个属性,并且可以计算坐标轴上任意一个点与圆的位置关系。
- 实例化一个圆,其中圆心为,半径为5,计算点与这个点的位置关系。
代码实现: ```python class Point: def init(self, x, y):
self.x = x
self.y = y
def repr(self):
return f"Point({self.x}, {self.y})"
class Circle: def init(self, center_point, radius): self.center_point = center_point # 圆心 self.radius = radius # 半径
def __repr__(self):
return f"Circle(center_point=({self.center_point.x}, {self.center_point.y}), radius={self.radius})"
def relation(self, other_point):
# 计算点与圆心的距离
cpx, cpy = self.center_point.x, self.center_point.y
opx, opy = other_point.x, other_point.y
distance = ((cpx - opx) ** 2 + (cpy - opy) ** 2) ** (1 / 2)
# 通过点与圆心的距离和半径的比较,判断点在圆上、圆内、圆外。
if distance > self.radius:
return "圆外"
elif distance < self.radius:
return "圆内"
else:
return "圆上"
center_point = Point(0, 0) circle = Circle(center_point, 5) other_point = Point(4, 6) print(f”{other_point}在{circle}的{circle.relation(other_point)}。”) # Point(4, 6)在Circle(center_point=(0, 0), radius=5)的圆外。
- has-a的体现:Circle类中用于描述圆心的属性center_point实际上是一个点,即Point类的对象。
- 并且,在Circle中还可以看到很多use-a的结构。
<a name="RT8iY"></a>
#### 4.3.2 班级和学生的关系(一对多的关系)
- 应用需求:
- 定义一个学生类,有学号、姓名、年龄、性别、成绩几个属性。
- 定义一个教室类,有教室编号、教室规模、已有学生几个属性(要求不同教室间的学生数据不共享)。有添加学生的属性(要求添加操作在符合教室规模的前提下执行)。
- 接着实例化一个教室,再实例化一些学生,将这些学生安排到这个教室中。
- 代码实现:
```python
class Student:
def __init__(self, sid, name, age, gener, score):
self.sid = sid
self.name = name
self.age = age
self.gender = gener
self.score = score
def __repr__(self):
return f"Student(sid={self.sid}, name={self.name}, age={self.age}, gener={self.gender}, score={self.score})"
class Classroom:
def __init__(self, cid, size, stus=None):
# stus的默认值不能直接设置为空(具体参考04. 函数与排序算法 -- 01. Python函数基础 -- 3.3.2 当默认参数为可变容器时的注意事项)
# 因此,若要在初始化时创建一个空列表,则先给默认参数定义为None,然后用以下方法定义空列表。
if stus == None:
stus = []
self.cid = cid
self.size = size
self.stus = stus
def add_stu(self, stu):
if len(self.stus) >= self.size:
print("教室已满,学生添加失败!")
else:
self.stus.append(stu)
print(f"{stu}添加成功")
# 构建教室对象
c1 = Classroom("1001", 40)
# 学生信息数据
sids = ["10001", "10002", "10003", "10004", "10005"]
names = ["乐乐", "笑笑", "倩倩", "欢欢", "晴晴"]
ages = [16, 17, 15, 16, 17]
genders = ["男", "女", "女", "女", "女"]
scores = [83, 65, 57, 72, 55]
# 根据数据构建学生对象,并添加到教室中
for i in range(len(sids)):
stu = Student(sid=sids[i], name=names[i], age=ages[i], gener=genders[i], score=scores[i])
c1.add_stu(stu)
- has-a的体现:Classroom类中的stus属性虽然是个列表,但其中存储的都是学生对象,以此实现一个一对多的关系。