本文将主要介绍 Python 面向对象,包括类定义、各类属性、各类方法、继承、多态、封装、单例模式等。阅读本文预计需要 30 min
一文了解Python面向对象
- 1. 前言
- 2. 面向对象 VS 面向过程
- 3. 初窥类
- 3.1 类的定义
- 3.2 类对象和实例对象
- 4. 属性
- 4.1 类属性和实例属性
- 4.2 公有属性、半私有属性和私有属性
- 4.3 魔法属性
- 4.3.1 `__slots__` 魔法属性
- 5. 方法
- 5.1 类方法、实例方法和静态方法
- 5.2 公有方法、半私有方法和私有方法
- 5.3 魔法方法
- 5.3.1 `__init__()` 方法
- 5.3.2 `__new__()` 方法
- 5.3.3 `__str__()` 方法
- 5.3.4 `__del__()` 方法
- 5.3.5 `__iter__()` 和 `__next__()` 方法
- 5.4 @property
- 6. 继承
- 6.1 单继承
- 6.2 多继承
- 6.3 多层继承
- 6.4 方法解析顺序(MRO)
- 6.5 `isinstance()` 和 `issubclass` 函数
- 6.6 子类重写父类同名属性和方法
- 6.7 子类调用父类同名属性和方法
- 6.8 super()的使用
- 7. 封装
- 8. 多态
- 9. 单例模式
- 10. 小结
- 11. 巨人的肩膀
1. 前言
Python 是一门高级语言,掌握类的知识是我们使用 Python 进行面向对象编程非常重要的一步。这篇文章主要是结合官方文档类一节的教程,总结面向对象 VS 面向过程、类的创建、属性、方法、继承、多态、封装等方面的知识,并尽可能的详细,当然有些地方由于本人水平有限,不够完善,欢迎各位读者补充、指正。接下来就开始进入正文学习。
- 类对象、实例对象
- 类属性、实例属性、半私有属性、公有属性、私有属性、魔法属性(
__slots__
、__doc__
) - 类方法、实例方法、私有方法、公有方法、半公有方法、私有方法、魔法方法(
__init__()
/__new__()
/__del__()
/__str__()
等) - 继承、单继承、多继承、装饰器(
@property
、@classmethod
、@staticmethod
) - 单例模式
- 方法解析顺序(MRO)
- 多态
- 封装
2. 面向对象 VS 面向过程
先让我们了解一下什么是面向对象和面向过程。面向对象编程(Object oriented Programming, OOP)
:是一种程序设计思想,它把一个个对象作为基本单元
,其中对象是由若干数据和若干个操作数据的函数组成的集合体。面向对象把计算机程序看作一组对象的集合,对象之间可以互相接收信息,并对信息进行处理,而计算机的执行过程就是一系列消息在各个对象之间传递。
面向过程编程(Procedure oriented programming, POP)
:是把计算机程序视为一系列的命令集合,即一组函数的顺序执行。
下面通过【处理学生成绩】这个例子来说明面向过程和面向对象的不同
面向过程的做法是考虑如何一步步执行流程,从而得到结果。
student1 = {'name': 'Jock', 'score': 100}
student2 = {'name': 'Kobe', 'score': 98}
def print_score(student):
"""打印学生成绩信息"""
print(f"{student['name']}: {student['score']}")
print_score(student1)
print_score(student2)
结果输出:
Jock: 100
Kobe: 98
面向对象的做法是先考虑将 Student
作为一个对象(object),这个对象有 name
和 score
两个属性(attribute)
,然后还有一个“行为”,即 print_score
方法(method)
,在实例对象中的函数,一般我们称之为方法。具体做法如下:
class Student(object):
"""自定义一个 Student 对象"""
def __init__(self, name, score):
self.name = name
self.score = score
def print_score(self):
print(f"{self.name}: {self.score}")
Jock = Student('Jock', 100)
Kobe = Student('Kobe', 99)
Jock.print_score()
Kobe.print_score()
结果输出:
Jock: 100
Kobe: 99
两种方法各有优劣,不过面向对象的编程相对更加接近我们人类社会的习惯。
面向对象的三大特点是:
- 封装
- 继承
- 多态
上面的代码还不明白?不要紧,下面我们都会详细讲解。
3. 初窥类
计算机发展本身是一个不断抽象的过程,最终实现让计算机如同人一般思考,为人类服务。类
是对对象
的进一步抽象,类是具有相似内部状态和运动规律的实体的集合,或者说具有相同属性和行为事务的统称。自然界中有类(Class
)和实例(Instance
),Class 是抽象的概念,比如:我们刚刚定义的 Student
,是指学生这个概念,这跟我们初中学的生物分类“界门纲目科属种”差不多,Student 是类,Jock 和 Kobe 是 Student 这个类的实例(Instance)。
Python 中的类是 C++ 和 Modula-3 两种语言中类机制的结合。
3.1 类的定义
最简单的类定义是这样的:
class ClassName: # 经典类(旧式类) 定义形式
<statement-1>
...
<statement-N>
class ClassName(object): # 新式类定义形式
<statement-1>
...
<statement-N>
这样我们就完成了类定义。有几点需要说明一下:
-
class
是定义类的关键字,如def
是定义函数的关键字。类名的命名规则按照大驼峰命名法
,即所有单词首字母大写。 - 定义类有两种形式:新式类和旧式类,两种方式都可以,目前个人倾向于新式类。其中
object
是 Python 中所有类的最顶级父类,继承的时候会再讲。 - 类定义的时候,会创建一个命名空间,作为局部作用域。类中的变量和函数都会放到这个命名空间。
- 当完成类定义时,就会创建一个
类对象(class object)
。 这基本上是一个包围在类定义所创建命名空间内容周围的包装器。原始的(在进入类定义之前起作用的)局部作用域将重新生效,类对象将在这里被绑定到类定义头所给出的类名称。
掌握怎么定义类,以及相应的命名约定。后面两点暂时不太明白也没关系,这里我翻译的不是很好,以后大家可以再看看英文文档,慢慢理解。接下来我们学习完成定义类之后可以做什么?
3.2 类对象和实例对象
类对象(class object)支持两种操作:属性引用(attribute references)
和实例化(instantiation)
。
属性引用的语法是:obj.name
。有效的属性名称是类对象被创建时存在于类命名空间中的所有名称(所有的变量名和函数名)。不过,通常我们把类中的变量当做属性(attribute)
,把类中定义的函数当做方法(method)
。函数和方法在很多中文教程中基本不加区分,大家能明白它们的意思就行。
类的实例化语法是:ClassName()
,可以发现类的实例化使用了函数表示法。 把类对象当做一个不带参数的函数,这个函数返回的是该类的一个新实例。
举个栗子:
class Hero(object): # 新式类定义形式
"""A simple example class"""
game = "王者荣耀"
def move(self):
"""实例方法"""
print("全军出击!")
# Hero.game是类数据属性引用
print(Hero.game) # 打印Hero.game
# Hero.move类函数属性引用
print(type(Hero.move)) # 打印Hero.move的类型,结果:function
# 类实例化,创建实例对象 hero
hero = Hero()
# 类的文档说明
print(Hero.__doc__)
print(hero.game) # 打印实例对象hero的game属性
print(type(hero.move)) # 打印hero.move的类型,结果:method
hero.move() # 调用实例对象hero的方法move()
结果输出:
王者荣耀
<class 'function'>
A simple example class
王者荣耀
<class 'method'>
全军出击!
说明:
- “”“A simple example class”"" 是类的文档字符串,和函数文档字符串一样,可以通过
__doc__
这个魔法属性获取。 - game 是类 Hero 的属性,move 是类 Hero 的函数。注意这里我们不能通过 Hero.move()的形式调用 move 函数。
-
Hero.name
和Hero.move
都是有效的属性引用,将分别返回一个字符串和一个函数对象。 -
hero = Hero()
创建类 Hero 的实例对象并赋值给局部变量 hero。hero
称为实例对象。 - 实例对象只有一种操作:属性引用(attribute reference)。包括数据属性(data attribute)和方法(method)。
- 数据属性:对应于 Smalltalk 中的实例变量或者 C++中的数据成员,Python 实例对象数据属性不需要声明,和局部变量一样,它们在第一次赋值时产生。
- 方法:实例对象中的函数称为方法。在上面的例子中,hero.move 是有效的方法引用,因为 Hero.move 是一个函数,但 hero.game 不是方法,因为 Hero.name 不是一个函数。 但是 hero.move 与 Hero.move 并不是一回事,hero.move 是一个方法对象,Hero.move 是函数对象。简单理解就是类对象定义里面函数叫函数,实例对象里面的函数叫做方法。
看到这里我们梳理一下对象、类对象、实例对象
三者的关系和区别:
- 对象(object):在 Python 中
一切皆对象
。类对象、实例对象都属于对象,函数、列表、元组等等也都可以当做对象。所以当别人说对象的时候,你最好反应一下是哪种对象? - 类对象(class object):用关键字 class 定义类,当完成类定义时就会创建一个类对象,并且把类名绑定到这个类对象。上面这个例子中 Hero 就是一个类对象。
- 实例对象(instance object):类对象实例化生成的对象,是一个具体的存在。如 hero 就是类 Hero 的一个实例,Jock = Hero(),则 Jock 是另一个实例。类对象可以被当做一个模板,可以用它生成出多个实例对象。
4. 属性
类
是对对象
的进一步抽象,是具有相同属性和行为事务的统称。类的属性根据不同的分类方式可分为不同的类别。
- 根据是否被每个实例所共享分类,可以把属性分为
类属性
和实例属性
。 - 根据访问权限分,可以分为
共有属性
、半私有属性
和私有属性
。
4.1 类属性和实例属性
一般来说,实例属性是每个实例对象单独拥有的属性,而类属性是类对象所拥有的属性,也是该类所有实例共享的属性,在内存中只存在一个副本,这个和 C++中类的静态成员变量有点类似。这有点像,实例属性是个性,类属性是共性。
举个栗子:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 类属性,所有实例对象共有
def __init__(self, name):
self.name = name # 实例属性,每个实例都单独有一个
zhuang_zhou = Hero('庄周')
hou_yi = Hero('后裔')
print(f"zhuang_zhou.game是{zhuang_zhou.game},hou_yi.name是{hou_yi.game}")
print(f"zhuang_zhou.game是的内存地址是{id(zhuang_zhou.game)},hou_yi.game的内存地址是{id(hou_yi.game)}")
print(f"zhuang_zhou.name是{zhuang_zhou.name},hou_yi.name是{hou_yi.name}")
print(f"zhuang_zhou.name是的内存地址是{id(zhuang_zhou.name)},hou_yi.name的内存地址是{id(hou_yi.name)}")
结果输出:
zhuang_zhou.game是王者荣耀,hou_yi.name是王者荣耀
zhuang_zhou.game是的内存地址是1989739646864,hou_yi.game的内存地址是1989739646864
zhuang_zhou.name是庄周,hou_yi.name是后裔
zhuang_zhou.name是的内存地址是1989738801072,hou_yi.name的内存地址是19897385531368
可以看到,类属性 game 被所有实例对象(zhuang_zhou 和 hou_yi)共享,zhuang_zhou.game 和 hou_yi.game 指向同一个内存地址。类属性是各个实例的共性!
self.name 是实例属性,每个实例之间不共享实例属性。实例属性是各个实例的个性!
实例属性和类属性怎么区分呢?
实际上 self 代表的就是实例对象本身,以在函数内以 self.xxx = value
形式赋值的就是实例属性,在函数外以 xxx = value
形式赋值的就是类属性。
但是在函数外,以self.xxx = value
形式赋值会报错,同时在函数内以 xxx = value
形式赋值并不会得到相应的类属性,也不会得到一个实例属性。
举例如下:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 类属性,所有实例对象共有
# self.hp = 6666 # NameError: name 'self' is not defined
def __init__(self, name):
self.name = name # 实例属性,每个实例都单独有一个
age = 25 # 这既不是类属性,也不是实例属性
zhuang_zhou = Hero('庄周')
print(zhuang_zhou.age)
输出结果:
AttributeError: 'Hero' object has no attribute 'age'
所以我们要设置实例属性一定要在函数里面以 self.xxx = value
的形式赋值。self
就代表实例。age = 25
在函数里面,所以它不是一个类属性,它也不是一个实例属性,因为它前面没有 self.
,没有这个前缀,这个变量就无法绑定到实例对象上。
注意,共享数据在涉及可变对象时会引发意想不到的结果
。所以类属性不要设置为可变对象,如列表,字典等
,这个和函数里面默认参数值不要设置为可变类型一样。
如下面代码中的 tricks 列表不应该作为类变量,因为所有的 Dog 实例将共享这唯一一个单独的列表。下面是官网的例子:
class Dog(object):
tricks = [] # mistaken use of a class variable
def __init__(self, name):
self.name = name # 实例属性
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks # unexpectedly shared by all dogs
['roll over', 'play dead']
正确的做法如下:
class Dog(object):
def __init__(self, name):
self.name = name
self.tricks = [] # creates a new empty list for each dog
def add_trick(self, trick):
self.tricks.append(trick)
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']
我们如何访问类属性和实例属性呢?
答案是可以通过类引用或者实例引用直接访问相应的属性。
举个栗子:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 类属性,所有实例对象共有
def __init__(self, name):
self.name = name # 实例属性,每个实例都单独有一个
zhuang_zhou = Hero('庄周') # 创建实例化对象
print(f"通过类引用访问类属性game:{Hero.game},id是:{id(Hero.game)}")
print(f"通过实例引用访问类属性game:{zhuang_zhou.game},id是:{id(zhuang_zhou.game)}")
print(f"通过实例引用访问实例属性name:{zhuang_zhou.name},id是:{id(zhuang_zhou.name)}")
# print(f"通过类引用访问实例属性name:{Hero.name},id是:{id(Hero.name)}") # AttributeError: type object 'Hero' has no attribute 'name'
结果输出:
通过类引用访问类属性game:王者荣耀,id是:2403988901776
通过实例引用访问类属性game:王者荣耀,id是:2403988901776
通过实例引用访问实例属性name:庄周,id是:2403986759472
从上面可以发现,类属性可以被类对象和实例对象访问,而实例属性只能被实例对象访问,不能通过类对象访问。
那么我们如何修改类属性和实例属性呢?
方法如下:
- 修改类属性:必须通过类对象的引用来修改类属性。
- 实例属性:通过实例对象的引用来修改实例属性。
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 类属性,所有实例对象共有
def __init__(self, name):
self.name = name # 实例属性,每个实例都单独有一个
zhuang_zhou = Hero('庄周') # 创建实例化对象
print(f"通过类引用修改前,类属性game为:{Hero.game}")
print(f"通过实例对象访问修改前类属性game,类属性game为:{zhuang_zhou.game}")
Hero.game = "刺激战场"
print(f"通过类引用修改后,类属性game为:{Hero.game}")
print(f"通过实例对象访问修改后类属性game,类属性game为:{zhuang_zhou.game}")
输出结果:
通过类引用修改前,类属性game为:王者荣耀
通过实例对象访问修改前类属性game,类属性game为:王者荣耀
通过类引用修改后,类属性game为:刺激战场
通过实例对象访问修改后类属性game,类属性game为:刺激战场
我们可以发现,通过类引用修改了类属性,会导致所有的实例对象类属性都会改变,因为类属性是所有实例对象共享的。
那么能不能通过实例引用来修改类属性呢?
答案是不能。
测试如下:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 类属性,所有实例对象共有
def __init__(self, name):
self.name = name # 实例属性,每个实例都单独有一个
zhuang_zhou = Hero('庄周') # 创建实例化对象
hou_yi = Hero('后裔') # 创建实例化对象
print(f"通过实例引用修改类属性前,类属性game为:{Hero.game}")
print(f"通过zhuang_zhou引用修改类属性前,类属性game为:{zhuang_zhou.game}")
print(f"通过hou_yi实例访问修改前类属性,类属性game为:{hou_yi.game}")
print("-" * 20)
# 这会为zhuang_zhou这个实例对象创建一个实例属性,而不会修改类属性
zhuang_zhou.game = "刺激战场"
print(f"通过实例引用修改类属性后,类属性game为:{Hero.game}")
print(f"通过zhuang_zhou引用尝试修改类属性后,类属性game为(实际上这里访问的是实例属性game):{zhuang_zhou.game}")
print(f"通过hou_yi实例访问修改后类属性,类属性game为:{hou_yi.game}")
print("-" * 20)
del zhuang_zhou.game # 删除实例对象zhuang_zhou的实例属性game,但类属性game依然存在
print(f"通过类对象访问类属性,类属性game为:{Hero.game}")
print(f"删除zhuang_zhou实例属性game后,类属性game为:{zhuang_zhou.game}")
print(f"通过hou_yi实例访问类属性,类属性game为:{hou_yi.game}")
输出结果:
通过实例引用修改类属性前,类属性game为:王者荣耀
通过zhuang_zhou引用修改类属性前,类属性game为:王者荣耀
通过hou_yi实例访问修改前类属性,类属性game为:王者荣耀
--------------------
通过实例引用修改类属性后,类属性game为:王者荣耀
通过zhuang_zhou引用尝试修改类属性后,类属性game为(实际上这里访问的是实例属性game):刺激战场
通过hou_yi实例访问修改后类属性,类属性game为:王者荣耀
--------------------
通过类对象访问类属性,类属性game为:王者荣耀
删除zhuang_zhou实例属性game后,类属性game为:王者荣耀
通过hou_yi实例访问类属性,类属性game为:王者荣耀
可以看到,并不能通过实例对象修改类属性,尝试使用实例对象修改类属性,只会在该实例对象中创建一个新的实例属性,原有的类属性依然存在。当实例属性和类属性同名时(产生冲突),优先访问的是实例属性,屏蔽掉类属性。
小结:
- 类属性不能设置为可变类型。
- 实例属性以在函数内
self.xxx = value
的形式定义,类对象在函数外以xxx = value
的形式定义。 - 类属性可以被类对象和实例对象访问,而实例属性只能被实例对象访问,不能通过类对象访问。
- 通过类引用修改了类属性,会导致所有的实例对象类属性都会改变,因为类属性是所有实例对象共享的。
- 当实例属性和类属性同名时(产生冲突),优先访问的是实例属性,屏蔽掉类属性。
- 实例对象修改类属性,尝试使用实例对象修改类属性,只会在该实例对象中创建一个新的实例属性,原有的类属性依然存在。因此,在类外修改类属性,必须通过类引用。
4.2 公有属性、半私有属性和私有属性
公有、半私有、私有的区别在于能否在类外被访问。
- 公有属性:在类外能够被调用者访问或者修改的属性,包括公有类属性和公有实例属性。通常形式是
xxx = value
或者self.xxx = value
。 - 半私有属性:其实属于私有属性,这里只是多做了个区分。尽管在类外能够被调用者访问或者修改,包括半私有类属性和半私有实例属性,但是一般不建议调用者访问或者修改,因为这些半私有属性通常属于开发者测试阶段,可能会后期被删除。通常形式是
_xxx = value
或者self._xxx = value
。大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如_spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。 - 私有属性:在类外不能够被调用者访问或者修改的属性,但是可以在本类内部访问,包括私有类属性和私有实例属性。通常形式是
__xxx = value
或者self.__xxx = value
。类的私有属性,都不会被子类继承,子类也无法访问。私有属性往往来处理类的内部事情,不通过对象处理,起到安全作用。
举栗子:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 公有类属性,可以在类外访问
_version = 2020.05 # 半私有类属性,可以在类外访问
__money = 6666 # 私有类属性,类外部无法访问
def __init__(self, name):
self.name = name # 公有实例属性,可以在类外访问
self._sex = "男" # 半私有实例属性,可以在类外访问
self.__age = 4 # 私有实例属性,无法在类外访问
zhuang_zhou = Hero('庄周') # 创建实例化对象
print(Hero.game) # 王者荣耀
print(Hero._version) # 2020.05
# print(Hero.__money) # AttributeError: type object 'Hero' has no attribute '__money'
print(zhuang_zhou.game) # 王者荣耀
print(zhuang_zhou._version) # 2020.05
# print(zhuang_zhou.__money) # AttributeError: 'Hero' object has no attribute '__money'
print(zhuang_zhou.name) # 庄周
print(zhuang_zhou._sex) # 男
# print(zhuang_zhou.__age) # AttributeError: 'Hero' object has no attribute '__age'
从上面可以看到公有和半私有属性都是可以访问的,但是私有的属性都是无法访问。
实际上,私有的也是可以访问的,那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam
的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam
,其中 classname
为当前类名称,不是实例对象的名字。这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。
所以私有属性实际上也是可以访问的:
class Hero(object): # 新式类定义形式
game = "王者荣耀" # 公有类属性,可以在类外访问
_version = 2020.05 # 半私有类属性,可以在类外访问
__money = 6666 # 私有类属性,类外部无法访问
def __init__(self, name):
self.name = name # 公有实例属性,可以在类外访问
self._sex = "男" # 半私有实例属性,可以在类外访问
self.__age = 4 # 私有实例属性,无法在类外访问
zhuang_zhou = Hero('庄周') # 创建实例化对象
print(Hero._Hero__money)
print(zhuang_zhou._Hero__money)
print(zhuang_zhou._Hero__age)
结果输出:
6666
6666
4
从上面我们可以看到,私有属性其实也是可以访问到的,但是一般我们不这么干。除非在测试中,我们可以这么访问私有属性。
名称改写有助于让子类重载方法而不破坏类内方法调用。以下是官网的例子,例如:
class Mapping(object):
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)
def update(self, iterable):
for item in iterable:
self.items_list.append(item)
__update = update # private copy of original update() method
class MappingSubclass(Mapping):
def update(self, keys, values):
# provides new signature for update()
# but does not break __init__()
for item in zip(keys, values):
self.items_list.append(item)
上面的示例即使在 MappingSubclass 引入了一个 __update
标识符的情况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update
而在 MappingSubclass 类中被替换为 _MappingSubclass__update
。这里涉及到继承,暂时弄不懂也没关系,可以先放着,以后再看。
请注意,改写规则的设计主要是为了避免意外冲突;访问或修改被视为私有的变量仍然是可能的。这在特殊情况下甚至会很有用,例如在调试器中。
如果需要修改一个对象的属性值,通常有两种方法:
- 直接修改:对象名.属性名 = 数据
- 间接修改:对象名.方法名()
私有属性不能直接访问,所以无法通过第一种方式修改,一般通过第二种方式修改私有属性的值:定义一个公有方法 set(),在这个公有方法内修改私有属性,再定义一个公有方法 get()访问这个私有属性。
举个栗子:
class Person(object):
def __init__(self):
self.name = "Jock" # 公有实例属性
self.__age = 25 # 私有实例属性,可以在类内部通过self调用,但不能通过对象访问
# 一般会定义 get_xxx()方法和set_XXX()方法来获取和修改私有属性
def get_age(self):
"""返回私有属性值"""
return self.__age
def set_age(self, age):
"""接收参数修改私有属性"""
self.__age = age
Jock = Person()
# print(Jock.__age) # 实例对象不能访问私有权限的属性和方法
# 可以通过访问公有方法set_age()来修改私有属性的值
Jock.set_age(18)
# 可以通过公有方法get_age()来获取公有属性的值
print(Jock.get_age())
结果输出:
18
小结:
- 公有属性:在类外能够被调用者访问或者修改的属性,包括公有类属性和公有实例属性。通常形式是
xxx = value
或者self.xxx = value
。 - 半私有属性:在类外能够被调用者访问或者修改的属性,包括半私有类属性和半私有实例属性,但是一般不建议调用者访问或者修改,因为这些半私有属性通常属于开发者测试阶段,可能会后期被删除。通常形式是
_xxx = value
或者self._xxx = value
。大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如_spam
) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。 - 私有属性:在类外不能够被调用者访问或者修改的属性,但是可以在本类内部访问,包括私有类属性和私有实例属性。通常形式是
__xxx = value
或者self.__xxx = value
。 - 类的私有属性,都不会被子类继承,子类也无法访问。私有属性往往来处理类的内部事情,不通过对象处理,起到安全作用。
- 私有属性一般通过 get()和 set()方法进行访问和修改。
- 私有属性可以非常访问,通过
obj._ClassName__xxx
来实现访问。
4.3 魔法属性
在 Python 中有一些属性是以双下划线开头,和双下划线结尾的,类似于 __xxx__
这样的属性,具有特殊功能,我们称为魔法属性。这部分建议看了继承部分后再回来看。
4.3.1 __slots__
魔法属性
__slots__
:可以用来限制类能添加的属性。
In [1]: class Student(object):
...: __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
...:
In [2]: s = Student() # 创建实例对象
IIn [3]: s.name = 'Jock' # 绑定属'name'
IIn [4]: s.age = 25 # 绑定属'age'
In [6]: s.score = 100 # 绑定属'score'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-6-312c53518669> in <module>
----> 1 s.score = 100 # 绑定属'score'
AttributeError: 'Student' object has no attribute 'score'
由于 score
没有被放到 __slots__
中,所以不能绑定 score
属性,试图绑定 score 将得到 AttributeError
的错误。
使用 __slots__
要注意,__slots__
定义的属性仅对当前实例起作用,对继承的子类是不起作用的。
In [7]: class GraduateStudent(Student):
...: pass
...:
In [8]: g = GraduateStudent()
In [9]: g.score = 100 # 可以绑定上去
除非子类中也定义 __slots__
,这样子类实例允许定义的属性就是自身的 __slots__
加上父类的__slots__
In [12]: class UndergraduateStudent(Student):
...: __slots__ = ('score', 'gender')
...:
...:
In [13]: f = UndergraduateStudent()
In [14]: f.name = 'Bob'
In [15]: f.age = 18
In [16]: f.score = 66
In [18]: f.gender = 'male'
In [19]: f.city = '武汉'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-19-94672d8905ba> in <module>
----> 1 f.city = '武汉'
AttributeError: 'UndergraduateStudent' object has no attribute 'city'
__slots__
小结:
- 通过在类定义中,用
__slots__ = ('attr1', 'attr1', ...)
来限制该类能添加的属性。如果添加('attr1', 'attr1', ...)
外的属性将得到AttributeError
的错误。 -
__slots__
只作用于本类的实例对象,对于继承类(子类)不起限制作用。 - 如果子类中也定义
__slots__
,那么子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
5. 方法
方法按照类似于属性的分标准,可以分为类方法、实例方法、静态方法以及公有方法、半私有方法和私有方法。
5.1 类方法、实例方法和静态方法
类方法
:需要用装饰器 @classmethod
标识,对于类方法,第一个参数必须是类对象,一般以cls
作为第一个参数(当然也可以用其他的名称的变量,但是大部分人都习惯以cls
作为第一个参数的名字,默认就好),能够通过实例对象和类对象去访问。类方法可以直接类名.方法名直接调用,也可以创建实例调用,类方法不能访问实例属性。静态方法
:需要用装饰器 @staticmethod
标识,对第一个参数没有要求,和普通函数一样。静态方法可以直接类名.方法名直接调用,也可以创建实例调用,静态方法不能访问实例属性。实例方法
:第一个参数必须为 self,代表实例对象,所以实例方法必须要创建实例才能调用。
举个栗子:
class Person(object):
def instance_method(self):
print(f"我是类{Person}的实例方法,只能被实例对象调用")
@staticmethod
def static_method():
print("我是静态方法")
@classmethod
def class_method(cls):
print("我是类方法")
Person.static_method()
Person.class_method()
# Person.instance_method() # TypeError: instance_method() missing 1 required positional argument: 'self'
Person.instance_method(Person)
print('----------------')
p = Person()
p.instance_method()
p.static_method()
p.class_method()
结果输出:
我是静态方法
我是类方法
我是类<class '__main__.Person'>的实例方法,只能被实例对象调用
----------------
我是类<class '__main__.Person'>的实例方法,只能被实例对象调用
我是静态方法
我是类方法
下面我们看一下,如何在三个方法中调用类属性和实例属性,以及互相调用。
class Person(object):
country = "中国" # 公有类属性
def __init__(self):
self.name = "Jock" # 公有实例属性
def instance_method(self):
# print(f"在实例方法中通过self.类属性方式调用类属性{country}") # NameError: name 'country' is not defined
print(f"在实例方法中通过self.类属性方式调用类属性{self.country}") # 实例方法中调用类属性
# print(f"在实例方法中通过self.实例属性方式调用实例属性{name}") # NameError: name 'name' is not defined
print(f"在实例方法中通过self.实例属性方式调用实例属性{self.name}") # 实例方法中调用实例属性
self.static_method() # 实例方法中调用静态方法
self.class_method() # 实例方法中调用类方法
@staticmethod
def static_method():
# print(f"在静态方法中调用类属性{country}") # NameError: name 'country' is not defined
print(f"在静态方法中通过类名.类属性方式调用类属性{Person.country}")
@classmethod
def class_method(cls):
# print(f"在类方法中通过cls.类属性方式调用类属性{country}") # NameError: name 'country' is not defined
print(f"在类方法中通过cls.类属性方式调用类属性{cls.country}")
p = Person()
print('----------------')
p.instance_method()
print('----------------')
p.static_method()
print('----------------')
p.class_method()
print('----------------')
Person.static_method()
print('----------------')
Person.class_method()
print('----------------')
# Person.instance_method() # TypeError: instance_method() missing 1 required positional argument: 'self'
Person.instance_method(p)
结果输出:
----------------
在实例方法中通过self.类属性方式调用类属性中国
在实例方法中通过self.实例属性方式调用实例属性Jock
在静态方法中通过类名.类属性方式调用类属性中国
在类方法中通过cls.类属性方式调用类属性中国
----------------
在静态方法中通过类名.类属性方式调用类属性中国
----------------
在类方法中通过cls.类属性方式调用类属性中国
----------------
在静态方法中通过类名.类属性方式调用类属性中国
----------------
在类方法中通过cls.类属性方式调用类属性中国
----------------
在实例方法中通过self.类属性方式调用类属性中国
在实例方法中通过self.实例属性方式调用实例属性Jock
在静态方法中通过类名.类属性方式调用类属性中国
在类方法中通过cls.类属性方式调用类属性中国
可以发现,由于实例属性和实例方法是个性,所以静态方法和类方法是无法访问调用实例方法和实例属性的。在静态方法中要调用类方法和类属性,必须用 类名.类属性
或者 类名.类方法
的形式调用访问。在类方法中要调用类方法和类属性,用 cls.类属性
或者 cls.类方法
的形式调用访问,其中 cls 就代表类对象本身,所以 cls 也可以用类名来替换。在实例方法中调用类属性和类方法或者实例属性和实例方法,只要前面加上 self.
即可,self 代表实例对象本身。
类方法还有一个用途就是可以在实例对象中对类属性进行修改:
class Person(object):
country = "中国" # 类属性
# 类方法,用 @classmethod进行装饰
@classmethod
def get_country(cls):
return cls.country
@classmethod
def set_country(cls, country):
cls.country = country
p = Person()
print(p.get_country()) # 可以通过实例对象引用
print(Person.get_country()) # 可以通过类访问
p.set_country("日本")
print(p.get_country())
print(Person.get_country())
结果输出:
中国
中国
日本
日本
实例对象调用类方法进行修改类属性后,通过类对象和实例对象访问的类属性都发生了改变。
小结:
- 从定义形式上:类方法的第一个参数是类对象
cls
,实例方法的第一个参数是实例对象self
,静态方法中不需要额外定义参数,和普通函数一样。 - 类方法和实例方法不能调用实例属性和实例方法。实例方法全部属性和方法都可以调用。
- 类方法通过 cls.xxx 形式调用类属性和类方法,静态方法通过 ClassName.xxx 形式调用类属性和类方法,实例方法通过 self.xxx 的形式代用所有属性和方法。
- 如果实例对象调用类方法,那么产生的影响会作用于类对象及其所有的实例对象。即类对象可以通过调用类方法来修改类属性或者类方法。
- 从定义形式上:类方法的第一个参数是类对象
cls
,那么通过cls
引用的必定是类对象的属性和方法;实例方法的第一个参数是实例对象self
,那么通过self
引用的可能是类属性、也有可能是实例属性(这个需要具体分析)。静态方法中不需要额外定义参数,因此在静态方法中引用类属性的话,必须通过类对象来引用。
5.2 公有方法、半私有方法和私有方法
这部分的规则和公有属性、半私有属性和私有属性一样。
公有方法:可以在类外部访问调用。通常以 xxx()
形式定义。
半私有方法:其实也属于私有方法,尽管能够在类外部调用访问,但是一般不建议使用。通常以 _xxx()
形式定义。
私有方法:只能在类中调用访问,不能在类外调用访问。通常以 __xxx()
形式定义。
因为跟属性那部分类似,这里简单举个例子:
class Person(object):
def print_name(self):
print("我是公有方法!")
print(f"在公有方法中访问半私有方法:{self._print_age()}")
print(f"在公有方法中访问私有方法:{self.__print_sex()}")
def _print_age(self):
print("我是半私有方法!")
def __print_sex(self):
print("我是私有方法!")
p = Person()
print("-" * 20)
p.print_name()
print("-" * 20)
p._print_age()
print("-" * 20)
# p.__print_sex() # AttributeError: 'Person' object has no attribute '__print_sex'
p._Person__print_sex() # 非常规访问私有方法
结果输出:
--------------------
我是公有方法!
我是半私有方法!
在公有方法中访问半私有方法:None
我是私有方法!
在公有方法中访问私有方法:None
--------------------
我是半私有方法!
--------------------
我是私有方法!
从上面可以看到,私有方法也是可以通过非常规途径访问的。
小结:
- 在属性名和方法名前面加上两个下划线
__
为私有属性和私有方法,都不能通过在类外直接访问,类的私有属性和私有方法,都不会被子类继承,子类也无法访问;私有属性和私有方法往往来处理类的内部事情,不通过实例对象处理,起到安全作用。
5.3 魔法方法
有魔法属性自然就有魔法方法(magic method),Python 的类中,两个下划线开始,两个下划线结束的方法,就是魔法方法,魔法方法通常以 __xxx__()
的形式出现。它们具有特殊的一些功能和用途。
5.3.1 __init__()
方法
实例化操作(“调用”类对象)会创建一个空对象。但是我们想创建带有特定初始状态的自定义实例,拥有自己的个性怎么办呢?__init__()
魔法方法就是干着事的。__init__()
:通常用来做属性初始化或赋值操作。创建带有特定初始状态的自定义实例。即定制化实例对象。如果为了能够完成自己想要的初始状态,可以自己定义 __init__()
方法。如果类没有写 __init__()
方法,Python 会自动创建,但是不执行任何操作。因此,一个类里无论自己是否编写 __init__()
方法,一定有 __init__()
方法。
class Hero(object):
"""定义一个英雄类"""
def __init__(self, name, skill, hp, atk, armor):
"""__init() 方法用来做变量初始化 或 赋值操作,在类实例化对象的时候自动被调用"""
self.name = name # 姓名
self.skill = skill # 技能
self.hp = hp # 生命值
self.atk = atk # 攻击力
self.armor = armor # 护甲值
def info(self):
"""在类的实例方法中,通过self获取该对象的属性"""
print(f"英雄{self.name}的生命值:{self.hp}")
print(f"英雄{self.name}的攻击力:{self.atk}")
print(f"英雄{self.name}的护甲值:{self.armor}")
# 实例化英雄对象时,参数会传递到对象的 __init__() 方法里
zhuang_zhou = Hero("庄周", "化蝶", 6666, 1000, 900)
hou_yi = Hero("后裔", "炙热之风", 5000, 666, 999)
print("调用不同实例对象的方法")
zhuang_zhou.info()
print("-" * 20)
hou_yi.info()
print("-" * 20)
结果输出:
调用不同实例对象的方法
英雄庄周的生命值:6666
英雄庄周的攻击力:1000
英雄庄周的护甲值:900
--------------------
英雄后裔的生命值:5000
英雄后裔的攻击力:666
英雄后裔的护甲值:999
--------------------
-
__init__()
方法第一个参数一定是 self,表示实例对象本身,它在实例对象完成创建后,自动调用,从而初始化实例对象。 -
__init__()
方法,在创建一个实例对象时默认被调用,不需要手动调用; -
__init__()
方法中 self 参数不需要开发者传递, Python 解释器会自动把当前对象引用传递过去。
5.3.2 __new__()
方法
__new__()
方法是用来创建一个实例对象的。
class Person(object):
def __init__(self):
print("这是__init__()方法")
self.country = "中国"
def __new__(cls):
print("这是__new__()方法")
return object.__new__(cls)
Person()
输出:
这是__new__()方法
这是__init__()方法
__new__()
和__init__()
小结:
- 从分类上,
__new__()
方法是一个类方法,__init__()
是一个实例方法。 - 从作用上,
__new__()
方法用于创建新实例对象,__init__()
用于初始化一个实例化对象。 - 从语法上,
__new__()
方法第一个参数是cls
,代表要实例化的类,此参数在实例化时由 Python 解释器自动提供,同时必须要有返回值,返回实例化出来的实例,这点在自己实现__new__()
时要特别注意,可以 return 父类 new 出来的实例对象,或者直接是 object 的 new 出来的实例对象。__init__()
第一个参数是self
,就是这个__new__()
返回的实例对象,__init__()
在__new__()
的基础上可以完成一些其它初始化动作,__init__()
不需要返回值。 - 从执行的顺序上,先执行
__new__()
,再执行__init__()
。
5.3.3 __str__()
方法
__str()__
方法也是一个魔法方法,用来显示信息。该方法需要 return 一个数据,并且只有 self 一个参数,当在类的外部 print(对象),则打印这个数据。如果没有自定义这个 __str()__
方法,则默认打印实例对象的内存地址。
当类的实例化对象,拥有 __str()__
方法后,那么打印实例对象,则打印 __str()__
方法的返回值。
举例如下:
class Foo(object):
pass
class Hero(object):
"""定义一个英雄类,可以移动和攻击"""
def __init__(self, name, skill, hp, atk, armor):
"""__init() 方法用来做变量初始化 或 赋值操作,在类实例化对象的时候自动被调用"""
self.name = name # 姓名
self.skill = skill # 技能
self.hp = hp # 生命值
self.atk = atk # 攻击力
self.armor = armor # 护甲值
def move(self):
"""实例方法"""
print("全军出击!")
def attack(self):
"""实例方法"""
print(f"放出{self.skill}!")
def __str__(self):
"""魔法方法,用来显示信息"""
return f"英雄{self.name}数据:生命值 {self.hp},攻击力 {self.atk},护甲值 {self.armor}"
f = Foo()
print(f) # 没有自定义__str__(),默认打印实例对象f的内存地址
zhuang_zhou = Hero("庄周", "化蝶", 6666, 1000, 900)
hou_yi = Hero("后裔", "炙热之风", 5000, 666, 999)
# 自定义 __str__() 方法后,打印 __str()__ 方法返回值
print(zhuang_zhou)
print(hou_yi)
# 查看类的文档说明
print(Hero.__doc__)
结果输出:
<__main__.Foo object at 0x0000028F8665BF08>
英雄庄周数据:生命值 6666,攻击力 1000,护甲值 900
英雄后裔数据:生命值 5000,攻击力 666,护甲值 999
定义一个英雄类,可以移动和攻击
小结:
- print(object) 时,默认打印该对象的内存地址。如果类定义了
__str__(self)
方法,那么就会打印__str__(self)
方法的返回值。 -
__str__(self)
方法通常返回一个字符串,作为这个对象的描述信息。
5.3.4 __del__()
方法
__new__()
方法创建实例对象, __init__()
方法初始化实例对象。当彻底删除对象时,Python 解释器也会默认调用 __del__()
方法。
class Hero(object):
"""定义一个英雄类,可以移动和攻击"""
def __init__(self, name):
"""__init() 方法是一个初始化方法,用来做变量初始化 或 赋值操作。
在创建完对象后自动被调用"""
print("__init__()方法被调用")
self.name = name
def __del__(self):
"""当对象删除时,会被自动调用"""
print("___del__()方法被调用")
print(f"{self.name}被淘汰了!")
def __str__(self):
"""魔法方法,用来显示信息"""
return f"英雄{self.name}淘汰!"
# 创建对象
zhuang_zhou = Hero("庄周")
print("-" * 20)
# 删除对象
print(f"{id(zhuang_zhou)}被删除1次")
del(zhuang_zhou)
print("-" * 20)
hou_yi = Hero("后裔")
hou_yi1 = hou_yi
hou_yi2 = hou_yi
print(f"{id(hou_yi)}被删除第1次")
del(hou_yi)
print(f"{id(hou_yi1)}被删除第2次")
del(hou_yi1)
print(f"{id(hou_yi2)}被删除第3次")
del(hou_yi2)
结果输出:
__init__()方法被调用
--------------------
2133117525512被删除1次
___del__()方法被调用
庄周被淘汰了!
--------------------
__init__()方法被调用
2133117525512被删除第1次
2133117525512被删除第2次
2133117525512被删除第3次
___del__()方法被调用
后裔被淘汰了
总结:
- 当有变量保存了一个对象的引用时,该对象的引用计数就会 +1。
- 当使用
del()
删除变量指向的对象时,则会减少对象的引用计数。如果对象的引用计数不为 1,那么会让这个对象的引用计数 -1,当对象的引用计数为 0 时,这个对象才会被真正的删除(内存被回收)。 -
___del__()
方法在实例对象引用计数为 0 时会自动调用。
5.3.5 __iter__()
和 __next__()
方法
__iter__()
和 __next__()
方法用于给类添加迭代器行为。定义一个 __iter__()
方法来返回一个带有 __next__()
方法的对象。如果类已定义了 __next__()
,则 __iter__()
可以简单地返回 self,这里给出官方的例子。
class Reverse(object):
"""Iterator for looping over a sequence backwards."""
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
... print(char)
...
m
a
p
s
5.4 @property
【问题】
在绑定属性时,我们直接把属性暴露出去,虽然写起来简单,但是没办法检查参数,导致属性可以随意修改。如对于学生 Student
这个类:s = Student()
s.score = 66666
学生分数为 6666,这显然不合逻辑。
【解决方案】
为了限制 score 范围,可以通过一个 set_score
方法来设置成绩,再通过一个get_score
方法来获取成绩,这样在set_score
方法里就可以检查参数:
class Student(object):
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, int):
raise ValueError('Score must be an integer')
if value < 0 or value > 100:
raise ValueError('Score must between 0~100')
self._score = value
这样,对任意的 Student 实例进行操作,都不能随心所欲地设置 score 了。
In [2]: s = Student()
In [3]: s.set_score(100) # ok!
In [4]: s.get_score()
Out[4]: 100
In [5]: s.set_score(6666) # error
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-5-497d0a8bc0b3> in <module>
----> 1 s.set_score(6666) # error
<ipython-input-1-edf7716397d7> in set_score(self, value)
8 raise ValueError('Score must be an integer')
9 if value < 0 or value > 100:
---> 10 raise ValueError('Score must between 0~100')
11 self._score = value
ValueError: Score must between 0~100
但是这样的调用方法又略显复杂,没有直接用属性,那么简单直接。
如何实现既能检查参数,又能用类似属性这样简单的方式来访问类的变量呢?
通过装饰器(decorator)可以动态和函数加上功能。对于类方法,装饰器一样起作用。
Python 内置的 @property
装饰器就是负责把一个方法变成属性调用的:
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('Score must be an integer')
if value < 0 or value > 100:
raise ValueError('Score must between 0~100')
self._score = value
@property
的实现比较复杂,我们先考虑如何使用。把一个 getter
方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器 @score.setter
,负责把一个 setter
方法变成属性赋值,于是我们就拥有一个可控的属性操作:
In [2]: s = Student()
In [3]: s.score = 99 # OK!,实际转化为s.set_score(99)
In [4]: s.score # OK,实际转化为s.get_score()
Out[4]: 99
In [5]: s.score = 666
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-5-3afa376b49cc> in <module>
----> 1 s.score = 666
<ipython-input-1-9bf23040a06a> in score(self, value)
10 raise ValueError('Score must be an integer')
11 if value < 0 or value > 100:
---> 12 raise ValueError('Score must between 0~100')
13 self._score = value
14
ValueError: Score must between 0~100
注意到这个神奇的 @property
,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过 getter
和 setter
方法来实现的。
还可以定义只读属性,只定义 getter
方法,不定义 setter
方法就是一个只读属性:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2020 - self._birth
上面的 birth
是可读写属性,而 age
就是一个只读属性,因为 age
可以根据 birth 和当前时间计算出来。
小结:@property
广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。
6. 继承
继承(inheritance)是描述多个类之间的所属关系。好比爷爷、爸爸、儿子这样的关系。如果不支持继承,语言特性就不值得称为类。
如果一个类 A 的属性和方法可以复用,则可以通过继承的方式,传递到类 B。
那么类 A 就是基类(base class)
,也叫做父类;类 B 就是派生类(derived class)
,也叫做子类。
class Father(object):
"""这是父类"""
def __init__(self):
self.age = 50
def print_age(self):
print(self.age)
class Son(Father):
"""这是子类,继承 Father 类"""
pass
son = Son()
print(son.age)
son.print_age()
结果输出:
50
50
6.1 单继承
子类只继承一个父类。
子类可以继承父类的所有属性和方法,哪怕子类没有自己的属性和方法,也可以使用父类的属性和方法。
class Father(object):
"""定义 Father 类"""
def __init__(self):
# 属性
self.car = "自行车"
def go_work(self):
print(f"通过{self.car}去上班")
class Son(Father):
"""定义 Son类,继承 Father 类"""
pass
Jock = Son() # 创建子类实例对象
print(Jock.car) # 子类对象直接使用父类的属性
Jock.go_work() # 子类对象直接使用父类的方法
结果输出:
自行车
通过自行车去上班
小结:
- 虽然子类没有自定义
__init__()
方法,也没有定义实例方法,但是父类有。所以只要创建子类的对象,就默认执行了那个继承过来的__init__()
方法。 - 子类在继承的时候,在定义类时,小括号()中为父类的名字。
- 父类的属性、方法、会被继承给子类。
6.2 多继承
子类继承多个父类。
class Father(object):
"""定义 Father 类"""
def __init__(self):
self.car = "自行车"
def go_work(self):
print(f"通过{self.car}去上班")
def black_hair(self):
print("爸爸的黑发!")
class Mother(object):
"""定义 Mother 类"""
def __init__(self):
self.car = "电动车"
def go_work(self):
print(f"通过{self.car}去上班")
def big_eyes(self):
print("妈妈的大眼睛!")
class Son(Father, Mother):
"""定义 Son类,继承 Father 类 和 Mother 类"""
pass
Jock = Son() # 创建子类实例对象
print(Jock.car) # 执行 Father 属性
Jock.go_work() # 执行 Father 的实例方法
# 子类的魔法属性 __mro__ 决定了属性和方法的查找顺序
print(Son.__mro__)
Jock.black_hair() # 不重名不受影响
Jock.big_eyes()
结果输出:
自行车
通过自行车去上班
(<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class 'object'>)
爸爸的黑发!
妈妈的大眼睛!
小结:
- 多继承可以继承多个父类,也继承了所有父类的属性和方法。
- 注意多个父类中有同名的属性和方法,则默认使用第一个父类的属性和方法。根据类的模法属性
__mro__
的顺序来查找。 - 多个父类中,不重名的属性和方法,不会有任何影响。
6.3 多层继承
多层继承即多次继承。
class Father(object):
"""定义 Father 类"""
def __init__(self):
self.car = "自行车"
def go_work(self):
print(f"爸爸通过{self.car}去上班")
class Mother(object):
"""定义 Mother 类"""
def __init__(self):
self.car = "电动车"
def go_work(self):
print(f"妈妈通过{self.car}去上班")
class Son(Father, Mother):
"""定义 Son类,继承 Father 类 和 Mother 类"""
def __init__(self):
self.car = "小轿车"
self.age = 25
def go_work(self):
self.__init__() # 执行本类的__init__方法,做属性初始化 self.car = "小轿车"
print(f"儿子通过{self.car}去上班")
# 调用父类方法格式:父类类名.父类方法(self)
def dad_go_work(self):
Father.__init__(self) # 调用父类Father的__init__方法,self.car = "自行车"
Father.go_work(self) # 调用父类Father的实例方法
def mom_go_work(self):
Mother.__init__(self) # 调用父类 Mother 的__init__方法,self.car = "电动车"
Mother.go_work(self) # 调用父类Mother的实例方法
class Grandson(Son):
"""定义类 Grandson, 多层继承"""
pass
Bob = Grandson()
Bob.go_work()
Bob.dad_go_work()
Bob.mom_go_work()
print(Bob.age)
输出:
儿子通过小轿车去上班
爸爸通过自行车去上班
妈妈通过电动车去上班
25
6.4 方法解析顺序(MRO)
方法解析顺序(Method Resolution Order, MRO),对于多继承和多层继承很重要的。
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
对于多数应用来说,在最简单的情况下,你可以认为搜索从父类所继承属性的操作是深度优先、从左至右的
,当层次结构中存在重叠时,不会在同一个类中搜索两次
。因此,如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜索它,然后(递归地)到 Base1 的基类中搜索,如果在那里未找到,再到 Base2 中搜索,依此类推。
具体官网文档是:Method Resolution Order
在写多层继承时,需要注意一下父类的定义顺序和继承顺序
,可以参看TypeError: Cannot create a consistent method resolution order (MRO) 在查找实例属性/方法时,Python 需要确定以哪种顺序搜索(直接和间接)基类。 它通过使用 C3 或 MRO 算法将继承图线性化(即通过将基类的图转换为序列)来实现此目的。 MRO 算法是一种独特的算法,可以实现几个理想的属性:
- 每个祖先类只出现一次
- 一个类总是出现在其祖先之前(“单调性”),即继承时,
- 同一类的直接父级应该以与类定义中列出的顺序相同的顺序出现(“一致的本地优先顺序”)
- 如果 A 类的子代始终出现在 B 类的子代之前,则 A 应该出现在 B 子之前(“一致的扩展优先顺序”)
换句话说就是(这部分有待进一步深入了解):
- 多个父类不能有重复,如 Son(Father, Father)。
- 多个父类之间是继承关系时,子类写在父类前面,如 A(object), B(A), 那么 C(B, A),如果写成 C(A, B)则会报错 TypeError,具体可以看前面给出的链接。
- 多个父类是并列关系时,继承顺序要和定义父类的顺序一致。
- 如果 A 类的子代始终出现在 B 类的子代之前,则 A 应该出现在 B 子之前;
6.5 isinstance()
和 issubclass
函数
Python 有两个内置函数可被用于继承机制:
isinstance()
:用来检查一个实例对象的类型,如:isinstance(obj, int) ,obj 为 int 类型或者继承自 int 类型时,返回 True,反之返回 False。
issubclass()
:用来检查类的继承关系,如:issubclass(bool, int) 为 True,因为 bool 是 int 的子类。 但是,issubclass(float, int) 为 False,因为 float 不是 int 的子类。
class A(object):
pass
class B(object):
pass
class C(A):
pass
a = A()
print(f"a是A的实例:{isinstance(a, A)}")
print(f"a是B的实例:{isinstance(a, B)}")
print(f"C是A的子类:{issubclass(C, A)}")
print(f"C是B的子类:{issubclass(C, B)}")
结果输出:
a是A的实例:True
a是B的实例:False
C是A的子类:True
C是B的子类:False
6.6 子类重写父类同名属性和方法
如果子类和父类的方法名和属性名相同,则默认使用子类的,叫做子类重写父类的同名方法和属性。
class Father(object):
"""定义 Father 类"""
def __init__(self):
self.car = "自行车"
def go_work(self):
print(f"通过{self.car}去上班")
def black_hair(self):
print("爸爸的黑发!")
class Mother(object):
"""定义 Mother 类"""
def __init__(self):
self.car = "电动车"
def go_work(self):
print(f"通过{self.car}去上班")
def big_eyes(self):
print("妈妈的大眼睛!")
class Son(Father, Mother):
"""定义 Son类,继承 Father 类 和 Mother 类"""
def __init__(self):
self.car = "小轿车"
def go_work(self):
print(f"通过{self.car}去上班")
Jock = Son() # 创建子类实例对象
print(Jock.car) # 子类和父类有同名属性,则默认使用子类的
Jock.go_work() # 子类和父类有同名方法,则默认使用子类的
# 子类的魔法属性 __mro__ 决定了属性和方法的查找顺序
print(Son.__mro__)
输出结果:
小轿车
通过小轿车去上班
(<class '__main__.Son'>, <class '__main__.Father'>, <class '__main__.Mother'>, <class 'object'>)
6.7 子类调用父类同名属性和方法
重点:无论何时何地,self
都是表示子类的对象。在调用父类方法时,通过传递 self
参数,来控制方法和属性的访问修改。
class Father(object):
"""定义 Father 类"""
def __init__(self):
self.car = "自行车" # 实例变量,属性
def go_work(self):
"""实例方法,方法"""
print(f"通过{self.car}去上班")
class Mother(object):
"""定义 Mother 类"""
def __init__(self):
self.car = "电动车"
def go_work(self):
print(f"通过{self.car}去上班")
class Son(Father, Mother):
"""定义 Son类,继承 Father 类 和 Mother 类"""
def __init__(self):
self.car = "小轿车"
def go_work(self):
print(f"执行子类__init__方法前,self.car属性:{self.car}")
self.__init__() # 执行本类的__init__方法,做属性初始化,self.car = "小轿车"
print(f"执行子类__init__方法后,self.car 属性:{self.car}")
print(f"son 是通过{self.car}去上班!")
def dad_go_work(self):
# 不推荐这样访问父类的实例属性,相当于创建了一个新的父类对象
# print(f"直接调用 Father 类的 car 属性:{Father().car}")
# 可以通过执行 Father 类的 __init__ 方法,来修改 self 的属性值
print(f"执行 Father 的__init__方法前,self.car属性:{self.car}")
Father.__init__(self) # 调用父类 Father 的__init__方法,self.car = "自行车"
print(f"执行 Father 的__init__方法后,self.car属性:{self.car}")
Father.go_work(self) # 调用父类Father的实例方法
def mom_go_work(self):
# 不推荐这样访问父类的实例属性,相当于创建了一个新的父类对象
# print(f"直接调用 Mother 类的 car 属性:{Mother().car}")
# 可以通过执行 Mother 类的 __init__ 方法,来修改 self 的属性值
print(f"执行 Mother 的__init__方法前,self.car属性:{self.car}")
Mother.__init__(self) # 调用父类 Mother 的__init__方法,self.car = 电动车"
print(f"执行 Mother 的__init__方法后,self.car属性:{self.car}")
Mother.go_work(self) # 调用父类Mother的实例方法
Jock = Son() # 实例化对象,自动执行子类的__init__方法
Jock.go_work() # 调用子类的方法(默认重写了父类的同名方法)
print("-" * 20)
Jock.dad_go_work() # 进入实例方法去调用父类Father的方法
print("-" * 20)
Jock.mom_go_work() # 进入实例方法去调用父类Mother的方法
print("-" * 20)
Jock.go_work() # 调用本类的实例方法
结果输出:
执行子类__init__方法前,self.car属性:小轿车
执行子类__init__方法后,self.car 属性:小轿车
son 是通过小轿车去上班!
--------------------
执行 Father 的__init__方法前,self.car属性:小轿车
执行 Father 的__init__方法后,self.car属性:自行车
通过自行车去上班
--------------------
执行 Mother 的__init__方法前,self.car属性:自行车
执行 Mother 的__init__方法后,self.car属性:电动车
通过电动车去上班
--------------------
执行子类__init__方法前,self.car属性:电动车
执行子类__init__方法后,self.car 属性:小轿车
son 是通过小轿车去上班!
6.8 super()的使用
class Father(object):
"""定义 Father 类"""
def __init__(self):
self.car = "自行车"
def go_work(self):
print(f"爸爸通过{self.car}去上班")
# 父类是Father
class Son(Father):
"""定义 Son 类,单继承 Father"""
def __init__(self):
self.car = "电动车"
def go_work(self):
print(f"儿子通过{self.car}去上班")
super().__init__() # 执行父类的构造方法
super().go_work() # 执行父类的实例方法
# 父类是 Father 和 Son
# class Grandson(Father, Son): # 报错,TypeError: Cannot create a consistent method resolution order (MRO) for bases Father, Son
class Grandson(Son, Father):
"""定义类 Grandson, 多继承,继承多个父类"""
def __init__(self):
self.car = "小轿车"
def go_work(self):
self.__init__() # 执行本类的__init__方法,做属性初始化 self.car = "小轿车"
print(f"孙子通过{self.car}去上班")
def all_go_work(self):
# 方式1.指定执行父类的方法 (代码臃肿)
# Son.__init__(self)
# Son.go_work(self) # 这里打印儿子通过电动车去上班 和 爸爸通过自行车去上班
#
# Father.__init__(self)
# Father.go_work(self)
#
# self.__init__()
# self.go_work()
# 方法2. super() 带参数版本,只支持新式类
# super(Grandson, self).__init__() # 执行父类的__init__方法
# super(Grandson, self).go_work()
# self.go_work()
# 方法3. super()简化版,只支持新式类
super().__init__() # 执行父类的__init__方法
super().go_work() # 执行父类的实例方法
self.go_work() # 执行本类的实例方法
Jock = Grandson()
Jock.go_work()
print("-" * 20)
Jock.all_go_work()
结果输出:
孙子通过小轿车去上班
--------------------
儿子通过电动车去上班
爸爸通过自行车去上班
孙子通过小轿车去上班
小结:
- 子类继承了多个父类,如果父类的类名更改了,那么子类也要多次修改,而且需要重复写多次调用,显得代码臃肿。
- 使用 super() 可以逐一调用所有的父类方法,并且只执行一次。调用顺序遵循 mro 类属性的顺序。
- 注意:如果继承了多个父类,且父类都有同名方法,则默认只执行第一个父类的(同名方法只执行一次,目前 super() 不支持执行多个父类的同名方法)
- super() 是在 Python 2.3 之后才有的机制,用于通常单继承的多层继承。
7. 封装
封装是一种思想。生活中,出去旅行,要把所用到的东西分门别类摆放整齐的装进旅行箱里,就是封装。
计算机中,封装是指将数据与具体操作的实现代码放在某个对象内部,使这些代码的实现细节不被外界发现,外界只能通过接口使用该对象,而不能通过任何形式修改对象内部实现。
由于封装机制,程序在使用某一对象时,不需要关心该对象的数据结构细节及实现操作的方法。使用封装能隐藏对象实现细节,使代码更易维护。
同时因为不能直接调用、修改对象内部的私有属性,在一定程度上保证了系统安全性。
类通过将函数和变量封装在内部,实现了比函数更高一级的封装。
8. 多态
多态:定义时的类型和运行时的类型不一样,即称为多态。多态的概念是应用于 Java 和 C# 这一类强类型的语言中,而 Python 崇尚“鸭子类型”。
鸭子类型:我想要一只鸭子,虽然你给了我一只鸟,但是只要这只鸟走路像鸭子,叫起来像鸭子,游泳也像鸭子,我就认为这是鸭子。
Python 的多态就是弱化类型,重点在于对象参数是否有指定的属性和方法,如果有就认定合适,而不关心对象的类型是否正确。
class F1(object):
def show(self):
print('F1.show')
class S1(F1):
def show(self):
print('S1.show')
class S2(F1):
def show(self):
print('S2.show')
# 由于在Java或C#中定义函数参数时,必须指定参数的类型,为了让Func函数既可以执行S1对象的show方法,又可以执行S2对象的show方法
# 所以在 def Func 的形参中obj的类型是S1和S2的父类,即F1
def Func(F1 obj): # 这在 Python 里面会报错,需要写成 Func(obj)
"""Func 函数需要接收一个F1类型或者F1子类的类型"""
obj.show()
s1_obj = S1()
Func(s1_obj) # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S1.show
s2_obj = S2()
Func(s2_obj) # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S2.show
理解:定义 obj 这个变量是说的类型是 F1 的类型,但是在真正调用 Func 函数时给其传递的不一定是 F1 类的实例对象,有可能是其子类的实例对象,这种情况就是所谓的多态。
class F1(object):
def show(self):
print('F1.show')
class S1(F1):
def show(self):
print('S1.show')
class S2(F1):
def show(self):
print('S2.show')
def Func(obj):
"""Python 是弱类型,即无论传递过来的是什么,obj变量都能够指向它,这也就没有所谓的多态了(弱化了这个概念)"""
obj.show()
s1_obj = S1()
Func(s1_obj) # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S1.show
s2_obj = S2()
Func(s2_obj) # 在Func函数中传入S1类的对象 s1_obj,执行S1的show方法,结果:S2.show
9. 单例模式
单例是什么:我们电脑中的回收站,在整个操作系统中,回收站只能有一个实例,真个系统都是用这个唯一的实例,而且回收站自行提供自己的实例。因此回收站是单例模式的应用。
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,单例模式是一种对象创建型模式。
创建单例:保证只有 1 个实例对象
class Singleton(object):
__instance = None
def __new__(cls, age, name):
"""如果类属性__instance的值为None,
那么就创建一个对象,并且赋值为这个对象的引用,保证下次调用这个方法时,
能够指导之前已经创建过对象了,这样就保证了只有1个对象"""
if not cls.__instance:
cls.__instance = object.__new__(cls)
return cls.__instance
a = Singleton(18, "Jock")
b = Singleton(18, "Jock")
print(id(a))
print(id(b))
a.age = 19 # 给a指向的对象添加一个属性
print(b.age) # 获取b指向的对象的age属性
运行结果:
2438110470920
2438110470920
19
创建单例时,只执行一次__init__()
方法。
class Singleton(object):
"""单例"""
__instance = None
__is_first = True
def __new__(cls, age, name):
if not cls.__instance:
cls.__instance = object.__new__(cls)
return cls.__instance
def __init__(self, age, name):
if self.__is_first:
self.age = age
self.name = name
Singleton.__is_first = False
a = Singleton(18, "Jcok")
b = Singleton(25, "Jock")
print(id(a))
print(id(b))
print(a.age)
print(b.age)
a.age = 19
print(b.age)
输出结果:
1696365766536
1696365766536
18
18
19
10. 小结
- 掌握类的定义以及类的实例化。
- 区分类对象、实例对象、对象。
- 掌握类属性、实例属性、公有属性、私有属性的定义、访问和修改。
- 掌握类方法、静态方法、实例方法的定义、调用。
- 掌握
__slots__
、__doc__
、__mro__
等魔法属性。 - 掌握
__init__(self)
和__new__(cls)
的用法和区别。 - 掌握
@classmethode
、@staticmethod
和@property
等装饰器在类中的使用。 - 掌握单继承、多继承以及多层继承。
- 掌握单例模式
- 了解方法解析顺序(MRO)是深度优先和从左到右。
- 了解封装的思想。
- 了解 Python 的多态。
面向对象需要不断的学习和实践,才能更好地理解里面的思想。总结的不对的,希望各位读者能批评指正!
11. 巨人的肩膀