进阶7:面向对象思想

面向对象简介

面向对象的思想(Object-Oriented Thinking,简称OOT)是一种将现实世界中的事物、概念或问题抽象为对象的思维方式。通过对象及其交互来组织程序,从而更好地模拟和解决实际问题。面向对象的思想不仅是编程语言的特性,也是设计和解决问题的哲学,它让我们能够以更加自然和直观的方式处理复杂的问题。

面向对象的核心思想

抽象(Abstraction) 抽象是指提取事物的共性,将复杂的现实问题简化为模型。在面向对象中,抽象通过定义类来实现,将类看作现实世界中的某一类事物,并用类的属性和方法来描述事物的特征和行为。通过抽象,我们忽略掉不必要的细节,专注于问题的核心。

  • 例子:例如“汽车”这个概念,我们不需要每次都讨论“汽车”的每一个细节,而是将“汽车”抽象为一个类,包含如“品牌”,“颜色”,“速度”等属性,以及“启动”,“加速”,“刹车”等方法。

封装(Encapsulation) 封装是将数据和操作这些数据的代码封装在一个对象中,对外提供接口而隐藏实现细节。这不仅提高了代码的可维护性,还能保证对象状态的有效性和一致性。

  • 例子:如果我们有一个“银行账户”的对象,银行账户内部可能有余额、账户号等属性,我们通常不允许外部直接修改余额,而是通过“存款”和“取款”方法来进行操作。这样封装了对账户的操作逻辑,防止了直接更改数据的错误。

继承(Inheritance) 继承是面向对象思想中的一个重要机制,允许我们创建新类时复用现有类的功能。通过继承,子类可以继承父类的属性和方法,并可以在此基础上进行扩展或修改。继承体现了“父类-子类”的关系。

  • 例子:假设我们有一个“动物”类,其中有“吃饭”和“睡觉”等方法,然后我们可以派生出“狗类”和“猫类”,这两个类继承“动物类”的特性,但也可以根据自己的需要添加特有的行为(比如“汪汪叫”或“喵喵叫”)。

多态(Polymorphism) 多态是指同一个方法调用可以表现出不同的行为,具体的行为取决于调用时的对象类型。多态使得代码更加灵活,能够处理不同类型的对象,而不需要在代码中明确指定每个对象的类型。

面向对象的思想的好处

  1. 提高程序的可维护性和可扩展性
    由于对象通过封装隐藏了复杂的实现,外部代码只关心对象的接口(方法),而不需要关心内部实现。当需要修改对象的实现时,只需要改变类内部代码,外部代码不受影响。
  2. 增强代码复用性
    通过继承机制,可以让子类复用父类的代码,并且可以在子类中扩展或修改父类的功能。这使得代码更加简洁,避免重复编写相同的代码。
  3. 更加符合现实世界的建模方式
    面向对象的思维方式非常符合人们对世界的理解和思考方式,因为它将实际的物理或抽象事物抽象成对象,这让程序设计更加直观和易于理解。
  4. 提高代码的灵活性
    通过多态,不同类型的对象可以共享相同的接口,这使得系统能够灵活处理不同类型的对象,提高了代码的灵活性和可扩展性。

详细讲述下封装、继承、多态三个概念

封装(Encapsulation)

封装是指将对象的状态(属性)和行为(方法)组合在一起,并控制它们的访问。通常,类的内部数据是私有的,外部只能通过公开的接口(如方法)来访问和修改数据。

例如,在Dog类中,我们通过方法bark()来控制狗对象的行为,而不能直接通过外部访问它的属性。可以通过设置访问控制来实现封装。

继承(Inheritance)

继承是面向对象编程的一个重要特性,它允许我们通过现有的类创建新的类。新类可以继承现有类的属性和方法,也可以新增或重写一些功能。

例如,假设我们想创建一个Cat类,它也有bark()方法,但我们希望它的行为与Dog不同。我们可以通过继承来实现:

class Cat(Dog):
    def bark(self):
        print(f"{self.name} says meow!")

这样,Cat类继承了Dog类的属性和方法,但我们修改了bark()方法以适应猫的特点。

多态(Polymorphism)

多态指的是相同的接口或方法可以根据不同的对象类型表现出不同的行为。比如,DogCat对象都可以调用bark()方法,但它们表现出的行为不同。

class Dog:
    def bark(self):
        print("Woof")

class Cat:
    def bark(self):
        print("Meow")

def make_sound(animal):
    animal.bark()

make_sound(Dog())  # 输出 Woof
make_sound(Cat())  # 输出 Meow

类与对象

1. 类(Class)

类是对象的蓝图或模板,它定义了对象的属性和方法。类本质上是对一组相似对象的抽象,它包含两个部分:

  • 属性(Attributes):对象的特征或状态,通常表现为变量。
  • 方法(Methods):对象的行为或功能,通常表现为函数。

类的设计

进阶7:面向对象思想

下图中的类:

进阶7:面向对象思想
植物类
	射手类
		双发豌豆射手
		寒冰射手
			血量:5
			(发射炮弹)
			(减速)
	坚果类
		圆坚果
		高坚果
	近战类
		大嘴花
	生产类
		向日葵
	消耗品类
		土豆雷
		樱桃炸弹
		毁灭炸弹
僵尸类
	近战僵尸
		铁桶僵尸
	远程僵尸
		投石车僵尸
	。。。

地图类
	房顶
	夜晚
	后院

定义类

在 Python 中,类是通过 class 关键字来定义的:

# 经典类(旧式类)定义形式
class Person:
class Person():

# 新式定义
class Person(object):

设计类

class 类名:
    # 方法列表
    # 类的方法,类的方法和函数的区别之一就在于有默认有一个参数self,可以这么理解:出现在类外的叫做函数,出现在类里面的都叫做方法
    def 方法1(self):
        print("我是方法一")
    
    def 方法 2(self):
        print("我是方法二")

例如:

# 抽象一个人类
class Person:
    # 第一个方法
    def eat(self, food):
        print('一个人在吃',food)
    # 第二个方法
    def sleep(self,t):
        print('一个人睡了', t, '个小时')

2. 对象(Object)

对象是现实世界事物的抽象,它通常包括:

  • 属性(Attributes):对象的特征,通常表现为数据或状态。
  • 方法(Methods):对象可以执行的操作或行为。

创建对象

通过类来创建对象时,我们通常会调用类名并传入所需的参数:

格式:

# 创建对象格式:
对象名 =  类名(参数列表...)
# 对象调用方法格式:
对象名.方法名(参数列表)

通过类,我们可以实例一个对象,实例对象时,会在内存中分配一块内存空间,这个空间就是这个对象的位置,然后将引用这个地址返回给对象名:

实例对象:

Tom = Person()

验证对象名是否返回值为地址:

进阶7:面向对象思想

在当前模块下 的Person 的类创建了一个 对象(Tom)object,开辟的新地址为0x10b6d4190

对象执行类中的方法

进阶7:面向对象思想

实例化多个对象

首先我们需要知道的是,每实例一个对象,都会开辟一个新的地址

进阶7:面向对象思想

每个对象调用类中的方法:

进阶7:面向对象思想

这样就可以实现多个对象使用方法了,但是问题来了,输出的内容中怎么区分 Tom 和 Jack 呢?

动态绑定多个属性

刚才我们已经可以确认,Tom 和 Jack 并不是同一个对象(因为指向了不通的地址),创建的是多个类型相同,但是地址不同的多个对象

我们尝试修改下代码,为对象动态添加一个属性 name:

动态为对象绑定属性时,给哪个对象绑定了属性,哪个对象才会有该属性,其他对象没有此属性。如果再方法中引用了该属性,没有该属性的对象调用这个方法就会报错

进阶7:面向对象思想

上述原理图:

进阶7:面向对象思想

这种属性绑定方式其实是创建完对象才进行绑定,那么上面这个多次绑定对象属性的过程太麻烦了,能不能在创建对象的时候就绑定这些属性呢?

