01. 封装
1.1 封装的基本理解
- 所谓封装,就是隐藏一切可以隐藏的实现细节,只对外暴露简单的接口即可。
- 在类中定义的方法就可以理解为封装。在类中定义方法使得我们在创建对象之后,只需要通过
对象.方法名(参数)
的形式就可以执行一些定义好的操作。这个时候其实我们只需要直到方法的名字和参数即可,不需要知道其内部是怎么实现的。 比如
list_obj.append(data)
可以实现向列表list_obj的末尾添加数据data,但我们只知道这个方法可以实现添加数据的操作,并不知道它具体是怎么添加数据,这就是所谓的封装。1.2 可见性
1.2.1 可见性与属性/方法私有化
在很多编程语言中,对象的属性或方法通常都有三个描述关键字:private(私有的)、public(公共的)、protected(受保护的)。
- 其中,被private和protected修饰的属性或方法就无法被直接访问了。
- Python中虽然没有private、public、protected这些关键字,但是Python中依然有这三档不同程度的可见性。
Python中不同可见性的实现:
- 在默认情况下,一个类的属性或方法默认就是公共的,不管在哪个位置,只要可以创建对象就可以访问。
- 若要将属性或方法设置成受保护protected的,只需要在属性名或方法名前添加一个下划线,如:
_name
。 若要将属性或方法设置成私有private的,只需要在属性名或方法名前添加两个下划线,如:
__age
。 ```python class Student: def init(self, sid, name, age): self.sid = sid # public属性 self._name = name # protected属性 self.__age = age # private属性私有化方法
def __print_name(self): print(self._name)
stu = Student(1001, “小明”, 18) print(stu.sid) # 1001 print(stu.name) # 无法访问,报错:AttributeError: ‘Student’ object has no attribute ‘name’ print(stu.age) # 无法访问,报错:AttributeError: ‘Student’ object has no attribute ‘age’ stu.print_name() # 无法访问,报错:AttributeError: ‘Student’ object has no attribute ‘print_name’
- 通过运行结果可以看出,将属性或方法私有化之后,就无法通过`对象.属性/方法()`的形式进行访问了。
<a name="IisTl"></a>
#### 1.2.2 私有化的应用场景
- 私有化属性:
- 一但确定值就不再改变的属性,是不允许被外界修改值的,这类属性通常会被私有化。
- 比如一个人的性别在自然情况下是从出生那一刻起就确定了的,不会再修改,那么gender属性就可以私有化成`__gender`。
- 这种属性一般就会进行访问,不会进行改值,因此提供get()方法即可,不需要提供set()方法(但也可以提供)。
- 属性的值不能被外界随便修改,这种情况下也要对属性进行私有化。
- 比如一个人的姓名一般情况是不会轻易修改的,因此name属性可以私有化成`__name`。但是通过个人向相关户籍单位申请,那也是可以修改的。
- 这种属性既可以进行访问,也可以进行改值,因此需要提供get/set方法。
- 属性值的修改需要满足一定的条件时,也可以将属性进行私有化。
- 如电脑要加内存,最好的情况就是组双通道,若不将内存属性私有化,那么内存就可以被随便赋值了。
- 此时可以将属性私有化,然后在set中指定逻辑,比如若不满足双通道,就不扩容,只有满足双通道时才扩容成功。
- 私有化方法:当某个方法是为了辅助类中其他方法完成计算才被定义出来,且该方法提供给对象使用无意义的情况下,会将这个方法私有化。
<a name="EDXmX"></a>
#### 1.2.3 get/set方法
- 设置为private/protected的属性外界是无法直接访问的(包括取值与修改),因此可以针对于一个属性提供get/set方法来访问私有化的属性。
- `get_属性名()`:用于获取私有化属性的值。
- `set_属性名`:用于修改私有化属性的值。
- 注意:get/set方法必须是public的。
- 示例:将Student类的name、age属性私有化,并为它们提供对应的get/set方法。
```python
class Student:
def __init__(self, name, age):
self.__name = name
self.__age = age
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
def get_age(self):
return self.__age
def set_age(self, age):
self.__age = age
# 实例化并访问私有化属性
stu = Student("小明", 18)
print(stu.get_name(), stu.get_age()) # 小明 18
# 修改私有化属性
stu.set_name("小黑")
stu.set_age(25)
print(stu.get_name(), stu.get_age()) 小黑 25
1.3 动态属性
1.3.1 对象动态增加属性
- Python是一门动态的语言,所谓动态语言,就是指程序在运行的时候,可以改变数据结构的语言。
- 补充知识点:
- 构造对象的时候,解释器为对象分配一个字典,来存储对象的特征信息(属性与属性值的键值对)。
- 可以通过
对象.__dict__
获取该字典。
在Python中,对象可以动态的增加属性:
对象.属性名 = 值
```python class Computer: def init(self, brand, memory, hard_disk):"""
:param brand: 品牌
:param memory: 内存
:param hard_disk: 硬盘
"""
self.__brand = brand
self.__memory = memory
self.__hard_disk = hard_disk
def repr(self):
return f"Computer(brand={self.__brand}, memory={self.__memory}, hard_disk={self.__hard_disk})"
macbook = Computer(“苹果”, “8G”, “256G”) print(macbook) # Computer(brand=苹果, memory=8G, harddisk=256G) print(macbook._dict) # {‘_Computerbrand’: ‘苹果’, ‘_Computermemory’: ‘8G’, ‘_Computer__hard_disk’: ‘256G’}
print(macbook.brand) # 试图访问私有化属性品牌,发现会报错
macbook.brand = “小米” # 试图修改品牌属性,发现并没有报错。 print(macbook) # Computer(brand=苹果, memory=8G, harddisk=256G),但self.brand依旧是苹果。 print(macbook.brand) # 小米,再访问品牌属性,发现并没有报错,并且值是小米。 print(macbook.dict_) # {‘_Computerbrand’: ‘苹果’, ‘_Computermemory’: ‘8G’, ‘_Computer__hard_disk’: ‘256G’, ‘brand’: ‘小米’},但字典中增加了键值对’brand’: ‘小米’
使用动态属性还可以再增加其他属性
macbook.externalscreen = “AOC” macbook.mouse = “logitech” print(macbook._dict) # {‘_Computerbrand’: ‘苹果’, ‘_Computermemory’: ‘8G’, ‘_Computer__hard_disk’: ‘256G’, ‘brand’: ‘小米’, ‘external_screen’: ‘AOC’, ‘mouse’: ‘logitech’}
- 注意:动态属性是针对于具体对象而言的,如现在再创建一个新的Computer对象thinkpad,发现在thinkpad的`__dict__`里并没有macbook动态增加的brand、external_screen、mouse这些属性。
```python
thinkpad = Computer("联想", "32G", "1T")
print(thinkpad.__dict__) # {'_Computer__brand': '联想', '_Computer__memory': '32G', '_Computer__hard_disk': '1T'}
1.3.2 私有属性并非将属性私有化
- 从1.3.1的程序中的前两次
print(macbook.__dict__)
可以看出,第19行被注释掉的代码print(macbook.brand)
尝试访问brand属性会报错是因为macbook.__dict__
中并没有'brand': '苹果'
这样的键值对。 而Computer类中私有化的brand属性实际上是_Computer__brand属性,尝试访问这个属性,发现获取到的值真的是“苹果”。
- 另外两个私有化属性也是一样的道理:self.memory体现为_Computermemory、self.hard_disk体现为_Computerhard_disk。
- 这些属性都是可以直接访问的。
print(macbook._Computer__brand) # 苹果
由此可以得出,Python在语法上没有严格的私有化属性,只是给私有化的属性和方法换了个名字(原属性/方法名 => 类名属性/方法名),因此用原来的名字就无法访问到指定的结构了。
总结:Python并没有像Java那样的强私有化,只要知道了Python私有化是怎么改变量名的,就还是可以访问到被私有化的结构,因此私有化在Python中实际上不常用。
在类列结构中直接声明
__slots__
可以限制动态属性的实现,变量__slots__
的值就是由所有属性名组成的元组。class Computer:
__slots__ = ("__brand", "__memory", "__hard_disk")
def __init__(self, brand, memory, hard_disk):
"""
:param brand: 品牌
:param memory: 内存
:param hard_disk: 硬盘
"""
self.__brand = brand
self.__memory = memory
self.__hard_disk = hard_disk
def __repr__(self):
return f"Computer(brand={self.__brand}, memory={self.__memory}, hard_disk={self.__hard_disk})"
声明
__slots__
后动态属性就不能用了。macbook = Computer("苹果", "8G", "256G")
macbook.brand = "小米" # 尝试动态属性报错,AttributeError: 'Computer' object has no attribute 'brand'
__slots__
实际上是在创建对象的时候将分配给对象的字典改成不可变容器(元素),由于元组是不能增加数据的,因此动态属性也就失效了。- 同等长度下,元组开辟的内存空间比字典小。
- 因此声明了
__slots__
后的内存占用会比声明之前小。
注意:
__slots__
限制之后__dict__
就不能在用了。print(macbook.__dict__) # 报错:AttributeError: 'Computer' object has no attribute '__dict__'
1.4 方法属性化
1.4.1 方法属性化的基本概念
方法属性化是基于私有化,并且提供了get/set方法基础上实现的。
- 要访问私有化的数据,就需要调用get/set方法,而方法属性化就是要让调用get/set方法看起来像使用一个普通属性一样。
之前访问私有化属性都使用
get_file()
/set_file(value)
方法: ```python class Person: slots = (“name”, “__age”)def init(self, name, age):
self.name = name
self.__age = age
def get_age(self):
return self.__age
def set_age(self, age):
if age <= 0:
raise ValueError("年龄不能小于0")
else:
self.__age = age
构造对象
lele = Person(“乐乐”, 18)
修改与获取普通属性的值
lele.name = “大乐” name = lele.name print(name) # 大乐
修改与获取私有化属性的值
lele.set_age(21) age = lele.get_age() print(age) # 21
- 而方法属性化就是用`obj.get_file`获取对象的私有化属性(即将get_file()的小括号去掉,让它变成属性的形式)。
- 同理,用`obj.file = value`的方式给私有化属性赋值(代替`obj.set_file(value)`的形式)。
<a name="blaCj"></a>
#### 1.4.2 @property装饰get方法
- 在高阶函数中有提到,`obj.get_file`本质是`get_file`这个函数本身,`get_file()`才是调用这个方法。
```python
print(lele.get_age) # <bound method Person.get_age of <__main__.Person object at 0x000002074ED9EFD0>>
print(lele.get_age()) # 18
若要让
obj.get_file
实现obj.get_file()
的功能,就要在get方法的定义前使用@property
装饰器。 ```python class Person: slots = (“name”, “__age”)def init(self, name, age):
self.name = name
self.__age = age
@property def get_age(self):
return self.__age
def set_age(self, age):
if age <= 0:
raise ValueError("年龄不能小于0")
else:
self.__age = age
lele = Person(“乐乐”, 18) print(lele.get_age) # 18
print(lele.get_age()) # 报错:TypeError: ‘int’ object is not callable
意味着方法被@property修饰后,就变成一个属性了。
- 为了让其看起来更像是在调用属性,可用把`get_file`这个函数名就改成`file`。
```python
class Person:
__slots__ = ("name", "__age")
def __init__(self, name, age):
self.name = name
self.__age = age
@property
def age(self):
return self.__age
def set_age(self, age):
if age <= 0:
raise ValueError("年龄不能小于0")
else:
self.__age = age
lele = Person("乐乐", 18)
print(lele.age) # 18,看起来更像是在调用属性,但实际是在调用age()方法。
1.4.3 @get方法名.setter装饰set方法
普通属性用等号赋值,私有化属性用set方法赋值。
lele.name = "大乐"
lele.set_age(21)
set方法属性化后就可以用
obj.set_file = value
的形式给私有化属性赋值。同样的,set方法属性化也需要使用装饰器装饰set方法,这个装饰器为:
@get方法名.setter
。 ```python class Person: slots = (“name”, “__age”)def init(self, name, age):
self.name = name
self.__age = age
@property def age(self):
return self.__age
@age.setter def set_age(self, age):
if age <= 0:
raise ValueError("年龄不能小于0")
else:
self.__age = age
lele = Person(“乐乐”, 18) lele.set_age = 21 print(lele.age) # 21
- 同理,为了让其看起来更像是在调用属性,可用把`set_file`这个函数名就改成`file`。
- 根据装饰器是函数嵌套的本质,因此两个`file`方法实际上并不冲突。
```python
class Person:
__slots__ = ("name", "__age")
def __init__(self, name, age):
self.name = name
self.__age = age
@property
def age(self):
return self.__age
@age.setter
def age(self, age):
if age <= 0:
raise ValueError("年龄不能小于0")
else:
self.__age = age
lele = Person("乐乐", 18)
lele.age = 21
print(lele.age) # 21
"""
解释器会更具有无等号自动判断调用哪个age方法。
有等号的就是set,调用@age.setter修饰的age方法。
没有等号的就是get,调用@property修饰的age方法。
"""
02. 继承
2.1 继承的简介
2.1.1 继承的基本概念
- 继承是指从两个或两个以上的相关普通类(继承中称之为子类、衍生类)中提取出共同的特征和行为到一个通用类(继承中称之为父类、超类、基类)中,并通过指定的声明形式,使得子类可以使用父类中的信息,这种格式就被称之为继承。
- 子类和父类之间的关系:子类继承自父类,父类派生出子类。
只要子类和父类之间设置好联系,那么子类构造的对象就可以直接继承使用父类中的所有信息。
2.1.2 继承的基本语法
继承的基本语法: ```python class 通用父类: def init(self): # 用于实现通用属性声明以及其他相关的通用初始化操作
def 通用方法(self): # 定义那些通用的方法
class 子类(通用父类): def init(self): # 定义子类中特有的属性和初始化操作 通用父类.init(self, 父类属性) # 执行父类中的初始化操作。 子类初始化操作
def 子类中特有的方法(self):
- 示例:定义一个学生类和一个教师类,然后后继承的形式改写这两个类。
- 学生类:属性:学号、姓名、年龄、性别、成绩;行为:吃饭、学习。
- 教师类:属性:教师编号、姓名、年龄、性别、职称;行为:吃饭、教学。
```python
class Student:
def __init__(self, id, name, age, gender, score):
self.id = id
self.name = name
self.age = age
self.gender = gender
self.score = score
def eat(self):
print(f"{self.name}在吃饭")
def study(self):
print(f"{self.name}在学习")
def __repr__(self):
return f"Student(id={self.id}, name={self.name}, age={self.age}, gender={self.gender}, score={self.score})"
class Teacher:
def __init__(self, id, name, age, gender, title):
self.id = id
self.name = name
self.age = age
self.gender = gender
self.title = title # 职称
def eat(self):
print(f"{self.name}在吃饭")
def teach(self):
print(f"{self.name}在教学")
def __repr__(self):
return f"Teacher(id={self.id}, name={self.name}, age={self.age}, gender={self.gender}, title={self.title})"
student = Student(1001, "小明", 18, "男", 98)
teacher = Teacher(1001, "张老师", 25, "女", "语文老师")
print(student) # Student(id=1001, name=小明, age=18, gender=男, score=98)
print(teacher) # Teacher(id=1001, name=张老师, age=25, gender=女, title=语文老师)
可以发现,属性:id、name、age、gender和eat方法在两个类中都存在,因此可以把这部分内容提取到一个父类Person中,然后让Student、Teacher这两个类继承Person类即可。 ```python class Person: def init(self, id, name, age, gender): self.id = id self.name = name self.age = age self.gender = gender
def eat(self): print(f”{self.name}在吃饭”)
class Student(Person): def init(self, id, name, age, gender, score): Person.init(self, id, name, age, gender) # 执行父类中的初始化操作 self.score = score
def study(self):
print(f"{self.name}在学习")
def __repr__(self):
return f"Student(id={self.id}, name={self.name}, age={self.age}, gender={self.gender}, score={self.score})"
class Teacher(Person): def init(self, id, name, age, gender, title): Person.init(self, id, name, age, gender) # 执行父类中的初始化操作 self.title = title # 职称
def teach(self):
print(f"{self.name}在教学")
def __repr__(self):
return f"Teacher(id={self.id}, name={self.name}, age={self.age}, gender={self.gender}, title={self.title})"
student = Student(1001, “小明”, 18, “男”, 98) teacher = Teacher(1002, “张老师”, 25, “女”, “语文老师”) print(student) # Student(id=1001, name=小明, age=18, gender=男, score=98) print(teacher) # Teacher(id=1001, name=张老师, age=25, gender=女, title=语文老师)
<a name="U7GGD"></a>
### 2.2 三种父类初始化的方式
- 子类会自动拥有父类中声明的所有方法,但是无法直接给继承自父类的属性进行赋值,因此需要在子类的__init__()方法中初始化父类。
- 在子类中初始化父类有三种形式:`父类名.__init__()`、`super(子类类名, 子类对象).__init__()`、`super().__init__()`。
<a name="MmGU6"></a>
#### 2.2.1 父类名.__init__()
- 在子类的`__init__()`首行,使用`父类名.__init__()`可以初始化父类的结构(注意这里是用父类名调用__init__(),因此需要手动给self传值指定对象)。
```python
def __init__(self, id, name, age, gender, score):
Person.__init__(self, id, name, age, gender) # 执行父类中的初始化操作
self.score = score
2.2.2 super关键字
- Python中的
super
关键字代表着父类,可以利用super
创建父类对象,调用父类的__init__()
。 super关键字的语法结构:
super(子类类名, 子类对象).__init__()
。用super构造对象时,Python会默认将当前子类以及当前对象对象传递过去。
因此
super()
中的子类类名和子类对象可以不写。def __init__(self, id, name, age, gender, score):
super().__init__(id, name, age, gender)
self.score = score
-
2.3 方法重写
2.3.1 方法重写的概念
方法重写是指子类继承父类后,会获得父类中所有结构(包括方法)。但子类中有些方法可能实现起来与父类中的方法不一样,此时就可以使用方法重写来修改逻辑。
- 方法重写的两种场景:
- 子类与父类中方法的实现完全不一致。
- 子类方法中的实现与父类中部分一致。(在子类方法体中写子类特有的逻辑,然后在相同的部分中调用父类的方法即可)
方法重写的语法格式:
- 方法的声明需要与父类中完全一致(即方法名、形参列表需要保持完全一样)。
- 方法的方法体(具体功能的是实现)可以重新编写。
def 父类中的方法名(self, 父类中的):
方法体:子类中重写的逻辑
当子类重写父类中的方法后,从内存的角度来看,是子类中重写后的内容将从父类中继承过来的内容覆盖了。
因此用子类对象来进行调用的时,调用的是子类中重写的方法,父类中的结构已经不存在了,故无法直接用对象调用父类中未重写的方法,只能通过
super().方法名()
的形式调用。2.3.2 方法重写的应用案例
需求描述:
- 在愤怒的小鸟游戏中有许多不同颜色的小鸟,不同颜色的小鸟都有攻击的能力,但是攻击的具体表现行为是不一样的。
- 红鸟:特征【昵称】,行为【攻击(技能为死幢)】。
- 蓝鸟:特征【昵称】,行为【攻击(技能为分身、死撞)】。
- 黄鸟:特征【昵称】,行为【攻击(技能为加速、死撞)】。
- 黑鸟:特征【昵称】,行为【攻击(技能为爆炸)】。
- 要求以继承和方法重写的形式实现这个需求。
- 在愤怒的小鸟游戏中有许多不同颜色的小鸟,不同颜色的小鸟都有攻击的能力,但是攻击的具体表现行为是不一样的。
代码实现: ```python
所有鸟的父类
class Bird: def init(self, name):
self.name = name
def attack(self):
print("死撞")
class RedBrid(Bird):
# 因为红鸟中的结构与父类Bird中结构完全一样。
# 因此只需要继承Bird类即可,不需要声明其他东西。
pass
class BlueBird(Bird):
# 蓝鸟的属性与父类Bird中的属性完全一样,因此这里不需要再写__init__()方法。
def attack(self):
print("分身", end=",")
super().attack() # 与父类Bird中的attack()有相同的部分,因此直接调用即可。
class YellowBird(Bird): def attack(self): print(“加速”, end=”,”) super().attack()
class BlackBird(Bird): def attack(self): print(“爆炸”)
# 因为BlackBird子类的attack()与父类Bird中的attack()完全不一样。
# 因此这里只需要重写BlackBird子类的attack()中特有的逻辑即可,不需要调用父类中的方法。
rd = RedBrid(“愤怒红”) rd.attack() # 死撞
bd = BlueBird(“蓝弟弟”) bd.attack() # 分身,死撞
yb = YellowBird(“飞镖黄”) yb.attack() # 加速,死撞
bb = BlackBird(“炸弹黑”) bb.attack() # 爆炸
<a name="TWNy8"></a>
### 2.4 object类
<a name="RznMw"></a>
#### 2.4.1 __base__父类与mro()继承体系
- `类名.__base__`可以查看到指定类的父类(若一个类有多个直接父类,则使用`类名.__bases__`进行查看)。
- `类名.mro()`可以查看到一个类的继承体系(包括当前类、父类、爷爷类、太爷爷类、……)。
```python
print(BlackBird.__base__) # <class '__main__.Bird'>
print(BlackBird.mro()) # [<class '__main__.BlackBird'>, <class '__main__.Bird'>, <class 'object'>]
2.4.2 object根父类
- object类是所有类的根类(也被称为顶级父类),即Python中的所有类都直接或间接继承自object类。
- 当定义一个类并且没有声明继承结构时,它默认就是继承自object类的。 ```python class Test: pass
print(Test.base) #
- 像`__eq__`、`__init__`、`__dict__`等这些常用的方式都是定义在object类中的,因此Python所有的对象都可以直接使用这些方法,也可以根据实际需求重写这些方法。
<a name="MCk6M"></a>
### 2.5 多继承
- Python是支持多继承的,即一个子类可以有多个直接父类,子类会拥有多有父类中的所有结构。
- 注意:多继承在子类的`__init__()`方法中初始化父类中的结构时只能使用`父类名.__init__()`这种方式。
- 因为`super(子类类名, 子类对象).__init__()`这种方式只会将值传给第一个父类,即这里的A;第二个父类B拿不到值,无法进行初始化。
```python
class A(object):
def __init__(self, a):
self.a = a
def show_a(self):
print(f"a={self.a}")
class B(object):
def __init__(self, b):
self.b = b
def show_b(self):
print(f"b={self.b}")
class C(A, B): # 继承类A和类B
def __init__(self, a, b, c):
A.__init__(self, a) # 初始化第一个父类A的结构
B.__init__(self, b) # 初始化第二个父类B的结构
self.c = c # 初始化自己特有的结构
def show_c(self):
print(f"c={self.c}")
c = C(10, 20, 30)
c.show_a() # a=10
c.show_b() # b=20
c.show_c() # c=30