7. 类和对象

Python 是一门面向对象的编程语言,Python 的面向对象比较简单。Python 语言支持面向对象的三大特征:封装、继承和多态,本章中将详细介绍 Python 中面向对象的基本概念以及其三大特征。

7.1. 类和对象

在面向对象的程序设计过程中有两个重要概念: 类( class )和对象( object,也被称为实例,instance ),其中类是某一批对象的抽象,可以把类理解成某种概念。对象才是一个具体存在的实体。

7.1.1. 类的定义

Python 中使用 class 关键字创建一个类, class 关键字之后为类的名称并以冒号 : 结尾,定义类的语法如下所示:

class ClassName:
	class_member

其中, class_member 是由类成员组成,包括类方法,类变量。其中,类方法定义了类的对象的行为,类变量则定义了类中包含的数据。

在类中定义的方法默认是实例方法,定义实例方法的方法与定义函数的方法基本相同,只是实例方法的第一个参数会被绑定到方法的调用者,即该类的实例,该参数通常会被命名为 self。

在实例方法中有一个特殊的方法 __init__() ,这个方法被称为构造方法。构造方法用于构造该类的对象, Python 通过调用构造方法返回该类的对象。如果开发者没有为该类定义任何构造方法,那么 Python 会自动为该类定义一个只包含一个 self 参数的默认的构造方法 。

以定义一个 Person 类为例,其定义方法如下所示:

# 定义一个 Person 类
class Person:
	def __init__(self, name, age):
		# 下面为 Person 对象增加两个变量
		self.name = name
		self.age = age
	
	# 下面定义了一个 info 方法
	def info(self):
		print("Hello, ", self.name, " , ", self.age)

7.1.2. 创建对象

对象是通过构造方法创建的,即通过 __int__() 方法创建一个对象。以创建一个 Person 类的对象为例,代码如下所示:

# 调用 Person 类的构造方法,创建一个 Person 对象
person = Person("zzy", 34)

有了对象之后,便可以通过该对象操作对象的实例变量以及调用对象的方法。对象访问方法或变量的语法如下所示:对象.变量|方法(参数)。使用方法如下代码所示:

下面代码通过 Person 对象来调用 Person 的实例和方法(程序清单同上)。

# 访问实例变量
print(person.name, ", ", person.age) # zzy , 34
# 增加实例变量
person.id = 123
print(person.id, ", ", person.name, ", ", person.age) # 123 , zzy , 34
# 调用方法
person.info() # Hello, zzy , 34

7.2. 方法

方法是类或对象的行为特征的抽象, Python 中的方法其实也是函数,其定义方式、调用方式和函数都非常相似。

7.2.1. 实例方法和类方法

在 Python 的类体中定义的方法默认都是实例方法,可以通过对象来调用实例方法。同样也可以使用类来调用实例方法,但是却不能直接调用,如下代码所示:

# 通过实例调用
person = Person("zzy", 34)
person.info() # Hello, zzy , 34

# 通过类调用
Person.info()

上述代码在执行到 Person.info() 时报了如下的错误:

TypeError: info() missing 1 required positional argument: 'self'

这是因为在 info() 函数中需要一个 self 的参数,而这个 self 是自动指向的调用该方法的对象,因此在使用类调用时也必须指定一个对象,上述代码应该修改为:

# 通过类调用
Person.info(person) # Hello, zzy , 34

实际上, Python 支持定义类方法和静态方法,类方法和静态方法都是可以使用类直接调用,不同点是: Python 会自动绑定类方法的第一个参数,通常设置为 cls ,与 self 类似, cls 会自动绑定到类本身,而静态方法则不会自动绑定。具体的定义方法如下:

  • 类方法使用 @classmethod 修饰
  • 静态方法使用 @staticmethod 修饰

具体的使用方法如下代码所示:

# 定义一个 Person 类
class Person:
	# 类变量
	name = "zzy"
	age = 34
	
	def __init__(self, name, age):
		# 下面为 Person 对象增加两个变量
		self.name = name
		self.age = age
	
	# 下面定义了一个 info 方法
	def info(self):
		print("Hello, ", self.name, " , ", self.age)

	# 定义类方法
	@classmethod
	def class_info(cls):
		print("ClassMethod: ", cls.name, cls.age)

	# 定义静态方法
	@staticmethod
	def static_info():
		print("StaticMethod: ")

# 通过类调用
Person.class_info()
Person.static_info()

在类方法中,只能访问到类变量

7.2.2. @函数装饰器

@staticmethod@classmethod 的本质是函数装饰器,其中 staticmethodclassmethod 都是 Python 内置的函数。使用 符号引用已有的函数后,可用于修饰其他的函数,装饰被修饰的函数。同样,可以开发自定义的函数装饰器,当程序使用 @函数 ,如函数 A 装饰另一个函数 B 时,实际上完成如下两步:

  • 将被修饰的函数 B 作为参数传给 @符号 引用的函数 A
  • 将函数 B 替换成修饰函数的返回值