初始化方法中绑定属性(__init__方法)

在Python中,__init__方法是一个特殊的成员方法(魔法方法),通常用于初始化对象。它是类的构造方法,当类的实例被创建时不需要手动调用,而是会自动调用。__init__方法允许你在创建对象时自动调用进行一些基础的设置,如初始化属性。

下面这段代码来证明__init__方法在对象创建时会被自动调用:

进阶7:面向对象思想

利用__init__的特征,可以在实例对象时就绑定一些属性,在创建对象时,括号内添加初始化对象的参数列表,实参通过__init__方法的形参传递变量

class Person:
    # 类的构造函数,用于初始化对象
    def __init__(self, name, age):
        self.name = name  # 实例变量,保存名字
        self.age = age  # 实例变量,保存名字

    # 第一个方法
    def eat(self, food):
        print(self.name, '在吃', food)

    # 第二个方法
    def sleep(self, t):
        print(self.name, '睡了', t, '个小时')

# 创建一个 Tom 实例
Tom = Person('Tom','18')
Tom.eat('饭')
Tom.sleep(8)

# 创建一个 Jack 实例
Jack = Person('Jack','22')
Jack.eat('海鲜')
Jack.sleep('10')
进阶7:面向对象思想

我们一直没有强调 self 到底是什么,这里重点说一下

在调用方法时,方法的第一个参数 self 是不用手动传参的,这个参数会由解释器自动调用该方法的对象传递过去,即使self 修改其他变量名称也可以,因为每创建一个对象都可以使用相同类下的属性和方法,self 变量就承担起了传递当前对象地址的重担,当有一个对象创建时,会开辟一个新地址,利用self可以实现地址的传递,从而调用属性和方法,所以可以这么说:self 代表当前对象,谁来实例这个类,self 就是谁

进阶7:面向对象思想

这里相当于 self= Tom

__str__方法格式化对象

在 Python 中,__str__ 方法用于定义一个类实例在str() 函数调用或在使用 print() 函数打印时的字符串表示。这个方法是一个特殊方法(也称为魔法方法或双下划线方法),它提供了一种为对象提供可读性更强的字符串表示的途径。

这里有一个简单的示例,展示如何在一个类中实现 __str__ 方法:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __str__(self):
        return f"Person(name={self.name}, age={self.age})"

# 创建一个 Person 对象
person = Person("Alice", 30)

# 使用 print 输出对象信息
print(person)  # 输出: Person(name=Alice, age=30)

# 使用 str() 函数获取对象的字符串表示
person_str = str(person)
print(person_str)  # 输出: Person(name=Alice, age=30)
进阶7:面向对象思想

因为默认没有实现__str__方法,那么会打印:模块名 类名 object at 0x10ccd0f40

如果想按自己的格式显示,需要在类中实现该方法

  • 该函数必须有一个返回值
  • 并且这个返回值必须是一个字符串

如果需要将对象的信息按照一定格式格式化,可以在这里进行格式修饰,修饰完后,可以将这个格式化字符串返回,让 str()方法在执行时,得到该对象转换的字符

__str__方法作用:

__str__ 方法返回一个格式化的字符串,该字符串包含了对象的一些有用信息。当 print(person) 被调用时,Python 在幕后使用 str(person),这又会调用 person.__str__(),从而得到我们定义的字符串格式。

调用该方法的场景:

  • 通过 print()函数打印时,会自动调用该方法
  • 通过 str()函数对定义的对象进行类型转换时,会调用该方法

__del__方法

在Python中,__del__方法是一个特殊的方法,称为析构方法(destructor)。它在一个对象的生命周期结束时被调用,用于执行一些清理操作,比如关闭文件、断开网络连接或者释放其他外部资源。__del__方法在对象被垃圾收集器回收时触发。然而,由于Python使用自动的垃圾回收机制,开发者通常不需要手动管理内存,也就是说,在大多数情况下不需要定义__del__方法。

  • 该方法是用来在销毁对象时,回收释放资源使用的方法。也是自动调用
  • 当在使用 del 对象时,会调用方 法

示例

class MyClass:
    def __init__(self, resource):
        self.resource = resource
        print(f"{self.resource} has been acquired.")

    def __del__(self):
        print(f"{self.resource} has been released.")

# 使用示例
obj = MyClass("SomeResource")
del obj

在这个示例中,创建一个MyClass实例时会获取某种资源,并在该对象被删除时释放这个资源(提前手动释放)。

进阶7:面向对象思想

不主动释放地址,因为垃圾回收机制,程序运行完毕会回收内存地址

进阶7:面向对象思想

python引用计数器原理以及__del__销毁过程

class Pyobj:
    def __del__(self):
        print("对象被销毁")

print("1")
obj = Pyobj()
obj = 6     # 让变量obj指向其他对象
print("2")

当obj = 6 这行被执行时,__del__方法会被执行,print("对象被销毁") 会先于print("2") 执行。
程序输出结果

1
对象被销毁
2

下图从内存层面上展示了这一过程

进阶7:面向对象思想

面向对象项目案例:

下面是一个关于烤地瓜和烤箱的面向对象的 Python 代码示例。这个示例包含了两个类:SweetPotatoOvenSweetPotato 类表示一个地瓜,具有状态和烹饪时间。Oven 类表示一个烤箱,可以添加地瓜并开始烤制。

案例要求:

1、食物有自己的状态,默认是生的

2、食物有自己烧烤的总时间,由每次烧烤时间累加得出

3、不同食物烧烤时,状态随着烧烤总时间的变化而改变。代码中只体现两类食物
例如:

  • 薯类:[0,4)生的[4,7)熟了[7,+♾️)糊了,
  • 肉类:[0,6)生的[6,10)熟了[10,+♾️)糊了

4、食物可以添加自己想要添加的调料

骤构建类、属性、方法:

# 定义食物类
class FoodItem:
	## 食物属性初始化方法(name、type)
		# 食物名称name
		# 食物类型type(薯类、肉类)
		# 烤制时间累加器cooked_time
		# 状态属性state(default = 生的)
		# 附加调料属性condiment(default = [])

	## 食物方法
	# 判断食物状态(time)
		# 添加烧烤时间,判断状态
	# 添加调料(condiment)
		# 列表添加调料

	## 格式化输出

# 定义烤箱
class Oven:
	## 烤箱属性初始化方法
		# 食物填充状态

	## 烤箱方法
	# 添加食物(food_item引入食物对象)
	# 烘烤食物(time)
class FoodItem:
    def __init__(self, name, food_type):
        # 初始化方法,设置食物的名字和类型
        self.name = name  # 食物名称
        self.food_type = food_type  # 食物类型(如地瓜、肉类)
        self.cooked_time = 0  # 记录烹饪时间的初始值为0
        self.status = "生的"  # 初始状态为"生的"
        self.condiments = []  # 初始化调料列表为空

    def cook(self, time):
        # 根据不同类型的食物判断烹饪后的状态
        self.cooked_time += time  # 增加烹饪时间
        if self.food_type == "薯类":
            if self.cooked_time < 4:
                self.status = "生的"
            elif self.cooked_time < 7:
                self.status = "熟了"
            else:
                self.status = "烤糊了"

        elif self.food_type == "肉类":
            if self.cooked_time < 6:
                self.status = "生的"
            elif self.cooked_time < 10:
                self.status = "熟了"
            else:
                self.status = "烤糊了"

    def add_condiment(self, condiment):
        # 添加调料到食物
        self.condiments.append(condiment)

    def __str__(self):
        # 定义字符串表示,可用于打印输出对象信息
        condiments_str = ', '.join(self.condiments) if self.condiments else '无'
        return f"{self.name} 状态: {self.status}, 调料: {condiments_str}"


