第八章 面向对象

第八章面向对象

1.面向对象概述
面向对象是程序开发领域的重要思想,这种思想模拟了人类认识客观世界的思维方式,将开发中遇到的事物皆看作对象。
面向过程以要解决的问题为思考的出发点和核心,使用计算机逻辑描述要解决的问题和解决问题的方法。程序设计则以函数为单位,把计算机程序视为一系列的函数集合,对于问题,首先思考“怎么按步骤实现?”,然后分析出解决问题所需要的步骤,最后用函数把这些步骤逐一实现,使用的时候依次调用就可以了。面向过程的程序设计语言注重高质量的数据结构和算法,研究用什么样的数据结构来描述问题,以及采用什么样的算法来高效地解决问题。
面向过程的程序设计语言有一个缺点:解决问题依赖于过程,解决不同的问题需要不同的过程,所以需要编写不同的程序来解决,同一问题即使是发生了微小的变化也会对程序流程产生较大影响,代码的可维护性和可重用性都比较差。当需要解决的问题越来越复杂时,这种语言的缺点就更明显地暴露出来。
面向对象是以一种更接近人类一般思维的方式去看待世界,把世界上的任一个体都看成是一个对象,每个对象都有自己的特点,并以自己的方式做事,不同对象之间存在着通讯和交互,以此构成世界的运转。用计算机的专业术语来说,对象的特点是它们的属性,而能做的事则是它们的方法。
面向对象的程序设计以对象为单位,把计算机程序视为一组对象的集合,每个对象都有自己的属性和方法。对于问题,首先思考的是“怎么设计对象?”,把问题分解到各个对象。设计对象的目的不是为了完成一个步骤,而是为了描述某个对象在整个解决问题过程中的行为。面向对象的程序设计方法采用符合人类的思维方式的方法表达现实世界,降低了程序的复杂度,提高了程序的可重用性,使得计算机程序设计能够应对越来越复杂的应用需求。
类的定义和使用
面向对象编程有两个非常重要的概念:类和对象。
对象映射现实中真实存在的事物,如一本书。
具有相同特征和行为的事物的集合统称为类。
对象是根据类创建的,一个类可以对应多个对象。
类是对象的抽象,对象是类的实例。
类是由3部分组成的:
类的名称:大驼峰命名法,首字母一般大写,比如物种。
类的属性:用于描述事物的特征,比如毛色。
类的方法:用于描述事物的行为,比如跑步。
语法格式:
class 类名:
属性名 = 属性值
def 方法名(self):
方法体
根据类创建对象:
对象名 = 类名() car = Car()
适用对象的本质是访问对象成员:
对象名.属性名 car.wheels
对象名.方法名() car.drive()
在Python中,可以使用内置函数isinstance()来测试一个对象是否为某个类的实例,也可以使用内置函数type()查看对象的类型,例如:

isinstance(p, Person), type(p)
 (True, __main__.Person)

类的成员
属性
属性按声明的方式可以分成两类:类属性和实例属性。
1.类属性
声明在类内部,方法外部的属性。
可以通过类或对象进行访问,但只能通过类进行修改。

car = Car()           	                                                     # 创建对象car
print(Car.wheels)   	                                                     # 通过类Car访问类属性
print(car.wheels)   	                                                     # 通过对象car访问类属性
Car.wheels = 3    	                                                     # 通过类Car修改类属性wheels
print(Car.wheels)
print(car.wheels)
car.wheels = 4                                                                       # 通过对象car修改类属性wheels
print(Car.wheels)
print(car.wheels)

实例属性:
实例属性是在方法内部声明的属性。
Python支持动态添加实例属性。
(1)访问实例属性------只能通过对象访问。
示例:

class Car:
    def drive(self):  
        self.wheels = 4                      # 创建实例属性
car = Car()                                    # 创建对象car
car.drive()          
print(car.wheels)                           # 通过对象car访问实例属性
print(Car.wheels)                          # 通过类Car访问实例属性(AttributeError)

(2)修改实例属性——通过对象修改
示例:

class Car:
    def drive(self):  
        self.wheels = 4                   # 创建实例属性
car = Car()                                  # 创建对象car
car.drive()
car.wheels = 3                           # 修改实例属性      
print(car.wheels)                         # 通过对象car访问实例属性

(3)动态添加实例属性------类外部使用对象动态添加实例属性。

class Car:
    def drive(self):  
        self.wheels = 4                   # 添加实例属性