使用方法如下代码所示:

def funA(fun):
	print("function A")
	fun() # 执行传入的 fun 参数
	return "Hello World"

'''
将 funB 以参数传递给 funA
funB 被替换成 funA 的返回值
'''
@funA
def funB():
	print("function B")

print(funB) # Hello World

运行上面程序,可以看到如下的输出结果:

function A
function B
Hello

被修饰的函数总是被替换成 @符号 所引用的函数的返回值,因此被修饰的函数会变成什么,完全由 @符号 所引用的函数的返回值决定,如果 @符号 所引用的函数的返回值是函数,那么被修饰的函数在替换之后还是函数。

7.3. 成员变量

在类体内定义的变量,默认属于类本身。如果把类当成类命名空间,那么该类变量其实就是定义在类命名空间内的变量。

上面已经提及到类变量和实例变量,在类命名空间内定义的变量就属于类变量, Python 可以使用类来读取、修改类变量。使用方法如下代码所示:

# 定义一个 Person 类
class Person:
	# 类变量
	name = "zzy"
	age = 34
	
	# 下面定义了一个 info 方法
	def info(self):
		print("Hello, ", self.name, " , ", self.age)

person = Person()
person.info() # Hello, zzy , 34
# 修改类变量
Person.age = 35
person.info() # Hello, zzy , 35

但是需要区分实例变量和类变量,如下代码所示:

# 定义一个 Person 类
class Person:
	# 类变量
	name = "zzy"
	age = 34
	
	# 新定义的实例变量
	def modify(self, name, age):
		self.name = name
		self.age = age

# 输出类变量
print(Person.name, ", ", Person.age) # zzy , 34
person = Person()
person.modify("felixzhao", 35)
# 输出实例变量
print(person.name, ", ", person.age) # felixzhao , 35

从上述代码可以看出,通过对象对类变量赋值,其实不是对类变量赋值,而是定义类新的实例变量。

7.4. 面向对象的三大特征

封装,继承和多态是面向对象的三大特征,其中:

  • 封装是指将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问。

7.4.1. 封装

为了实现部分变量以及方法对外不可见,在 Python 中,只需在成员命名时以双下划线 __ 开始即可。使用方法如下代码所示:

# 定义一个 Person 类
class Person:
	def __init__(self, name, age):
		# 通过双下划线实现封装
		self.__name = name
		self.__age = age

	# 函数封装
	def __func1(self):
		print("。。。封装函数。。。")

	def setname(self, name):
		self.__name = name
	def setage(self, age):
		self.__age = age

	def getname(self):
		return self.__name
	def getage(self):
		return self.__age

	# 调用封装函数
	def func2(self):
		print("调用封装函数:")
		self.__func1()

person = Person("felixzhao", 34)
print(person.getname(), ", ", person.getage()) # felixzhao , 34
# 直接修改参数
person.__name = "zzy"
# AttributeError: 'Person' object has no attribute '__age'
print(person.__name, ", ", person.__age)
# AttributeError: 'Person' object has no attribute '__func1'
person.__func1()
'''
调用封装函数:
。。。封装函数。。。
'''
person.func2()

从上述代码的执行结果来看,通过对象直接访问 __age 变量和 __func1() 函数会报找不到的错误,注意, person.__name 不报错是因为上述的修改相当于重新定义。同时在上述的代码中还提供了能在外面访问这些变量和函数的方法,如 setname()getname()setage()getage()func2()

7.4.2. 继承

继承是实现软件复用的重要手段。通过继承,可以实现对父类中变量和函数的复用。同时, Python 中支持多继承机制,即一个子类可以同时有多个直接父类。 Python 中继承的语法格式如下所示:

class SubClass(SuperClassl, SuperClass2, ...):
	statements

其中, SubClass 称为 SuperClasslSuperClass2 的子类, SuperClasslSuperClass2 称为 SubClass 的父类。如果在定一个 Python 类时并未显式指定这个类的直接父类,则这个类默认继承 object 类。因此, object 类是所有类的父类,要么是其直接父类,要么是其间接父类。

有了上述的 Person 类,再定义一个 Book 类,具体代码如下:

# 定义一个 Book 类
class Book:
	def __init__(self, bookname):
		self.__name = bookname

	def setname(self, bookname):
		self.__name = bookname

	def getname(self):
		return self.__name

	def bookinfo(self):
		print(self.getname())

此时,我们定一个 Person 类和 Book 类的子类 Student 类,具体代码如下:

# 定义 Student 类
class Student(Person, Book):
	def __init__(self):
		print("Student类实例化")