class Oven:
    def __init__(self):
        # 初始化,没有食物
        self.food_item = None

    def insert_food(self, food_item):
        # 将食物放入烤箱
        self.food_item = food_item
        print(f"{food_item.name} 已放入烤箱。")

    def bake(self, time):
        # 烘烤指定时间
        if self.food_item:
            print(f"烤箱开始烤制 {self.food_item.name} {time} 分钟...")
            self.food_item.cook(time)  # 调用食物对象的cook方法
            print("烤制完成。")
        else:
            print("烤箱里没有食物!")


# 使用示例
# 创建一个地瓜对象
my_sweet_potato = FoodItem("地瓜", "薯类")
# 创建一个烤箱对象
my_oven = Oven()
my_oven.insert_food(my_sweet_potato)
# 烘烤3分钟
my_oven.bake(3)
# 打印地瓜状态
print(my_sweet_potato)
# 添加蜂蜜调料
my_sweet_potato.add_condiment("蜂蜜")
# 再烘烤3分钟
my_oven.bake(3)
print(my_sweet_potato)
# 添加辣椒粉调料
my_sweet_potato.add_condiment("辣椒粉")
# 再烘烤2分钟
my_oven.bake(2)
print(my_sweet_potato)

# 烤其他食物
# 创建一个鸡肉对象
my_chicken = FoodItem("鸡肉", "肉类")
my_oven.insert_food(my_chicken)
# 烘烤5分钟
my_oven.bake(5)
print(my_chicken)
# 再烘烤5分钟
my_oven.bake(5)
print(my_chicken)

类的复合

类的符合基本概念

类的复合(Composition) 是面向对象编程中的一种设计方法,指的是一个类通过将其他类的对象作为成员变量,来实现复杂功能的一种方式。

与继承不同,复合更关注于有一个(Has-A)关系而非是一个(Is-A)关系。例如,一个 Car 类可以包含一个 Engine 对象作为其成员,表示汽车有一个发动机。

class Engine:
    def start(self):
        return "Engine started."

class Car:
    def __init__(self):
        self.engine = Engine()  # Car has an Engine instance

    def start(self):
        return self.engine.start()

# 使用示例
my_car = Car()
print(my_car.start())  # 输出: Engine started.

在这个例子中,Car类包含了一个Engine类的实例。Car类通过复合关系来使用Engine类的功能,这也使得Car类可以通过其engine属性来访问Engine类的方法。

类的复合练习项目

需求:将家具放入家中,如果大于家面积的百分之 30,停止放置家具

class Furniture:
    def __init__(self, name, area):
        """
        初始化家具对象
        
        :param name: 家具名称
        :param area: 家具占地面积
        """
        self.name = name
        self.area = area

    def __repr__(self):
        return f"Furniture(name='{self.name}', area={self.area})"


class House:
    def __init__(self, total_area):
        """
        初始化房子对象
        
        :param total_area: 房子的总面积
        """
        self.total_area = total_area
        self.furniture_list = []
        self.used_area = 0

    def add_furniture(self, furniture):
        """
        尝试将家具添加到房子中
        
        :param furniture: 要添加的家具对象
        :return: 是否成功添加家具的布尔值
        """
        if self.used_area + furniture.area > self.total_area * 0.3:
            print(f"无法添加 {furniture.name},因为超过了房子面积的30%")
            return False
        else:
            self.furniture_list.append(furniture)
            self.used_area += furniture.area
            print(f"{furniture.name} 已添加。当前总占用面积:{self.used_area}/{self.total_area}")
            return True

    def __repr__(self):
        return (f"House(total_area={self.total_area}, used_area={self.used_area}, "
                f"furniture_list={self.furniture_list})")


# 示例使用
if __name__ == "__main__":
    sofa = Furniture("Sofa", 10)
    bed = Furniture("Bed", 15)
    table = Furniture("Dining Table", 7)

    my_house = House(100)
    
    my_house.add_furniture(sofa)  # 输出: Sofa 已添加。当前总占用面积:10/100
    my_house.add_furniture(bed)   # 输出: Bed 已添加。当前总占用面积:25/100
    my_house.add_furniture(table) # 输出: 无法添加 Dining Table,因为超过了房子面积的30%

私有属性

python中定义属性时,没有任何修饰的都是公有的。
在Python中,私有属性通常是通过在属性名或方法名前加上双下划线(__)来定义的。这种命名约定告诉其他程序员这个属性或方法是“私有的”,不应该在类的外部直接访问。然而,Python并没有真正的私有属性机制,所以这些属性仍然可以通过一些方法在类外部被访问。

定义私有属性

class MyClass:
    def __init__(self):
        self.__private_attribute = "I am private"

访问私有属性

直接从类的外部访问私有属性会导致AttributeError,因为私有属性的名称在内部被转换为了_ClassName__AttributeName的形式。不过,可以通过这种名称重整机制来访问私有属性。请尽量避免这么做,因为这违背了使用私有属性的初衷。

obj = MyClass()
# print(obj.__private_attribute)  # 这样会引发AttributeError

# 正确的方式是在类内通过方法来访问
class MyClass:
    def __init__(self):
        self.__private_attribute = "I am private"
    
    def get_private_attribute(self):
        return self.__private_attribute

obj = MyClass()
print(obj.get_private_attribute())  # 这是推荐的方式

# 不推荐但仍然可能的方式
print(obj._MyClass__private_attribute)  # 这样可以绕过但不建议

修改私有属性

修改私有属性和访问私有属性一样,推荐在类内提供一个公共方法来实现,而不是在类外直接修改。

class MyClass:
    def __init__(self):
        self.__private_attribute = "I am private"
    
    def set_private_attribute(self, value):
        self.__private_attribute = value
    
    def get_private_attribute(self):
        return self.__private_attribute

obj = MyClass()
obj.set_private_attribute("New value")  # 推荐的方式
print(obj.get_private_attribute())

# 不推荐但仍然可能
obj._MyClass__private_attribute = "Another new value"
print(obj._MyClass__private_attribute)

在Python中,可以通过使用封装来隐藏对象的内部状态和行为,并提供方法来访问和修改这些数据。封装是面向对象编程的一个重要概念。下面是如何在Python中封装属性的示例:

私有方法的定义和使用

私有属性定义好后,可以保证数据的访问安全
但是还有需求去属性进行访问,可以通过公有接口方法进行间接访问
一般对私有属性会提供一种称为存取器方法的公有方法

使用私有变量和getter/setter方法

在Python中,可以通过使用封装来隐藏对象的内部状态和行为,并提供方法来访问和修改这些数据。封装是面向对象编程的一个重要概念。下面是如何在Python中封装属性的示例:

class Person:
    def __init__(self, name, age):
        self.__name = name  # 私有变量
        self.__age = age

    # Getter方法
    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

    # Setter方法
    def set_name(self, name):
        self.__name = name

    def set_age(self, age):
        if age >= 0:  # 添加一些检查
            self.__age = age
        else:
            raise ValueError("Age cannot be negative")

# 使用示例:
p = Person("Alice", 30)
print(p.get_name())  # 输出:Alice
print(p.get_age())   # 输出:30

p.set_name("Bob")
p.set_age(25)
print(p.get_name())  # 输出:Bob
print(p.get_age())   # 输出:25

set/get 方法对私有属性操作时的好处:

  • 提供精确的访问控制权限
  • 隐藏实现细节,让代码更安全
  • 可以提供更加安全的数据精度控制

使用私有方法下载保护代码案例

'''
私有方法的使用
'''

class ThunderBird(object):

    # 实现一个初始方法,用来保存下载任务
    def __init__(self):
        self.__list = []

    # 实现一个公有的方法,用来添加任务
    def add_task(self,url):
        self.__list.append(url)
        # 在类的内部,直接访问私有方法
        self.__download_data(url)


    # 核心代码 ,用来下载数据的算法
    def __download_data(self,url):
        print(f'通过地址 {url} 下载数据中。。。。')


# 测试
tb = ThunderBird()
# 通过一个公有方法,间接 访问的了对象的私有方法,下载数据
tb.add_task('http://www.dytt88.net/复联4.mp4')