car = Car()                                  # 创建对象car
car.drive()
car.wheels = 3                            # 修改实例属性      
print(car.wheels)                         # 通过对象car访问实例属性
car.color = "红色"                        # 动态地添加实例属性
print(car.color)

方法
Python中的方法按定义可以分为三类:实例方法,类方法和静态方法。
1.实例方法
形似函数,但它定义在类内部。
以self为第一个形参,self参数代表对象本身
只能通过对象调用
2.类方法
类方法是定义在类内部
使用装饰器@classmethod修饰的方法
第一个参数为cls,代表类本身
可以通过类和对象调用
类方法中可以使用cls访问和修改类属性的值

class Car:
    wheels = 3               					# 类属性
    @classmethod
    def stop(cls):           					# 类方法
        print(cls.wheels)   					# 使用cls访问类属性
        cls.wheels = 4       					# 使用cls修改类属性
        print(cls.wheels)
car = Car()                
car.stop() 

3.静态方法
静态方法是定义在类内部
使用装饰器@staticmethod修饰的方法
没有任何默认参数
静态方法可以通过类和对象调用

class Car:
    @staticmethod
    def test():                                # 静态方法
        print("我是静态方法")
car = Car()                
car.test()                                      # 通过对象调用静态方法
Car.test()                                      # 通过类调用静态方法

静态方法内部不能直接访问属性或方法,但可以使用类名访问类属性或调用类方法

class Car:
    wheels = 3     # 类属性
    @staticmethod
    def test():  
        print("我是静态方法")
        print(f"类属性的值为{Car.wheels}")  # 静态方法中访问类属性

私有成员
类的成员默认是公有成员,可以在类的外部通过类或对象随意地访问,这样显然不够安全。
为了保证类中数据的安全,Python支持将公有成员改为私有成员,在一定程度上限制在类的外部对类成员的访问。
Python通过在类成员的名称前面添加双下画线(__)的方式来表示私有成员,语法格式如下:
__属性名
__方法名
示例:

class Car:
    __wheels = 4         						# 私有属性
    def __drive(self):  						# 私有方法
        print("开车")

私有成员在类的内部可以直接访问,在类的外部不能直接访问,但可以通过调用类的公有方法的方式进行间接访问。
示例:

class Car:
    __wheels = 4        						# 私有属性
    def __drive(self): 						# 私有方法
        print("行驶")
    def test(self):     
        print(f"轿车有{self.__wheels}个车轮") 	                                        # 公有方法中访问私有属性
        self.__drive()                                                                        	# 公有方法中调用私有方法

4.特殊方法
类中还包括两个特殊的方法:构造方法和析构方法,这两个方法都是系统内置方法。
构造方法
构造方法指的是__init__()方法。
创建对象时系统自动调用,从而实现对象的初始化。
每个类默认都有一个__init__()方法,可以在类中显式定义__init__()方法。
init()方法可以分为无参构造方法和有参构造方法。
当使用无参构造方法创建对象时,所有对象的属性都有相同的初始值。
当使用有参构造方法创建对象时,对象的属性可以有不同的初始值。
示例:

class Car:
    def __init__(self):      							# 无参构造方法
        self.color = "红色"        
    def drive(self):    
        print(f"车的颜色为:{self.color}")
car_one = Car()   							# 创建对象并初始化
car_one.drive()
car_two = Car()   							# 创建对象并初始化
car_two.drive()

析构方法
析构方法(即__del__()方法)是销毁对象时系统自动调用的方法
每个类默认都有一个__del__()方法,可以显式定义析构方法,不能传参。
销毁对象
Python通过引用计数器记录所有对象的引用(可以理解为对象所占内存的别名)数量,一旦某个对象的引用计数器的值为0,系统就会销毁这个对象,收回对象所占用的内存空间。
5.封装
封装是面向对象的重要特性之一,它的基本思想是对外隐藏类的细节,提供用于访问类成员的公开接口。
如此,类的外部无需知道类的实现细节,只需要使用公开接口便可访问类的内容,这在一定程度上保证了类内数据的安全。
为了契合封装思想,我们在定义类时需要满足以下两点要求。
1.将类属性声明为私有属性。
2.添加2个供外界调用的公有方法,分别用于设置或获取私有属性的值。
示例:

class Person:
    def __init__(self, name):
        self.name = name             # 姓名
        self.__age = 1               # 年龄,默认为1岁,私有属性
    # 设置私有属性值的方法
    def set_age(self, new_age):  
        if 0 < new_age <= 120:      # 判断年龄是否合法
            self.__age = new_age
    # 获取私有属性值的方法
    def get_age(self):
        return self.__age