stu = Student()
stu.setname("zzy")
stu.setage(34)
stu.setbookname("Python教程")
stu.info() # zzy , 34
stu.bookinfo() # Python教程

上述代码的运行结果如下所示:

Student类实例化
zzy ,  34
Python教程

如果在 Student 类中也定义了一个与 Person 类中一样的 info() 方法,如下:

def info(self):
	print(self.getname(), ", ", self.getage(), ", ", self.getbookname())

此时,上述代码的运行结果如下所示:

Student类实例化
zzy ,  34 ,  Python教程
Python教程

可见, Student 类中的 info() 方法覆写(Override)了 Person 类中的 info() 方法。 Python 的子类也会继承得到父类的构造方法 __init__() ,如果子类有多个直接父类,那么就必须调用父类的构造方法,可以使用 super() 函数调用父类的构造方法。

# 重新定义 Student 类
class Student(Person, Book):
	def __init__(self, name, age, bookname):
		super().__init__(name, age)
		Book.__init__(self, bookname)

	def info(self):
		print(self.getname(), ", ", self.getage(), ", ", self.getbookname())

7.4.3. 多态

面向对象的对态是指不同的实例在调用同一个函数时表现出多种的行为,如下代码所示:

# 定义一个父类
class Animal():
	def say(self):
		print("动物叫声")
# 子类 1
class Cat(Animal):
	def say(self):
		print("喵 喵 喵")
# 子类 2
class Dog(Animal):
	def say(self):
		print("汪 汪 汪")

animal = Animal()
animal.say() # 动物叫声
cat = Cat()
cat.say() # 喵 喵 喵
dog = Dog()
dog.say() # 汪 汪 汪

对于不同的实例,调用同样覆写父类的方法 say() ,有不一样的行为。

7.5. 枚举类

在某些情况下,一个类的对象是有限且固定的,比如季节类,它只有 4 个对象。这种实例有限且固定的类,在 Python 中被称为枚举类。

7.5.1. 定义枚举类

有两种方式来定义枚举类,分别为:

  • 直接使用 Enum 列出多个枚举值来创建枚举类
  • 通过继承 Enum 基类来派生枚举类

如下程序示范了直接使用 Enum 列出多个枚举值来创建枚举类:

import enum
# 定义 Season 枚举类
Season = enum.Enum(Season, ("SPRING", "SUMMER", "FALL", "WINTER"))

上面程序使用 Enum() 函数来创建枚举类,实际上就是利用 Enum 的构造方法,该构造方法的第一个参数是枚举类的类名,第二个参数是一个元组,用于列出所有枚举值。

另一种方法则可通过继承 Enum 来派生枚举类,在这种方式下可以为枚举定义额外的方法。具体如下代码所示:

import enum

class Season(enum.Enum):
	SPRING = "春"
	SUMMER = "夏"
	FALL = "秋"
	WINTER = "冬"

print(Season.SPRING) # Season.SPRING

7.5.2. 枚举的访问

在定义了上面的 Season 枚举类之后,程序可直接通过枚举值进行前问,这些枚举值都是该枚举的成员,每个成员都有 name 、value 两个属性,其中 name 属 性值为该枚举值的变量名, value 代表该枚举值的序号。具体如下代码所示:

# 直接访问指定枚举
print(Season.SPRING) # Season.SPRING 
# 访问枚举成员的变量名
print(Season.SPRING.name) # SPRING
# 访问枚举成员的值
print(Season.SPRING.value) # 1

程序除可直接使用枚举之外,还可通过枚举变量名或枚举值来访问指定枚举对象,如下代码所示:

# 根据枚举变量名访问枚举对象
print(Season['SUMMER']) #Season.SUMMER
#根据枚举值访问枚举对象
print(Season(3)) # Season.FALL

此外, Python 还为枚举提供了一个 __members__ 属性,该属性返回一个 dict 字典,字典包含了该枚举的所有枚举实例。程序可通过遍历 __members__ 属性来访问枚举的所有实例,如下代码所示:

# 遍历 Season 枚举的所有成员
for name, member in Season.__members__.items():
	print(name, '=>', member, ',', member.value)

运行上面代码,可以看到如下输出结果。

SPRING => Season.SPRING , 1
SUMMER => Season.SUMMER , 2
FALL => Season.FALL , 3
WINTER => Season.WINTER , 4

7.6. 本章小结

本章主要介绍了 Python 面向对象的基本知识,包括如何定义类,如何为类定义变量、方法,以及如何创建类的对象。本章还详细介绍了 Python 的 面向对象的三大特征:封装、继承和多态, Python 通过双下画线的方式来隐藏类中的成员。

本章需要掌握知识点:

  1. 对象的定义和使用方法
  2. 深刻理解面向对象的三大特征
  3. 理解@函数装饰器
  4. 学会使用枚举类