# 私有方法在类的外部是不能拼接访问的。
# tb.__downoad_data('http://www.dytt88.net/复联4.mp4')

类的继承

在 Python 中,继承是一种允许一个类(称为子类或者派生类)继承另一个类(称为父类或者基类)的属性和方法的机制。继承是面向对象编程的一个重要特性,它支持代码重用和逻辑结构的清晰组织。下面是 Python 类继承的一个概述:

基本概念

  1. 父类(基类或超类): 这是被继承的类,提供基础的属性和方法。
  2. 子类(派生类): 继承自父类的类,可以使用父类的属性和方法,并且可以定义自己的属性和方法

Python 中实现继承

使用继承时,需要在子类定义中指定其父类。语法如下:

class ParentClass:
    # 父类的属性和方法
    pass

class ChildClass(ParentClass):
    # 子类的属性和方法
    pass

子类继承父类的方法和属性

子类可以直接使用和覆盖父类的方法和属性。例如:

class Animal:
    def speak(self):
        return "Animal sound"

class Dog(Animal):
    def speak(self):
        return "Woof!"

在这个例子中,Dog 类继承自 Animal 类,并覆盖了 speak 方法。

使用 super() 函数

super() 函数允许子类调用父类的方法。这在重写方法时尤其有用,允许子类在修改行为的同时保留父类方法的功能。

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "Animal sound"

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)  # 调用父类的构造方法
        self.breed = breed

    def speak(self):
        return super().speak() + " Woof!"

多重继承

Python 支持多重继承,即一个子类可以继承多个父类。多重继承的语法如下:

class ParentClass1:
    pass

class ParentClass2:
    pass

class ChildClass(ParentClass1, ParentClass2):
    pass

需要注意的是,多重继承可能带来复杂性和潜在的冲突,因此需要小心使用

继承案例

调用子类方法时会在子类中查找,当子类无法找到对应的属性和方法,就回去父类查找这个方法

进阶7:面向对象思想

父类无法从子类中查找属性和方法(小灵通继承 Phone 可以打电话但是不能拍照)

进阶7:面向对象思想

发生继承调用关系过程:

进阶7:面向对象思想

子类不能使用父类私有属性和私有方法(因为在类外)

私有属性和方法旨在保护数据,仅允许在类的内部访问,而不允许在类的外部甚至是子类中直接访问。下面是一个例子,展示了子类无法直接访问父类的私有属性和私有方法。

# 定义父类
class ParentClass:
    def __init__(self):
        self.__private_attribute = "This is a private attribute"

    def __private_method(self):
        print("This is a private method")

    def access_private_method(self):
        # 通过类的内部方法,我们可以访问私有方法
        self.__private_method()


# 定义子类
class ChildClass(ParentClass):
    def try_to_access_private(self):
        try:
            # 尝试访问父类的私有属性
            print(self.__private_attribute)
        except AttributeError:
            print("子类无法访问父类的私有属性")

        try:
            # 尝试访问父类的私有方法
            self.__private_method()
        except AttributeError:
            print("子类无法访问父类的私有方法")


# 创建一个子类的实例
child = ChildClass()

# 尝试通过子类访问父类的私有属性和方法
child.try_to_access_private()

# 通过父类的公开接口方法访问私有方法
child.access_private_method()

输出结果:

进阶7:面向对象思想

子类对象初始父类中的属性

下面代码中,ChildClass 继承了 ParentClass,但没有为 ChildClass 提供初始化参数,所以当你尝试创建 ChildClass 的实例时会出现错误。因为 ParentClass 的构造函数 __init__ 需要一个参数 name,而在创建 ChildClass 的实例时你没有提供这个参数。

进阶7:面向对象思想

添加参数后,子类可以调用父类方法,比如创建一个 Tom 对象并调用名字方法

进阶7:面向对象思想

下面这段代码中子类有初始化参数方法,那么在使用子类实例对象时,就会调用子类自己的 init 方法,那么就不会调用父类的 init 方法了,父类当中的属性就不会有绑定的机会。所以这时是没有父类属性的
age 属性被正常打印出,name 并没有被绑定

进阶7:面向对象思想

如果想父类中的属性可以得到,需要执行父类中的 init 方法:父类名.init()

进阶7:面向对象思想

在面向对象编程中,当一个子类继承自父类时,我们通常希望子类能够在其构造函数中调用父类的构造函数,以便初始化继承自父类的属性。在 Python 中,有两种常见的方法可以实现这一点:直接调用父类的构造函数和使用 super() 函数。

示例中,ChildClass 继承自 ParentClass,并在其构造函数中通过 ParentClass.__init__(self, name) 这种直接调用父类构造函数的方式,将 name 参数传递给 ParentClass。这就确保了 ChildClass 对象继承并初始化了 ParentClass 中的 name 属性。

具体步骤如下:

  1. 子类 ChildClass 的构造函数首先调用其父类 ParentClass 的构造函数。
  2. 通过 ParentClass.__init__(self, name) 将参数 name 传递给父类的构造函数。
  3. ParentClass 的构造函数通过 self.name = name 初始化 name 属性。
  4. 返回到 ChildClass 的构造函数,继续初始化自己独有的属性 age

这样做的结果是,一个 ChildClass 的实例同时拥有继承自 ParentClassname 属性和自己定义的 age 属性。

当然,另一种推荐的做法是使用 super() 函数来调用父类的构造函数:super().__init__(name)。这种方式可以更清晰地指出是在调用父类的方法,并且在多重继承的情况下更为便利和安全,后续会讲到。

重写父类方法以及强制调用父类方法

在Python中,子类可以通过定义与父类同名的方法来重写父类的方法。如果你还需要在重写的方法中调用父类的方法,可以使用 super() 函数或在方法中调用 父类名字.方法()。

使用场景:

  • 增强父类方法:当需要在父类的方法基础上添加更多功能时,可以先调用父类的方法
  • 初始化:在子类中覆盖 __init__ 方法时,通常会先调用父类的 __init__ 方法以确保继承的所有初始化逻辑得以保留
  • 执行父类必要逻辑:有时候父类的方法包含一些必要的设置或逻辑,你在重写方法时不能忽略
class Parent:
    def some_method(self):
        print("This is the parent method.")

class Child(Parent):
    def some_method(self):
        # 通过 super() 调用父类的方法
        # super().some_method()
        Parent.some_method(self)
        print("This is the child method, after calling the parent method.")

# 创建一个 Child 类的实例
c = Child()

# 调用子类的方法
c.some_method()
进阶7:面向对象思想

多层继承的方法调用过程

在面向对象编程中,多层继承指的是一个类继承另一个类,而继承链可以有多层深度。

class A(object):
    def method(self):
        print("Method in A")

class B(A):
    def method(self):
        print("Method in B")

class C(B):
    def method(self):
        print("Method in C")

在这个例子中,C继承了B,而B又继承了A。如果我们在C的实例上调用method方法,Python会按照C -> B -> A的顺序查找。

obj = C()
obj.method()

输出将是:

Method in C

查找过程:当你调用obj.method()时,Python首先在类C中查找是否定义了method方法。找到后直接调用,不会继续查找。

MRO的查看:可以通过C.mro()或者help(C)来查看方法解析顺序。从输出中可以看到,Python会按照C -> B -> A -> object的顺序进行查找。

如果我们在C中没有定义method方法,那么Python会依次查找BA,直到object为止(object是所有类最终继承的基类)。

进阶7:面向对象思想

多层继承时属性初始化过程

class A(object):
    def __init__(self, a):
        self.a = a
class B(A):
    def __init__(self, a, b):
        A.__init__(self, a)
        self.b = b

class C(B):
    def __init__(self, a, b, c):
        B.__init__(self, a, b)
        self.c = c

C_01 = C(1,2,3)
print(C_01.a)

多继承的基本格式和使用场景