6.继承
继承是面向对象的重要特性之一,它主要用于描述类与类之间的关系,在不改变原有类的基础上扩展原有类的功能。
若类与类之间具有继承关系,被继承的类称为父类或基类,继承其他类的类称为子类或派生类,子类会自动拥有父类的公有成员。
1.单继承
单继承即子类只继承一个父类。现实生活中,波斯猫、折耳猫、短毛猫都属于猫类,它们之间存在的继承关系即为单继承。
Python中单继承的语法格式如下所示:
class 子类名(父类名):
子类继承父类的同时会自动拥有父类的公有成员。
自定义类默认继承基类object。
示例:

class Cat(object):  
    def __init__(self, color):
        self.color = color   
    def walk(self):
        print("走猫步~")
# 定义继承Cat的ScottishFold类
class ScottishFold(Cat): 
    pass
fold = ScottishFold("灰色")          # 创建子类的对象
print(f"{fold.color}的折耳猫")       # 子类访问从父类继承的属性
fold.walk()                                   # 子类调用从父类继承的方法

注意:子类不会拥有父类的私有成员,也不能访问父类的私有成员。
示例:

class Cat(object):  
    def __init__(self, color):
        self.color = color
        self.__age = 1        # 增加私有属性
    def walk(self):
        print("走猫步~")
    def __test(self):         # 增加私有方法
        print("测试")
def __test(self):
print("父类的私有方法")
print(fold.__age)     # 子类访问父类的私有属性
fold.__test()          # 子类调用父类的私有方法

2.多继承
程序中的一个类也可以继承多个类,如此子类具有多个父类,也自动拥有所有父类的公有成员。
Python中多继承的语法格式如下所示:
class 子类名(父类名1, 父类名2, …):
示例:

# 定义一个表示房屋的类
class House(object):  
    def live(self):   						# 居住
        print("供人居住")
# 定义一个表示汽车的类
class Car(object):  
    def drive(self):  						# 行驶
        print("行驶")
# 定义一个表示房车的类
class TouringCar(House, Car): 
    pass
tour_car = TouringCar()
tour_car.live()   						# 子类对象调用父类House的方法
tour_car.drive()  						# 子类对象调用父类Car的方法

如果House类和Car类中有一个同名的方法,如果子类继承的多个父类是平行关系的类,那么子类先继承哪个类,便会先调用哪个类的方法。
3.重写
子类会原封不动地继承父类的方法,但子类有时需要按照自己的需求对继承来的方法进行调整,也就是在子类中重写从父类继承来的方法。
在子类中定义与父类方法同名的方法,在方法中按照子类需求重新编写功能代码即可。
示例:

# 定义一个表示人的类
class Person(object):
    def say_hello(self):
        print("打招呼!")
# 定义一个表示中国人的类
class Chinese(Person):
    def say_hello(self):  					# 重写的方法
        print("吃了吗?")
chinese = Chinese()
chinese.say_hello()       					# 子类调用重写的方法

子类重写了父类的方法之后,无法直接访问父类的同名方法,但可以使用super()函数间接调用父类中被重写的方法。
示例:

# 定义一个表示中国人的子类
class Chinese(Person):
    def say_hello(self):
        super().say_hello()                # 调用父类被重写的方法
        print("吃了吗?")

7.多态
多态是面向对象的重要特性之一,它的直接表现即让不同类的同一功能可以通过同一个接口调用,表现出不同的行为。
示例:

class Cat:
    def shout(self):
        print("喵喵喵~")
class Dog:
    def shout(self):
        print("汪汪汪!")
def shout(obj):
    obj.shout()
cat = Cat()
dog = Dog()
shout(cat)
shout(dog)

8.特殊属性
在Python中,定义的类会自动继承一些属性和方法,这些属性和方法的命名以双下画线“__”开头和结尾,是类的特殊成员,被称为特殊属性和特殊方法(魔法方法)。这些属性或方法有着特别的用途,可以实现对象的高级行为,或与操作符的交互,可以用来实现更强大的类。
类常见的特殊属性如下表所列:
在这里插入图片描述
用于获取类或者函数的名称,例如:
在这里插入图片描述
name__还有一个常见的应用场景:每个Python文件都包含特殊属性__name,一个.py文件就是一个模块,一般情况下,模块的名字就是文件名(不包含扩展名.py),存放在__name__中。但是当一个模块作为脚本直接执行时,name__的值不再是模块名,而是__main。通过__name__的值,我们可以判断出该文件是作为脚本直接执行还是被import到其它Python模块,而根据这个判断,我们就可以选择性地执行代码。例如:
在这里插入图片描述
用于获取类定义所在的模块名,例如:
在这里插入图片描述
用于获取类定义所在的模块名,例如:

在这里插入图片描述
__class__可以用来获取对象或类所属的type,当然也可以通过内置函数type()获取对象或类所属的type,两种方法获取的结果是一致的,例如:
在这里插入图片描述
用于获取该类所有直接父类组成的元组,顺序是它们在子类继承列表中出现的顺序,例如:
在这里插入图片描述
注意:类的实例没有__bases__属性,“a.bases”会引发“AttributeError: ‘A’ object has no attribute ‘bases’”异常。

用于获取类、函数、模块的帮助信息,这些帮助信息通常存放于文档字符串,若不存在,则返回为None。文档字符串,它使用三个单引号’''或者三个双引号"""来定义,作为类、函数、模块的说明。文档字符串在代码执行时会被忽略,但会被解释器识别并放入所在类、函数或模块的__doc__属性中。例如:
在这里插入图片描述
用于获取多继承时的方法解析顺序(MRO),以便在调用父类方法时提供方法解析排序。对于支持继承的Python语言来说,其方法可能定义在当前类,也可能继承自父类,所以在调用方法时就需要对当前类和父类进行搜索以确定其所在的位置。对于单继承,搜索比较简单;而对于多继承,搜索就复杂很多。为了方便、快速地看清继承关系和搜索顺序,可以用__mro__属性来获取方法(属性)的调用顺序,例如:
在这里插入图片描述
用于获取类或者实例的属性,返回值为一个字典,其中:key为属性名,value为属性值。例如:
在这里插入图片描述
在之前,我们介绍过内置函数dir(),它也用于查询对象的方法和属性。这两者的区别在于:
1、dict__返回的是一个字典,dir()返回的是一个包含查询对象的所有属性和方法名称的列表;
2、dict__仅显示对象自身的属性,不包括父类的属性,dir()除了显示对象自身的属性,还可以显示从父类继承的属性,dict__是dir()的子集,dir()包含了__dict__中的属性。
3、对__dict__而言,不仅可以通过其访问属性,还可以通过其对属性进行设置、修改、删除等操作,例如可以通过a.dict[‘var’] = 3设置属性,完成与a.var = 3等效的操作,而dir()仅能展示对象的所有属性。
特殊方法
Python中类的特殊方法也是通过前后各两个下划线包围来命名的,类似这种格式:方法名,这些方法功能强大,使我们能够完全控制与对象进行交互的各种接口,仿佛充满了魔力,所以也被称为魔法方法(Magic Methods)。如果有需要,我们可以对类的特殊方法进行重写,从而实现一些特殊的功能。重写特殊方法有时候也被称为运算符重载。最常用的特殊方法就是对象的初始化方法__init
(),它定义了实例的初始化行为,可以通过重写这个方法来实现每个类各自不同的初始化逻辑。在Python中有着数量众多的魔法方法,它们总会在适当的时候被调用,展示“魔力”,而这一切都是自动发生的,不需要用户主动触发。
例如,当比较两个字符串是否相等时,调用的是与比较操作符有关的魔法方法__eq
():
在这里插入图片描述
解析’abc’ == 'xyz’表达式时,Python实际上是调用了字符串类型的魔法方法__eq
()。这一切发生的如此自然,以至于感觉不到魔法方法的存在。
如果自定义的类重载了这些方法中的某一个,那么原有的魔法方法就会被拦截,而重新定义的方法就会被调用。通过这种方式,你可以定义类希望展示的行为。例如,比较操作符通过逐一比较字符判断两个字符串是否相等,而现在我们期望可以通过字符串的长度来判断是否相等。如前所述,比较操作符是通过魔法方法__eq__()实现的,所以能够通过重载这个魔法方法,以重新定义比较操作符==的行为:
在这里插入图片描述
最后强调一下,除非必需,否则不建议修改类的特殊方法,因为这会使你的代码看起来很奇怪,并给阅读的人带来困扰。
一、new()、init()

我们最为熟知的特殊方法就是__init__(),在创建实例对象后对其初始化时使用。但是,使用a = A() 创建实例的时候,init()并不是第一个被调用的方法。事实上,解释器会首先调用__new__()方法为实例分配空间,创建并返回对一个实例对象的引用,并将其传递给__init__()方法,init()方法再对这个实例进行初始化。如果__new__()没有返回实例对象,则__init__()不会被调用。
new()是一个由object类提供的静态方法,很少进行重载,但是也有典型的应用场景:单例设计模式。单例模式指的是在系统中应用该模式的类只有一个实例,即一个类只有唯一的一个对象实例。例如,Windows操作系统中的Task Manager(任务管理器)就是典型的单例模式,在任何时候,只能打开一个任务管理器的窗口。在Python语言中,可以通过重载__new__()实现单例模式,单例模式的表现形式为每一次创建的实例,其内存地址是相同的。
二、del()
当程序不再需要一个对象时,解释器会销毁该对象,将其占用的内存空间释放出来,这个过程被称为GC(Garbage Collector,垃圾回收)。Python 会自动回收所有对象所占用的内存空间,开发者无须关心对象垃圾回收的过程。垃圾回收会自动触发对__del__()方法的调用。在一些文章和书籍中,del()被称为析构方法,这是不准确的表达方式。在Python 3的官方手册中,已经明确地指出析构器不是__del__()方法的合适名称, 也就是说__del__()方法不是类的析构方法,只是一个普通的方法,可以在任何时候被调用,可以执行任何操作,但它总会在实例即将被销毁之前调用。
在object类中没有__del__()方法,在自定义类中通常也不需要定义该方法。但是如果父类定义了__del__()方法,在子类中重构该方法时,必须显式调用父类的__del__()方法,以确保能够删除实例中父类的那一部分。
在继续深入__del__()之前,先来了解一下如何获取对象的引用个数:sys模块中的getrefcount()可以用来计算对象的引用个数,返回值 = 实际的引用个数 + 1,若返回2则说明该对象的实际引用个数为1,此时有1个变量引用了该对象。明确了如何获取对象的引用次数以后,再回来讨论__del__()的触发机制,先看以下代码:
在这里插入图片描述
在调试代码的时候,我们往往会想看下某个对象中的内容是不是符合预期,如果直接使用print()输出,只会得到类名和所在地址,这些信息对开发者来说几乎没有参考意义。多数情况下,我们想得到的是某个实例中具体的值,而不是它在内存中的地址,可以通过重载__repr__()、str()来完善print()时的输出显示信息。str()是面向用户的,而__repr__()是面向开发者,简单地说就是,当使用内置函数print()输出时,调用的是__str__()方法;而在交互式的命令行环境中,在不使用print()直接输出对象的时候,调用的就是__repr__()方法。在类中可以对这两个方法进行重载,例如:
在这里插入图片描述
同时重载这两个方法,可以在不同环境下支持不同的显示,如果期望在所有环境下都统一显示的话,仅重载__repr__()方法就可以满足需求。如果没有重载__str__(),print()会调用__repr__(),但反过来不成立。
还有,请记住__repr__()、str()都需要返回字符串。
在Python中,也可以通过特殊方法改变访问属性时的行为,与之有关的特殊方法包括__getattr__()、setattr()、delattr()、getattribute()

一、getattr(self, attr)
当试图访问一个不存在的属性时,可以通过这个特殊方法来定义类的行为,比如:可以拦截拼写错误的属性名称并且给出提示,或者可以在误用已被删除的属性时给出警告,这样就可以灵活地处理“AttributeError”异常。简单地说:尝试访问一个并不存在的属性就会调用__getattr__()方法,而如果属性存在则不会调用该方法。
二、setattr(self, attr, value)
setattr()是设置属性时会调用的魔法方法,它允许用户自定义设置属性的赋值行为。setattr()相当于设置属性前的一个钩子。每个设置属性的方法都绕不开它。事实上,类中任何位置对于self属性的赋值操作,都会去调用__setattr__()方法。所以,在使用时务必要小心,避免这种特性可能会导致的无穷递归循环。
三、delattr(self, attr)
delattr()的行为和__setattr__()很相似,只不过它是用于处理删除属性时的行为。和 setattr_()一样,使用它时也需要多加小心,防止产生无穷递归循环(在__delattr__()的实现中调用 del self.name 会导致无限递归)。
四、getattribute(self, attr)

getattribute()可以拦截对所有实例属性的访问(包括对__dict__的访问),也就是说:只要实例对象的属性被访问,就会自动调用类的__getattribute__()方法,所以在重载时要十分谨慎地避开无穷递归循环的陷阱。记住,在__getattribute__()方法中访问实例的属性时,唯一安全的方式是使用super()调用更高级类的__getattribute__()方法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值