在 Python 中,多继承是指一个类可以继承多个父类的特性和行为。这意味着一个子类可以从多个父类获取属性和方法。多继承可为代码的复用和组织提供便利,但也可能带来复杂性,因此需要小心使用。

基本格式

class ParentClass1:
    def method1(self):
        print("Method in ParentClass1")

class ParentClass2:
    def method2(self):
        print("Method in ParentClass2")

class ChildClass(ParentClass1, ParentClass2):
    pass

# 使用
child = ChildClass()
child.method1()  # 来自 ParentClass1
child.method2()  # 来自 ParentClass2

使用场景

  1. 代码重用:多继承允许重用代码,避免重复实现相同的功能。当不同的类共享部分行为时,可以将这些行为分解到多个父类中,通过多继承进行组合。
  2. 混入类 (Mixins):多继承常用于构建混入类,混入类是一种只提供部分功能的小类,通过将这些类组合到新的类中来扩展其功能。这可以提高代码的模块化和可复用性。
  3. 抽象层次结构:在复杂系统中,多继承可以帮助构建更灵活和动态的抽象层次结构。不同的子系统通过继承不同的基类来组合多个方面的特性。
进阶7:面向对象思想

多继承的属性初始化

python菱形继承 假如使用方法.init()去初始化父类属性,会导致什么问题?

在使用 Python 的多重继承时,如果采用菱形继承结构,直接调用 .init() 方法可能会导致父类被多次初始化。这是因为在菱形继承中,子类会从两个路径继承最终的基类,导致重复调用基类的初始化方法。

以菱形继承为例,假设有以下的类继承结构:

   A
  / \
 B   C
  \ /
   D

在这个结构中,D类继承自BC,而BC都继承自A。如果我们在D中使用.init()来显式调用父类的初始化方法,而不是使用super(),就可能会多次调用A的初始化方法。

下面是一个代码示例,演示如何出现这个问题:

class A:
    def __init__(self):
        print("Initializing A")
        self.a_attr = "A's attribute"

class B(A):
    def __init__(self):
        print("Initializing B")
        A.__init__(self)  # Explicit call to A's initializer
        self.b_attr = "B's attribute"

class C(A):
    def __init__(self):
        print("Initializing C")
        A.__init__(self)  # Explicit call to A's initializer
        self.c_attr = "C's attribute"

class D(B, C):
    def __init__(self):
        print("Initializing D")
        B.__init__(self)
        C.__init__(self)

# Create an instance of D
d = D()

输出:

Initializing D
Initializing B
Initializing A
Initializing C
Initializing A

在上面的例子中,A的初始化方法被调用了两次,因为BC都显式的调用了A.__init__(self)

MRO方法解析顺序

在 Python 中,MRO(Method Resolution Order,方法解析顺序)是用于确定在类层次结构中搜索方法的顺序的规则。它主要用于确定在继承环境中应该调用哪一个方法(特别是在多重继承的情况下)。

Python 中 MRO 的查找顺序是基于 C3 线性化算法,该算法保证了以下几点:

  1. 局部优先级优先于全局优先级:在一个类中,方法解析顺序首先会查看当前类及其直接基类。
  2. 继承链的顺序是从左到右:在定义继承关系时,基类从左到右排列,MRO 会首先考虑左侧的基类。
  3. 保持父类顺序:MRO 会保持继承图中父类的自左向右顺序。

计算 MRO 的算法总会确保某个类的方法被解析时,遵循最优的、合理的顺序来避免歧义。
在 Python 代码中,你可以使用类的 __mro__ 属性或 mro() 方法来查看特定类的 MRO。以下是一个简单的例子:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

class D(B, C):
    pass

print(D.__mro__)
# print(D.mro()) 这种方式也可以

这将输出 D 类的 MRO 顺序,通常是一个包含类和其祖先类的元组。对于上面的代码,结果将是:

(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

这一输出表示 Python 将按这个顺序查找方法和属性:首先在 D 中,其次在 B 中,然后在 CA 中,最后在最顶层的 object 类中。

下面通过 C3 线性化算法计算 MRO

假设有以下类结构:

class X: pass
class Y: pass
class Z: pass

class A(X, Y): pass
class B(Y, Z): pass

class C(A, B, Z): pass

我们需要为类 C 计算 MRO。

通过 C3 线性化算法,C 的 MRO 是根据以下原则计算的:

  1. 自己的类 C
  2. 从各个直接父类的 MRO 列表中,选择第一个类并将其附加到 MRO 中,这个选择必须满足:该类不是列表中任何首个元素的前面部分(即潜在超级类)。
  3. 重复直到所有列表都为空。

我们逐步计算:

  1. 直接父类是 A, B, Z。我们先计算 A, B, 和 Z 的 MRO:
    • A 的 MRO: A -> X -> Y -> object
    • B 的 MRO: B -> Y -> Z -> object
    • Z 的 MRO: Z -> object
  2. 合并列表:
    • [A, X, Y, object]
    • [B, Y, Z, object]
    • [Z, object]
    • [A, B, Z] (直接父类)
  3. 按照 C3 方法选择:
    • 选择 C 自身。
    • 看各个类表中的第一个元素,选择一个不在其他列表首部存在的类。第一个是 A
    • MRO: C -> A.
  4. 移除A,继续合并剩余的部分。
  5. 继续处理:
    • MRO: C -> A -> X
    • MRO: C -> A -> X -> Y
    • MRO: C -> A -> X -> Y -> B
    • MRO: C -> A -> X -> Y -> B -> Z
    • MRO: C -> A -> X -> Y -> B -> Z -> object

最终,类 C 的 MRO 是:

C -> A -> X -> Y -> B -> Z -> object

这个过程确保了对于每个类,都优先遍历其父类,并遵循 Python 中的多继承方法解析顺序原则。

为了解决上述代码中A的初始化方法被调用了多次这样的问题,Python 提供了super()函数,它遵循方法解析顺序(MRO),可以确保A的初始化方法只被调用一次:

class A:
    def __init__(self):
        print("Initializing A")
        self.a_attr = "A's attribute"

class B(A):
    def __init__(self):
        print("Initializing B")
        super().__init__()  # Use super() to call A's initializer
        self.b_attr = "B's attribute"

class C(A):
    def __init__(self):
        print("Initializing C")
        super().__init__()  # Use super() to call A's initializer
        self.c_attr = "C's attribute"

class D(B, C):
    def __init__(self):
        print("Initializing D")
        super().__init__()  # Use super() to call B, C initializers

# Create an instance of D
d = D()

输出:

Initializing D
Initializing B
Initializing C
Initializing A

在使用super()的时候,A的初始化方法只被调用了一次,这是因为super()遵循了 C3 线性化来解决方法解析顺序(MRO),避免了重复调用。

下面使用一个具体案例并初始化参数来讨论菱形

class A:
    def __init__(self, parameter1):
        print("Initializing A")
        self.parameter1 = parameter1

class B(A):
    def __init__(self, parameter1, parameter2):
        print("Initializing B")
        A.__init__(self, parameter1)  # Explicit call to A's initializer
        self.parameter2 = parameter2

class C(A):
    def __init__(self, parameter1, parameter3):
        print("Initializing C")
        A.__init__(self, parameter1)  # Explicit call to A's initializer
        self.parameter3 = parameter3

class D(B, C):
    def __init__(self, parameter1, parameter2, parameter3, parameter4):
        print("Initializing D")
        B.__init__(self, parameter1, parameter2)
        C.__init__(self, parameter1, parameter3)
        self.parameter4 = parameter4

# Create an instance of D
d = D(1,2,3,4)
print(d.parameter1,d.parameter2,d.parameter3,d.parameter4)

输出结果

进阶7:面向对象思想

对于上述的多继承初始化问题解决(使用 super(类名, self)):

在多继承时,如果继承的多个类同时继承同一个父类,那么这时会出现初始化问题,这个共同父类会被初始化多次

类名.__mro__ 得到了一个元组,元组中的元素是当前类在继承关系上的一个顺序
方法解析顺序:
method relational ordered
这个顺序不是我们确定的,是由在确定某个类的继承关系关系后,由解释器来确定这个顺序
一个类的继承书写顺序会影响mro顺序

class A:
    def __init__(self, parameter1):
        print("Initializing A")
        self.parameter1 = parameter1

class B(A):
    def __init__(self, parameter1, parameter2, parameter3):
        print("Initializing B")
        super(B, self).__init__(parameter1, parameter3)  # Explicit call to A's initializer
        self.parameter2 = parameter2

class C(A):
    def __init__(self, parameter1, parameter3):
        print("Initializing C")
        super(C, self).__init__(parameter1)  # Explicit call to A's initializer
        self.parameter3 = parameter3

class D(B, C):
    def __init__(self, parameter1, parameter2, parameter3, parameter4):
        print("Initializing D")
        super(D, self).__init__(parameter1, parameter2, parameter3)
        self.parameter4 = parameter4

# Create an instance of D
d = D(1,2,3,4)
print(d.parameter1,d.parameter2,d.parameter3,d.parameter4)

print(D.__mro__)
进阶7:面向对象思想
class B参数多了一个parameter3,不给就会报错?如何做到参数传递问题的解决?

首先,对于 class B不管是有多少个参数,都用*args进行封包和解包就行

进阶7:面向对象思想

class C同理

进阶7:面向对象思想

在多继承中使用*args解决的问题是为了解决参数在传递时传参的顺序问题

进阶7:面向对象思想

使用*args 后的修改的代码

class A:
    def __init__(self, parameter1):
        print("Initializing A")
        self.parameter1 = parameter1

class B(A):
    def __init__(self, parameter2, *args):
        print("Initializing B")
        super(B, self).__init__(*args)
        self.parameter2 = parameter2

class C(A):
    def __init__(self, parameter3, *args):
        print("Initializing C")
        super(C, self).__init__(*args)
        self.parameter3 = parameter3

class D(B, C):
    def __init__(self, parameter4, parameter2, parameter3, parameter1):
        print("Initializing D")
        super(D, self).__init__(parameter2, parameter3, parameter1)
        self.parameter4 = parameter4

# Create an instance of D
d = D(4,2,3,1)
print(d.parameter1,d.parameter2,d.parameter3,d.parameter4)

print(D.__mro__)
print(B.__mro__)
进阶7:面向对象思想

super的简使用格式解决多继承初始化(掌握):

super(类名, self)方式可以直接简化成super()

class A:
    def __init__(self, parameter1):
        print("Initializing A")
        self.parameter1 = parameter1

class B(A):
    def __init__(self, parameter2, *args):
        print("Initializing B")
        super().__init__(*args)
        self.parameter2 = parameter2

class C(A):
    def __init__(self, parameter3, *args):
        print("Initializing C")
        super().__init__(*args)
        self.parameter3 = parameter3

class D(B, C):
    def __init__(self, parameter4, parameter2, parameter3, parameter1):
        print("Initializing D")
        super().__init__(parameter2, parameter3, parameter1)
        self.parameter4 = parameter4

# Create an instance of D
d = D(4,2,3,1)
print(d.parameter1,d.parameter2,d.parameter3,d.parameter4)

print(D.__mro__)
print(B.__mro__)

类的继承书写会影响MRO的顺序

在 Python 中,类的继承顺序会影响方法解析顺序(MRO, Method Resolution Order)。MRO 决定了一个方法在多重继承中被调用的顺序。具体来说,Python 使用 C3 线性化算法来确定 MRO,它遵循深度优先搜索从左到右的顺序。

让我们看一个例子来说明继承顺序如何影响 MRO:

class A:
    def method(self):
        print("Method in A")

class B(A):
    def method(self):
        print("Method in B")

class C(A):
    def method(self):
        print("Method in C")

class D(B, C):
    pass

# 实例化 D 并调用 method
d = D()
d.method()

# 打印 D 的 MRO
print(D.mro())

在这个例子中,类 D 继承了 BC,而 BC 都继承了 A。由于 D 直接继承自 BC,MRO 的顺序将从左到右开始,即 BC 之前。

输出将是:

进阶7:面向对象思想

上图所见,d.method() 调用了 B 中的方法,因为在 MRO 顺序中 B 位于 C 之前。此外,D.mro() 的输出显示了从 D 到根类 object 的解析顺序。

如果你改变 D 的继承顺序,例如 class D(C, B):,那么 MRO 和方法解析顺序都会改变:

class D(C, B):
    pass

d = D()
d.method()
print(D.mro())
进阶7:面向对象思想

可以看到,method 调用了 C 中的方法,因为 C 现在在 MRO 中位于 B 之前。这样,改变继承的顺序就改变了方法解析顺序。

多重多继承时,方法的查找顺序也参考 MRO

class A:
    def method(self):
        print("Method in A")

class B(A):
    def method(self):
        print("Method in B")

class C(A):
    def method(self):
        print("Method in C")

class D(B, C):
    def method(self)
        super().method() # 会调用 B 类method方法
        A.method(self) # 如果想跳过B、C 直接调用 A 方法,可以选择这种方式

# 实例化 D
d = D()
d.method()
d.A.show
# 查看 MRO
print(D.__mro__)

输出结果为:

Method in B
Method in A
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

多态

在面向对象编程中,Python 中的多态(Polymorphism)是一种允许不同类的对象通过同一个接口处理的能力。多态性的核心思想是将代码写得更通用,以便同一个方法或操作能适用于不同类型的对象。

在 Python 中,多态主要通过方法重载(Method Overriding)和鸭子类型(Duck Typing)来实现。

方法重载(Method Overriding)

当一个子类继承了父类之后,可以重写(override)父类的方法。尽管不同的类可能会实现同一个方法接口,但它们的行为可以有所不同。例如:

class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        return "Woof!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

def animal_sound(animal: Animal):
    print(animal.make_sound())

dog = Dog()
cat = Cat()

animal_sound(dog)  # 输出: Woof!
animal_sound(cat)  # 输出: Meow!

在这个例子中,函数 animal_sound 对于不同的 Animal 对象(DogCat)调用时,会执行相应的 make_sound 方法,并输出不同的结果。

鸭子类型(Duck Typing)

在 Python 中,类型是动态的,因此并不需要显式的接口和继承关系,只要对象实现了需要的方法即可。例如:

class Bird:
    def make_sound(self):
        return "Chirp!"

class Car:
    def make_sound(self):
        return "Vroom!"

def make_some_noise(entity):
    print(entity.make_sound())

bird = Bird()
car = Car()

make_some_noise(bird)  # 输出: Chirp!
make_some_noise(car)  # 输出: Vroom!

在这个例子中,虽然 BirdCar 没有共同的父类或者接口,但因它们都实现了 make_sound 方法,所以可以通过同一个 make_some_noise 函数调用,这体现了多态的概念。

下面这段代码更加体现鸭子类型:

# 标准多态

class Father(object):
    def cure(self):
        print('老中医使用中医治病')


class Son(Father):
    def cure(self):
        print('小大夫使用中西医结合治病')

class AnimalDoctor(object):
    def cure(self):
        print('我是个兽医,主要给动物治病,但是也能给人凑合治一下')


class Dog(object):
    def bark(self):
        print('Won won ...')


# 病人类
class Person(object):
    # 需要一个大夫给他治病
    def need_doctor(self,doctor):
        doctor.cure()

# 测试
p = Person()

p.need_doctor(Father())
p.need_doctor(Son())
# p.need_doctor(Dog()) # 没有这个方法,就无法调用
p.need_doctor(AnimalDoctor())
进阶7:面向对象思想

在这个例子中,虽然 SonAnimalDoctor 没有共同的父类或者接口,但因它们都实现了 cure 方法,这体现了多态的概念。

实例对象属性和实例对象方法

为什么类名不能调用实例对象属性和实例对象方法呢?

因为对象存在时,不一定有实例对象存在

实例对象属性(Instance Attributes)

实例对象属性是与特定对象相关的数据。每个对象可以有自己独立的一组属性,即使它们是同一个类的实例。这些属性通常在类的构造方法(__init__方法)中进行初始化。在 Python 中,可以通过 self 关键字来引用实例属性。

例子:

class Dog:
    def __init__(self, name, age):
        self.name = name  # 实例属性 name
        self.age = age    # 实例属性 age

# 创建实例对象
dog1 = Dog("Buddy", 5)
dog2 = Dog("Lucy", 3)

print(dog1.name)  # 输出: Buddy
print(dog2.age)   # 输出: 3

实例对象方法(Instance Methods)

实例对象方法是属于某个类的函数,用来定义对象的行为。实例方法必须至少有一个参数,通常命名为 self,它指向调用该方法的实例对象。这些方法可以访问和修改对象实例的属性。

例子:

class Dog:
    def __init__(self, name, age):
        self.name = name  # 实例属性 name
        self.age = age    # 实例属性 age

    def bark(self):
        print(f"{self.name} is barking!")  # 实例方法 bark

    def get_age(self):
        return self.age  # 实例方法 get_age
        
    def set_age(self, age):
        self.age = age   # 实例方法 set_age

# 创建实例对象
dog1 = Dog("Buddy", 5)

dog1.bark()  # 输出: Buddy is barking!
print(dog1.get_age())  # 输出: 5

dog1.set_age(6)
print(dog1.get_age())  # 输出: 6

总结

  • 实例对象属性:用于存储每个对象的状态信息。
    • 定义在构造方法(__init__)中。
    • 使用 self 关键字来引用。
  • 实例对象方法:用于定义对象的行为。
    • 定义为类中的函数,第一个参数通常为 self
    • 可以访问和修改实例对象的属性。

通过实例对象属性和方法,类能够为对象提供完整的状态和行为描述,使对象具有丰富的功能和内涵。

类对象、类属性

类对象

类对象是在类定义时创建的,它是一个可调用的对象,可以用来创建类的实例。类对象包含类的所有属性和方法。创建类对象时,使用的是类名本身。

class MyClass:
    def __init__(self, value):
        self.instance_variable = value
    
    def instance_method(self):
        return self.instance_variable

# 创建一个类对象
obj = MyClass(10)
print(obj.instance_variable)  # 输出: 10
print(obj.instance_method())  # 输出: 10

类属性

是指属于类本身的属性,而不是实例对象的属性。类属性在所有实例之间共享。在类定义中,通过赋值语句定义类属性。

class MyClass:
    class_variable = 'I am a class variable!'
    
    def __init__(self, value):
        self.instance_variable = value
    
    def instance_method(self):
        return self.instance_variable

# 访问类属性
print(MyClass.class_variable)  # 输出: I am a class variable!

# 修改类属性
MyClass.class_variable = 'Changed class variable!'
print(MyClass.class_variable)  # 输出: Changed class variable!

# 创建一个类对象
obj = MyClass(10)
# 访问实例属性
print(obj.instance_variable)  # 输出: 10

# 访问类属性(通过实例访问)
print(obj.class_variable)  # 输出: Changed class variable!

# 修改类属性(通过实例修改)
obj.class_variable = 'Instance changes class variable!'
print(obj.class_variable)  # 输出: Instance changes class variable!

注意:当通过实例修改类属性时,并不会真正改变类属性,而是该实例创建了一个同名的实例属性。如果要真正改变类属性,应该通过类对象来修改。

保存对象的属性和方法

__dict__是一个魔法属性,用来保存当前对象的所有成员

'''
扩展:属性和方法的保存位置
'''

class Cat(object):
    def __init__(self,name):
        self.name = name
        self.__age = 1

    def public_method(self):
        print('公有的对象方法')

    def __private_method(self):
        print('私有的对象方法')


tom = Cat('Tom')
jack = Cat('Jack')
# __dict__ 是一个魔法属性,用来保存当前对象的所有的成员
print(Cat.__dict__)
print(tom.__dict__)
print(jack.__dict__)

输出结果:

{'__module__': '__main__', '__init__': <function Cat.__init__ at 0x10b5ec700>, 'public_method': <function Cat.public_method at 0x10b5ec790>, '_Cat__private_method': <function Cat.__private_method at 0x10b5ec820>, '__dict__': <attribute '__dict__' of 'Cat' objects>, '__weakref__': <attribute '__weakref__' of 'Cat' objects>, '__doc__': None} # 包含类级别的属性和方法
{'name': 'Tom', '_Cat__age': 1} # 初始化时以字典形式存了 name 和私有属性 _Cat__age
{'name': 'Jack', '_Cat__age': 1} # 

在 Python 中,__dict__ 是一个特殊的字典属性,用于存储对象的所有可访问的属性和方法。对于类对象,__dict__ 包含类级别的属性和方法。

在提供的代码中,print(Cat.__dict__) 输出了 Cat 类的字典,其中列出了类级别的所有信息,包括方法和一些内置的属性。下面是对这个输出的解释:

  1. __module__: 表示这个类所在的模块名称。在你的例子中,它显示为 '__main__',因为这是在当前脚本或解释器的顶层运行的。
  2. __init__: 这是类的初始化方法(构造函数),用来在创建对象时执行一些初始化操作。在输出中,它被表示为 <function Cat.__init__ at 0x10b5ec700>,其中后面的地址是内存地址,具体地址可能会因运行环境不同而变化。
  3. public_method: 类的一个公有方法,可以通过类的实例调用。在输出中,同样以函数的形式显示。
  4. _Cat__private_method: 这是类的一个私有方法,Python 使用名称改编(Name Mangling)使其不容易被外部访问。前导的 _Cat 是 Python 自动添加的,以防止子类意外覆盖和访问。私有方法__private_method 实际上在内部被表示为 _Cat__private_method
  5. __dict__: 这是一个类属性,是所有类实例会共享的,用于存储对象实例的可变属性。
  6. __weakref__: 用于支持弱引用机制,是一个可选属性,适用于需要对对象进行弱引用时。
  7. __doc__: 这个属性用于存储类的文档字符串(docstring),如果类没有指定文档字符串,则会显示为 None

tom.public_method()可以输出结果:“公有的对象方法”

那么问题是:类中的方法如何被对象找到的?

假如我们直接使用类去执行方法:Cat.public_method(tom),也会输出”公有的对象方法“

在 Python 中,当你在一个对象上调用一个方法时,该方法是通过一种称为“方法解析”的机制被访问的。在具体的例子 tom.public_method() 中,这个过程是这样的:

  1. 实例属性查找:当你尝试在一个类的实例(例如 tom)上访问一个属性(在这个例子中是方法 public_method)时,Python 首先检查该属性是否存在于该实例的 __dict__ 中。这个 __dict__ 是一个字典,它包含直接属于该实例的所有属性。
  2. 类属性查找:如果在实例的 __dict__ 中没有找到该属性,Python 接着检查创建该实例的类本身(即 Cat 类)。这是通过查看类的 __dict__ 来完成的。
  3. 继承和方法解析顺序(MRO):如果仍然没有找到该属性,并且涉及父类(继承),Python 会根据方法解析顺序(MRO)查看父类,这个顺序决定了在类的继承体系中 Python 查找方法的顺序。然而,在这个简单的例子中,除了直接的 object 类之外,没有其他父类的继承。
  4. 方法绑定:一旦 Python 在类中找到了方法,它会自动将方法绑定到实例(tom)上。这个绑定使得方法能够访问实例的属性和其他方法。这是通过方法中的 self 参数实现的,self 参数指的是实例(tom)。

假如我们直接使用类去执行方法:Cat.public_method()会发生报错:TypeError: public_method() missing 1 required positional argument: 'self'

但是随便赋予其一个值,便可成功调用:

进阶7:面向对象思想

为什么一定要添加这个‘self’呢?

解释:Cat.public_method() 会导致错误的原因在于 public_method 是一个实例方法,而不是类方法。在 Python 中,实例方法的第一个参数通常命名为 self,它表示实例本身。当你调用 Cat.public_method() 时,方法需要一个实例对象作为第一个参数,但你却没有提供任何实例,因此会导致 TypeError 错误。

  1. self 参数self 是实例方法的第一个参数,它表示调用该方法的对象实例。Python 自动传递这个参数给实例方法,当你用 tom.public_method() 这样调用时,tom 会被自动作为第一个参数传递给 public_method
  2. 调用实例方法:实例方法应该通过实例来调用,而不是通过类名调用。因此,应该用 tom.public_method() 来调用该方法,这时 self 会被自动绑定到 tom
  3. 能否省略 self 参数self 本身是一个命名约定,可以用其他名字代替,但它不能被省略,因为它是实例方法需要接收实例引用的机制。作为惯例,Python 开发者使用 self 来保持代码一致性和可读性。

如果希望用类名调用方法,而不需要提供实例引用,可以将方法变为静态方法或者类方法:

  • 静态方法:用 @staticmethod 装饰器定义的方法,不需要 self 参数,因为它与实例没有关联。例如:
  @staticmethod
  def static_method():
      print('这是一个静态方法')

调用时可以使用 Cat.static_method(),无需实例对象。

  • 类方法:用 @classmethod 装饰器定义的方法,第一个参数通常命名为 cls,表示类本身。比如:
  @classmethod
  def class_method(cls):
      print('这是一个类方法')

调用时可以使用 Cat.class_method(),同样无需实例对象。

进阶7:面向对象思想

类方法

在 Python 中,类方法(class methods)是与类关联的方法,而不是与类的实例关联。类方法的典型特征是,它们可以访问和修改与类本身有关的状态和行为,而不是单个实例的状态。类方法的第一个参数通常是 cls,表示所在的类,而不是 self,表示的实例。类方法通常通过类名调用,而不是实例调用。

要定义一个类方法,你可以使用 @classmethod 装饰器,装饰器的作用可以在执行类方法时自动将类对象传入到参数cls中。以下是一个简单的例子:

class MyClass:
    class_variable = 0

    def __init__(self, value):
        MyClass.class_variable += 1
        self.instance_variable = value

    @classmethod # 这是一个装饰器,用来表示下面的方法为类方法
    def get_class_variable(cls):
        return cls.class_variable

    @classmethod
    def set_class_variable(cls, value):
        cls.class_variable = value

# 使用类方法
print(MyClass.get_class_variable())  # 输出: 0

MyClass.set_class_variable(5)
print(MyClass.get_class_variable())  # 输出: 5

# 使用对象获取类方法
obj1 = MyClass(10)
print(obj1.get_class_variable())  # 输出: 6

obj2 = MyClass(20)
print(MyClass.get_class_variable())  # 输出: 7

在这个例子中:

  • get_class_variable 和 set_class_variable 是类方法。它们通过 @classmethod 装饰。
  • 类方法接收一个 cls 参数,代表类本身。在类方法中,你可以使用 cls 访问传递类变量。
  • class_variable 是一个类变量,它是所有实例共享的。
  • 通过类名或实例名都可以调用类方法,但一般推荐使用类名来调用以清晰表达用意。

再如:创建一个 MyMath 类(抽象的使用类方法):

class MyMath(object):
    @classmethod
    def sum(cls, *args):
        m = 0
        for i in args:
            m += i
        return m
print(MyMath.sum(1,2,3,4)) # 直接执行类方法

# 使用对象执行类方法
match_obj=MyMath()
print(match_obj.sum(1,2,3,4))
进阶7:面向对象思想

静态方法

在面向对象编程中,静态方法(Static Method)是一种属于类而不是类实例的方法。静态方法使用 @staticmethod 装饰器进行定义,它不需要访问实例或类的属性和方法,只是简单地组织相关功能。静态方法常用于某些不依赖于实例的逻辑功能。

以下是一个静态方法的示例:

class MyClass:
    @staticmethod
    def static_method(arg1, arg2):
        return arg1 + arg2

# 调用静态方法,不需要实例化类
result = MyClass.static_method(5, 10)
print(result)  # 输出: 15

特点

  1. 与实例无关: 静态方法不需要访问类属性和实例属性。
  2. 使用方式: 可以通过类名直接调用,也可以通过实例调用(虽然不常用)。
  3. 标识符: 使用 @staticmethod 装饰器。

实际应用

静态方法通常用来对某些数据进行处理,而这些数据与类的实例没有直接关系。它们类似于类的工具方法。

案例一:

import math

class Circle:
    @staticmethod
    def calculate_area(radius):
        return math.pi * (radius ** 2)

# 使用静态方法计算面积
area = Circle.calculate_area(5)
print(area)  # 输出: 78.53981633974483

在这个示例中,calculate_area 方法只是对给定的 radius 进行计算,并不涉及到 Circle 类的实例化。

案例二:

# 设计 一个编码工具类
class EncodeUtil(object):

    # 编码方法
    @staticmethod
    def encode_data(data, format):
        print(f'对数据 {data} 使用 {format} 格式 进行了编码')


    # 解码方法
    @staticmethod
    def decode_data(data, format):
        print(f'对数据 {data} 使用 {format} 格式 进行了解编码')

# 测试
# eu = EncodeUtil()
# eu.encode_data('hello','utf-8')


EncodeUtil.encode_data('Hello','utf-8')
EncodeUtil.decode_data("hello",'GBK')

这种情况不需要对象来实例化,但是编码和解码为一组可以打包到类EncodeUtil工具中,这种情况下可以设计成类方法形式

静态方法与类方法、实例方法区别

实例方法:必须通过实例对象调用执行(第一个参数是当前调用方法的实例)

定义:在类中定义的普通方法,且第一个参数为 self,表示的是类的实例(对象)本身。
使用:可以通过实例对象调用。
作用:可以访问和修改对象的属性,调用其他实例方法。

类方法:当不需要产生实例对象时,可以使用类方法来实现相应的代码功能,类方法可以直接使用类对象来调用,类方法的第一个参数是 cls,用来接收单签类的对象,通过参数,各个类方法中进行数据共享

定义:使用 @classmethod 装饰器修饰的方法,且第一个参数为 cls,表示的是类本身。
使用:可以通过类或实例对象调用。
作用:可以访问和修改类属性,调用其他类方法,但不能直接访问实例属性。

静态方法:与类方法相似,但是静态方法不接受任何默认参数(实例对象或类对象),静态方法其实就是将一些相关联的普通方法实用类进行整合。

定义:使用 @staticmethod 装饰器修饰的方法,不包含特殊的第一个参数(既不需要 self 也不需要 cls)。
使用:可以通过类或实例对象调用。
作用:既不访问类属性也不访问实例属性,只是和类有逻辑关系的方法。
class Example:
    # 实例方法
    def instance_method(self):
        return f"这是实例方法,{self} 是实例本身。"

    # 类方法
    @classmethod
    def class_method(cls):
        return f"这是类方法,{cls} 是类本身。"

    # 静态方法
    @staticmethod
    def static_method():
        return "这是静态方法,不依赖类或实例。"

# 创建类的一个实例
example = Example()
# 调用实例方法
print(example.instance_method())  # 输出: 这是实例方法,<__main__.Example object at 0x...> 是实例本身。

# 调用类方法
print(Example.class_method())     # 输出: 这是类方法,<class '__main__.Example'> 是类本身。

# 调用静态方法
print(Example.static_method())    # 输出: 这是静态方法,不依赖类或实例。

类方法和静态方法一般实现工具类

发布者:LJH,转发请注明出处:https://www.ljh.cool/41863.html

(0)
上一篇 2024年11月12日 上午10:30
下一篇 2024年11月28日 下午4:36

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注