Python3.11教程5:类与对象

十、python的类与对象

参考:Python 3.11官方文档《类》《datawhale——PythonLanguage》《Python3 面向对象》

10.1 面向对象编程

  面向对象编程(Object-Oriented Programming,OOP)是一种编程范式或编程方法,在许多编程语言中得到广泛应用,包括Python、Java、C++、C#等。它以对象为核心,将数据和操作数据的方法(属性和方法)封装在一起,以模拟现实世界中的事物和交互。

  OOP 的主要思想是将问题分解为对象,并通过这些对象之间的互动来解决问题,有助于构建更清晰、可维护和可扩展的代码。以下是面向对象编程的关键概念:

  • 对象(Object):对象是现实世界中的实体或事物的抽象表示,是类的实例。它包括属性(数据)和操作数据的方法。对象可以是物理实体(例如,汽车、手机)或概念实体(例如,用户、订单)。

  • 类(Class):类是对象的模板或结构,它定义了对象的属性和方法,它规定了对象应该有哪些特征和行为。类具有三大特性——封装、继承和多态。

    1. 封装(Encapsulation):封装是将数据(属性)和操作数据的方法(方法)捆绑在一起的概念。它隐藏了对象内部的细节,只提供了有限的接口供外部访问。封装使得对象的实现细节可以随时更改,而不会影响使用该对象的代码,这提高了数据的安全性和代码的可维护性

    2. 继承(Inheritance)继承允许一个子类继承另一个父类的属性和方法。子类可以重用父类的代码,并可以在其基础上进行扩展或修改。继承建立了类之间的层次关系。

    3. 多态(Polymorphism)

      • 同一个方法可以在不同的类中具有不同的实现,即子类可以重写父类的方法,来实现不同的功能
      • 多态实现了方法的动态绑定,提高了灵活性。你可以通过统一的接口来处理不同的对象,这样可以编写通用的代码,减少了代码的复杂性。

  以上这三个特性一起构成了面向对象编程的基础,它们提供了一种有效的方法来组织和管理复杂的代码,使得代码更易于理解、维护和扩展。通过封装、继承和多态,可以创建具有高内聚性和低耦合性的代码,提高了代码的质量和可重用性。

	class Animal:
	    def __init__(self, name):
	        self.name = name
	
	    def speak(self):
	        pass
	
	class Dog(Animal):
	    def speak(self):
	        return f"{
      
      self.name} says Woof!"
	
	class Cat(Animal):
	    def speak(self):
	        return f"{
      
      self.name} says Meow!"
	
	# 创建不同类型的动物对象
		my_dog = Dog("Buddy")
		my_cat = Cat("Whiskers")
		
	# 使用继承的方法
	print(my_dog.speak())  					  	 # 输出: Buddy says Woof!
	print(my_cat.speak()) 						 # 输出: Whiskers says Meow!

  在上述示例中,我们定义了一个基类 Animal,它有一个 speak 方法,然后创建了两个子类 DogCat,它们继承了 Animal 类,并覆盖了 speak 方法以提供自己的实现。这展示了继承和多态的概念,子类可以继承父类的属性和方法,并可以在不同的子类中进行自定义。

最后总结一下OOP 的优点:

  • 模块化:将问题分解为对象,使得代码更易于理解和维护。
  • 可重用性:可以创建通用的类和方法,以便在不同的项目中重复使用。
  • 扩展性:可以通过添加新的类和方法来扩展功能,而不必修改现有代码。
  • 抽象性:允许从现实世界中的概念中提取出抽象类和对象,使得问题的建模更接近实际情况。

10.2 类与对象

  类与对象是面向对象编程的核心概念,类的三大特性就是封装、继承和多态。在Python中,可以使用关键字 class 来定义一个类。然后,使用构造函数 __init__ 来初始化对象的属性,例如:

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 创建一个Dog类的对象
my_dog = Dog("Buddy", 3)

# 访问对象的属性
print(f"My dog's name is {
      
      my_dog.name}.")
print(f"My dog is {
      
      my_dog.age} years old.")

  这段代码定义了一个名为Dog的类,该类具有nameage属性。我们创建了一个名为my_dog的对象,并访问了它的属性。

  请注意,self 是一个特殊的参数,它指代对象本身。在类的方法中,通常会看到 self,以便访问和操作对象的属性。

10.2.1 类与对象的基本概念

  1. 属性: 在类的声明中,属性是用变量来表示的,这些属性用于存储对象的数据。

  2. 类属性(Class Attributes)或类变量(Class Variables):定义在类里面、方法外面的属性

  3. 实例属性(Instance Attributes)或实例变量(Instance Variables):定义在类的方法里面的属性

    class Student:
        school_name = "ABC School"  		 # 类属性
    
        def __init__(self, name, grade):
            self.name = name  				 # 实例属性
            self.grade = grade  		  	 # 实例属性
    
    # 创建两个学生对象
    student1 = Student("Alice", 10)
    student2 = Student("Bob", 8)
    
    # 访问类属性
    print(Student.school_name) 				 # 输出: ABC School
    
    # 访问实例属性
    print(student1.name)  					 # 输出: Alice
    print(student2.grade)  				     # 输出: 8
    
  4. 方法(Methods):类当中定义的函数称之为方法,它们定义了对象可以执行的操作,并使用self作为第一个参数,以引用对象实例。

  5. 方法重写:当一个子类继承自一个父类,并且在子类中定义了与父类同名的方法或属性时,子类中的方法会自动覆盖(重写)父类的方法。这意味着在子类中调用该方法时,将使用子类中的方法实现,而不是父类中的方法。

  6. 特殊方法(Special Methods): 以双下划线开头和结尾的方法称之为特殊方法,如__init____str__,它们会在特定的情况下由Python调用。

  7. 构造函数(Constructor):构造函数是一个特殊的方法,通常称为__init__,它用于初始化对象的属性。

  8. 类方法(Class Methods)

    • 类方法是一个装饰器,通常称为@classmethod
    • 它们是属于类而不是对象实例的方法。
    • 类方法可以访问和修改类属性,但不能访问实例属性。
  9. 静态方法(Static Methods)

    • 静态方法是一个装饰器,通常称为@staticmethod
    • 它们是不属于类或对象实例的方法,与类无关。
    • 静态方法通常用于实现与类有关的功能,但不需要访问类或对象状态。
  10. 实例化:创建一个类的实例,即类的具体对象。

10.2.2 类属性和实例属性区别及其访问方式

  1. 类属性(Class Attributes)或类变量(Class Variables)

    • 类属性是定义在类级的属性(不是实例级别),通常在类的顶部定义,即定义在类里面、方法外面的属性
    • 类属性类似全局变量,在整个类中都是共享的,对所有类的实例都相同
    • 类里面:通过类名self来调用类属性
    • 类外面:推荐通过类名来访问,而不是使用类实例来访问。另外通常不推荐在类外面修改类属性,因为这会影响所有对象实例。
    • 修改类属性时,如果使用实例对象进行修改,实际上是创建了一个与类属性同名的实例属性,而不是修改类属性本身。这会导致该实例对象有一个与类属性同名的实例属性,但不会影响其他实例或类属性。
      class MyClass:
          class_attr = 100  # 类属性
      
      # 创建一个类对象
      obj1= MyClass()
      
      # 通过实例对象修改类属性
      obj1.class_attr = 200
      
      # 访问类属性和实例属性
      print("类属性:", MyClass.class_attr) 		 # 输出: 100 (类属性未被修改)
      print("实例属性:", obj1.class_attr)    	 	 # 输出: 200 (实际上是一个实例属性)
      
      # 创建另一个对象
      obj2 = MyClass()
      
      # 访问类属性和实例属性
      print("新对象的类属性:", obj2.class_attr) 		 # 输出: 100 (类属性未被修改)
      
      MyClass.class_attr=300
      print("新对象的类属性:", obj2.class_attr) 
      
      类属性: 100
      实例属性: 200
      新对象的类属性: 100
      新对象的类属性: 300
      
  2. 实例属性(Instance Attributes)或实例变量(Instance Variables)

    • 定义在类的方法里面的属性,是对象实例的属性
    • 每个对象实例都可以具有不同的实例属性值。
    • 类里面:只能通过self调用实例属性,因为self是谁调用,它的值就属于该对象
    • 类外面:只能通过实例对象来访问和修改实例属性

  总的来说,类属性应该通过类名来访问或修改,实例属性只能通过实例对象。另外要注意的是,属性与方法名相同,属性会覆盖方法,因此在编写代码时需要小心使用相同的名称,这会导致冲突。

class MyClass:
    def __init__(self):
        self.my_attribute = "This is an attribute."

    def my_method(self):
        return "This is a method."

# 创建一个MyClass对象
my_object = MyClass()

# 访问属性和方法
print(my_object.my_attribute)  			# 访问属性
print(my_object.my_method())   			# 调用方法

# 覆盖方法
my_object.my_method = "This is now an attribute, not a method."

# 再次尝试访问属性和方法
print(my_object.my_attribute)  			# 访问属性
print(my_object.my_method)     			# 访问覆盖后的属性
print(my_object.my_method()) 			# TypeError: 'str' object is not callable
This is an attribute.
This is a method.
This is an attribute.
This is now an attribute, not a method.
TypeError: 'str' object is not callable

  在这个示例中,我们创建了一个名为MyClass的类,其中包含一个属性my_attribute和一个方法my_method。然后,我们创建了一个my_object对象,并访问了属性和方法。

  但接下来,我们将my_method属性设置为字符串,覆盖了原来的方法。因此,当我们再次访问my_method时,它实际上是一个字符串属性,而不再是一个方法。这导致属性覆盖了方法,使得原来的方法不再可用。

10.2.3 self 代表类的示例

  类的方法与普通的函数只有一个区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。

class Test:
    def prt(self):
        print(self)
        print(self.__class__)
 
t = Test()
t.prt()
<__main__.Test instance at 0x100771878>		 
__main__.Test

  上面第一行结果是对象 t 的字符串表示形式,其在内存中的地址为 0x100771878,即self代表的是类的实例。第二行显示了对象 t 所属的类的名称,即 Test 类。

  从这个例子可以看出,self 在类中的作用是指代当前对象实例,实现封装和面向对象编程的基本原则:

  1. 指代当前实例: self 允许你在类的方法内部引用和操作当前对象实例的属性和方法

  2. 实现封装: 使用 self 可以将对象的状态和行为封装在一起。这意味着你可以在类的内部定义属性和方法,同时可以确保这些属性和方法只对对象实例可见和可操作。

  3. 实现方法调用: 当你调用类的方法时,需要告诉方法是哪个对象在调用它。self 在方法的定义中提供了这个信息,使得方法能够正确地操作调用它的对象。

self 不是 python 关键字,你可以用其他名称代替,但习惯上都使用 self

# 这段代码输出结果一样
class Test:
    def prt(runoob):
        print(runoob)
        print(runoob.__class__)
 
t = Test()
t.prt()

10.3 继承

10.3.1 构造函数的继承

  构造函数__init__()的主要作用是初始化对象的属性(实例化类时自动调用),确保对象在创建时具有适当的初始状态。

  当子类继承父类时,子类通常希望继承并初始化父类的属性,可以通过在子类的构造函数中调用父类的构造函数来实现,这分两种情况。

  1. 默认继承父类构造函数
    如果子类没有显式定义构造函数(__init__方法),它会默认继承父类的构造函数来初始化对象。这个默认的继承行为确保子类可以继承父类的属性和方法。例如:
class Parent:
    def __init__(self):
        self.attribute = 42

class Child(Parent):
    pass

# 创建子类对象
child_obj = Child()

# 子类对象可以访问父类和子类的属性
print(child_obj.attribute) 				# 输出42
  1. 改写子类构造函数
    如果显式地定义子类的构造函数,它将覆盖默认的继承行为。通常建议在覆写构造函数时,同时调用父类的构造函数,以确保父类的初始化逻辑也被执行。调用方式有两种:

    • 使用 super().__init__()
      在Python中,可以使用 super().__init__()来调用父类的构造函数。这种方式会自动识别父类,并且不需要显式指定父类的名称。
    • 显式调用父类的构造函数
      在某些编程语言中,或者如果你希望显式指定父类的名称,你可以使用 父类名.__init__(self, ...) 来调用父类的构造函数。这种方式需要明确指定父类的名称。

    注意,super().__init__()只能调用一个父类的构造函数,如果是多继承的情况,推荐使用第二种方式来调用父类的构造函数。

下面是一个复杂的示例,演示了如何在子类中继承并初始化父类的属性:

# 定义一个父类 People
class People:
    name = '' 											    # 类属性,用于存储人名
    age = 0    												# 类属性,用于存储年龄
    __weight = 0											# 定义私有属性,私有属性在类外部无法直接进行访问

    # 构造方法,初始化对象的属性
    def __init__(self, n, a, w):
        self.name = n         								# 实例属性,存储人名
        self.age = a          								# 实例属性,存储年龄
        self.__weight = w     								# 实例属性,存储体重

	    
    def speak(self):										# 定义一个方法,用于输出人的信息
        print("%s 说: 我 %d 岁。" % (self.name, self.age))

# 定义一个子类 Student,继承自 People
class Student(People):
    grade = ''  											# 类属性,用于存储年级
    
    def __init__(self, n, a, w, g):							# 构造方法,初始化学生对象的属性
        # 调用父类 People 的构造方法进行属性的初始化
        super().__init__(n, a, w)  							# 或者使用 People.__init__(self, n, a, w)
        self.grade = g  									# 实例属性,存储年级

    # 覆写(重写)父类的方法
    def speak(self):
        print("%s 说: 我 %d 岁了,我在读 %d 年级" % (self.name, self.age, self.grade))

# 创建一个 Student 类的对象 s,传入姓名、年龄、体重和年级
s = Student('小马的程序人生', 10, 60, 3)
s.speak()  
小马的程序人生 说:10 岁了,我在读 3 年级

  在这个示例中,子类 student 继承了父类 people的属性,但子类也可以有自己的属性,如 grade

  在子类 student 的构造方法 __init__(self, n, a, w, g) 中,首先通过 super().__init__(n, a, w) 来调用父类 people 的构造方法,传递了相同的参数,以便父类初始化这些属性。这是为了确保子类对象同时拥有父类和子类的属性。

  如果没有调用父类 people 的构造函数,则会发生以下问题:

  1. 父类属性不会被初始化:父类 people 中的属性 nameage 和私有属性 __weight 不会被初始化。这意味着子类 student 的对象将不具有这些属性的初始值
  2. 可能导致错误或不一致的行为:如果子类 student 的方法或其他代码依赖于这些属性的初始值,那么没有初始化这些属性可能导致错误或不一致的行为。例如,在 student 类的 speak 方法中,如果使用了 self.nameself.age,并且这些属性没有被初始化,将导致 NameErrorAttributeError 等错误。

  总之,不调用父类的构造方法会导致父类属性未初始化,可能会破坏类的一致性,因此通常在子类的构造方法中应该调用父类的构造方法,以确保父类和子类的属性得到正确的初始化。

10.3.2 多继承中方法的解析顺序

  当一个类继承自多个父类时,如果这些父类中有相同名字的方法,而子类又没有指定使用哪个父类的方法时,解析顺序是从左到右,即使用靠前的父类的方法。

class A:
    def speak(self):
        print("A speaks")

class B:
    def speak(self):
        print("B speaks")

class C(A, B):
    pass

my_c = C()
my_c.speak()  # 输出: "A speaks",因为 C 继承自 A 和 B,但 A 在继承列表的左边,所以优先使用 A 的方法

  在这个示例中,类 C 继承自两个父类 A 和 B,但由于 A 在继承列表的左边,所以 C 中的 speak 方法使用了 A 类的方法。

10.3.3 多继承的注意事项

  多继承(组合继承)允许子类同时继承多个父类的属性和方法。这种继承方式结合了类继承和对象组合(将其他类的对象作为成员属性)两种方式,以实现更多灵活性和复用性。以下是一些组合继承的注意事项和示例:

  1. 潜在的方法冲突: 如果多个父类中具有相同名称的方法,子类可能会在调用时出现方法冲突。在这种情况下,子类必须明确指定要调用的方法,或者通过重写方法来解决冲突。

  2. 构造函数的调用: 子类通常需要在其构造函数中调用每个父类的构造函数,以确保父类的属性正确初始化。

    super() 默认只调用一个父类的构造函数,如果使用 super().__init__(),它将只调用第一个父类的构造函数

  3. 深度继承链: 当使用多层次的组合继承时,需要小心继承链变得过于复杂,可能会导致不必要的复杂性和性能问题。尽量保持继承链的层次不要过深。

  假设我们有两个父类,AnimalMachine,它们分别代表动物和机器的特征和行为。然后,我们创建一个子类 Robot,它同时继承了这两个父类的属性和方法。

# 定义 Animal 类
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        pass

# 定义 Machine 类
class Machine:
    def __init__(self, model):
        self.model = model

    def start(self):
        pass

# 定义 Robot 类,同时继承 Animal 和 Machine
class Robot(Animal, Machine):
    def __init__(self, name, model):
        # 方式1:显示调用父类的构造函数,初始化属性.
        Animal.__init__(self, name)
        Machine.__init__(self, model)

    def speak(self):
        return f"{
      
      self.name} says 'Beep boop!'"

    def start(self):
        return f"{
      
      self.model} starts working."

# 创建 Robot 实例
my_robot = Robot("Robo", "R2000")

# 使用继承的属性和方法
print(my_robot.name)   				 				 # 输出:Robo
print(my_robot.model)   		  					 # 输出:R2000
print(my_robot.speak()) 							 # 输出:Robo says 'Beep boop!'
print(my_robot.start())  							 # 输出:R2000 starts working.

  在上述示例中,Robot 类同时继承了 AnimalMachine 两个父类的属性和方法,然后定义了自己的属性和方法。子类初始化也可以写成:

class Robot(Animal, Machine):
    def __init__(self, name, model):
        # 方式2:直接初始化属性,不调用父类的初始化函数。推荐第一种方式
        self.name=name
        self.model=model

10.4 私有和公有

10.4.1 私有属性

  私有属性是指在类中以双下划线 __ 开头命名的属性,它在类的外部无法直接访问。私有属性具有以下特点:

  1. 增强数据的封装性: 私有属性在类的外部无法直接访问,所以它可以隐藏类内部的实现细节,使外部代码无法直接修改属性的值,提高了对数据的保护。通常,只允许通过类的方法来访问或修改这些属性。

  2. 名称重整: 在Python中,私有属性的名称会被重整,即会在属性名称前加上一个下划线和类名。例如,一个名为 __weight 的私有属性在类 People 中,其实际名称会被重整为 _People__weight,这避免子类意外地覆盖父类的私有属性。

  3. 仍然可以访问: 你可以通过类的方法来访问和修改私有属性,这提供了一定程度的控制,以确保属性的访问和修改是通过类的接口进行的,从而确保数据的完整性和一致性。

    # 一个电子设备类可能具有内部状态属性,但用户不需要知道这些细节
    
    class ElectronicDevice:
        def __init__(self):
            self.__is_on = False  # 私有属性
    
        def turn_on(self):
            # 打开设备方法
            self.__is_on = True
    
        def turn_off(self):
            # 关闭设备方法
            self.__is_on = False
    

  你可以通过重整的私有属性名(例如_People__weight)在类之外直接访问私有属性,但一般不建议这么做,因为这违反了封装的原则。建议只通过类的公有方法来访问和修改私有属性,而不是直接在类外部访问它们。这样做有以下好处:

  1. 安全性: 通过公有方法,你可以在内部控制访问私有属性的逻辑,确保数据的合法性和安全性。如果直接在外部访问私有属性,就失去了这种控制能力,可能导致不可预测的错误。
  2. 可维护性: 当你需要修改类的内部实现时,如果使用了公有方法来访问属性,你可以在不影响外部代码的情况下更改内部实现。如果外部代码直接访问了私有属性,那么任何内部实现的更改都可能导致外部代码的破坏。
  3. 文档和接口: 公有方法提供了类的接口,它们可以被文档化,让其他开发人员更容易理解如何正确使用类。直接访问私有属性会使类的接口变得模糊,降低了代码的可读性。
  4. 继承和子类化: 在子类中,你可以覆盖父类的方法,而不必担心破坏父类的内部状态。如果直接访问父类的私有属性,可能会导致不希望的副作用。

10.4.2 私有方法

  同理私有方法是在类中以双下划线 __ 开头命名的方法,它在类的外部无法被调用,它也具有一些特点:

  • 增强数据的封装性: 私有方法将一些类内部的操作和逻辑隐藏在类的内部,不让外部代码访问,从而提高类的封装性。
  • 提供辅助功能: 私有方法可以用于执行一些辅助功能和操作,使公有方法更专注于核心功能,以提供更清晰和简洁的公有方法接口。
  • 防止不合法调用: 私有方法通常用于执行类的内部操作,不允许外部代码直接调用。

  你可以通过重整的私有方法名(例如_People__method)在类之外直接访问私有方法,但也不建议这么做。

class BankAccount:
    def __init__(self, initial_balance):
        self.__balance = initial_balance  		# 私有属性,表示账户余额
	
	# 存款方法,控制访问私有属性
    def deposit(self, amount):        
        if amount > 0:
            self.__balance += amount
            
	# 取款方法,控制访问私有属性
    def withdraw(self, amount):        
        if 0 < amount <= self.__balance:
            self.__balance -= amount

    def get_balance(self):
        return self.__balance
	
	# 私有方法,记录交易日志
    def __log_transaction(self, transaction_type, amount):        
        print(f"{
      
      transaction_type}: ${
      
      amount}")

# 创建银行账户对象
account = BankAccount(1000)

# 使用公有方法进行操作
account.deposit(500)
account.withdraw(200)

# 无法直接访问私有属性和私有方法
# print(account.__balance)  					 # 报错:AttributeError: 'BankAccount' object has no attribute '__balance'
# account.__log_transaction("Deposit", 500) 	 # 报错:AttributeError: 'BankAccount' object has no attribute '__log_transaction'

account.get_balance()
1300

  在上面的示例中,银行账户的余额是敏感信息,通过将BankAccount 类的 __balance 设为私有属性,将 __log_transaction()设为私有方法,隐藏了账户余额和交易日志的实现细节,只能通过公有方法来访问和记录交易,这提供了更好的封装和安全性。

10.4.3 父类的私有属性和私有方法

  在Python中,子类是无法直接继承父类的私有属性和私有方法。私有属性和私有方法是被设计为在类内部使用的,不会被子类继承。这是Python中封装的一部分,目的是防止子类意外地修改或访问父类的私有成员。

class Parent:
    def __init__(self):
        self.__private_attribute = 42

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

class Child(Parent):
	# 不显示地定义构造函数,也会继承父类的属性,但不会继承其私有属性
    def access_parent_private(self):
        # 子类无法直接继承父类的私有属性和方法
        # 这里实际上是创建了一个新的子类私有属性和方法
        print(self.__private_attribute)  # 这是一个新的子类私有属性
        self.__private_method()  # 这是一个新的子类私有方法

child = Child()
child.access_parent_private()
AttributeError: 'Child' object has no attribute '_Child__private_attribute'

  上面这段代码中,因为子类并不能继承父类的私有属性好私有方法,所以access_parent_private函数中的调用,会被Python认为是子类Child的私有属性和私有方法,但这些在子类中我们没有定义,所以会报错AttributeError

  不过,私有属性可以通过类的方法来访问,所以子类如果想访问父类的私有属性,可以在父类中加入调用其私有属性的方法,并继承给子类。

class Parent:
    def __init__(self, value):
        self.__private_attribute = value				# 父类私有属性

    def get_private_attribute(self):
        return self.__private_attribute					# 父类方法调用其私有属性

class Child(Parent):
    def __init__(self, value_child, value):
        super().__init__(value)  						# 继承父类构造函数并传入父类的参数
        self.__private_attribute_child = value_child    # 子类私有属性

    def get_child_attributes(self):
    	# 返回子类私有属性,以及继承的父类方法来调用父类的私有属性
        return self.__private_attribute_child, self.get_private_attribute()

child_obj = Child(42, 100)

# 访问子类和父类的私有属性
child_attr, parent_attr = child_obj.get_child_attributes()
print("子类私有属性:", child_attr)  						
print("父类私有属性:", parent_attr)  					
子类私有属性: 42
父类私有属性: 100

10.5 魔法方法(特殊方法)

10.5.1 魔法方法简介

  魔法方法(Magic Methods),也称为双下划线方法或特殊方法,是Python中的一类特殊方法,它们以双下划线开头和结尾,例如__init____str____eq__等。这些方法有特殊的用途和行为,用于自定义类的行为和操作,会在特定的时候被调用,它们和普通方法的差异,主要在于其调用方式和用途:

  1. 调用方式:普通方法是通过对象实例调用的,而魔法方法通常由Python解释器在特定的情况下自动调用。例如,__init__方法是在创建对象时由Python自动调用的,普通方法则是根据需要在代码中显式调用的。

  2. 用途:魔法方法用于实现对象的特殊行为和操作,例如初始化对象、自定义对象的字符串表示、支持对象的比较、支持算术操作等。它们允许你覆盖默认的操作,以满足你的特定需求。普通方法是类的一般行为,用于实现类的功能。

  总的来说,魔法方法使你的类可以模拟内置数据类型的行为,从而使代码更具表现力和可读性。通过实现适当的魔法方法,你可以让你的自定义对象更自然地与Python语言和标准库进行交互。以下是一些常见的魔法方法

魔法函数 作用 示例 作用
__init__(self, ...) 初始化对象的属性 def __init__(self, ...) obj = MyClass(...)
__del__() 用于在对象销毁前执行清理操作,例如关闭文件,释放资源,通常不需要手动指定 def __del__(self, ...)
字符串魔法方法 作用 示例 作用
__str__(self) 返回对象的字符串表示 def __str__(self) str(obj)
容器操作魔法方法 作用 示例 作用
__getitem__(self, key) 获取对象的元素 def __getitem__(self, key) obj[key]
__setitem__(self, key, value) 设置对象的元素 def __setitem__(self, key, value) obj[key] = value
__delitem__(self, key) 删除对象的元素 def __delitem__(self, key) del obj[key]
__len__(self) 返回对象的长度 def __len__(self) len(obj)
__iter__(self) 返回对象的迭代器 def __iter__(self) iter(obj)
__next__(self) 返回迭代器的下一个元素 def __next__(self) next(iterator)
__contains__(self, item) 检查对象是否包含某个元素 def __contains__(self, item) item in obj
比较&运算魔法方法 作用 示例 作用
__eq__(self, other) 比较两个对象是否相等 def __eq__(self, other) obj1 == obj2
__ne__(self, other) 比较两个对象是否不相等 def __ne__(self, other) obj1 != obj2
__lt__(self, other) 比较两个对象是否小于 def __lt__(self, other) obj1 < obj2
__le__(self, other) 比较两个对象是否小于等于 def __le__(self, other) obj1 <= obj2
__gt__(self, other) 比较两个对象是否大于 def __gt__(self, other) obj1 > obj2
__ge__(self, other) 比较两个对象是否大于等于 def __ge__(self, other) obj1 >= obj2
算术操作魔法方法 作用 示例 作用
__add__(self, other) 实现对象的加法操作 def __add__(self, other) obj1 + obj2
__sub__(self, other) 实现对象的减法操作 def __sub__(self, other) obj1 - obj2
__mul__(self, other) 实现对象的乘法操作 def __mul__(self, other) obj1 * obj2
__div__(self, other) 实现对象的除法操作 def __div__(self, other) obj1 / obj2
__pos__(self) 正号操作符,用于实现正号操作 +obj
__neg__(self) 负号操作符,用于实现负号操作 -obj
__abs__(self) 绝对值操作,用于实现绝对值操作 abs(obj)
__invert__(self) 按位求反操作符,用于实现按位求反 ~obj

  这些是一些常见的魔法方法,它们可以帮助你自定义类的行为,使其更符合你的需求。你可以根据需要选择性地实现这些方法,以改变类的默认行为。

10.5.2 容器操作魔法方法

容器对象分为可变(Mutable)和不可变(Immutable),根据需要来实现的魔法方法:

  1. 不可变容器:如果你希望你的容器对象是不可变的(元组、字符串),也就是说一旦创建就不能修改其内容,那么只需要定义两个魔法方法:

    • __len__():这个方法用于返回容器中元素的数量(长度)。
    • __getitem__(self, key):这个方法用于获取容器中指定位置的元素。

    不可变容器通常用于表示一组数据,这些数据在创建后不能被更改,比如元组(tuple)。

  2. 可变容器:如果你希望你的容器对象是可变的(列表、字典),也就是说你可以修改、添加、删除容器中的元素,那么除了上述两个方法,还需要定义以下两个魔法方法:

    • __setitem__(self, key, value):这个方法用于设置容器中指定位置的元素的值。
    • __delitem__(self, key):这个方法用于删除容器中指定位置的元素。

示例:编写一个可改变的自定义列表,要求记录列表中每个元素被访问的次数。

class CountList:
    def __init__(self, *args):
        self.values = [x for x in args]
        # fromkeys(iterable, value) 是字典的一个方法,前者是字典的键,后者是所有键的初始值
        # 创建了一个字典,其中的键是从 0 到 len(self.values) - 1 的整数,所有的值都被初始化为 0
        self.count = {
    
    }.fromkeys(range(len(self.values)), 0)

    def __len__(self):
        return len(self.values)

    def __getitem__(self, item):
        self.count[item] += 1
        return self.values[item]

    def __setitem__(self, key, value):
        self.values[key] = value

    def __delitem__(self, key):
        del self.values[key]
        for i in range(0, len(self.values)):
            if i >= key:
                self.count[i] = self.count[i + 1]
        self.count.pop(len(self.values))
c1 = CountList(1, 3, 5, 7, 9)
c2 = CountList(2, 4, 6, 8, 10)
print(c1[1],c2[2])  				# 3,6

c2[2] = 12
print(c1[1] + c2[2])  				# 15
print(c1.count)						# {0: 0, 1: 2, 2: 0, 3: 0, 4: 0}
print(c2.count)						# {0: 0, 1: 0, 2: 2, 3: 0, 4: 0}
del c1[1]
print(c1.count)						# {0: 0, 1: 0, 2: 0, 3: 0}

10.5.3 迭代器

10.5.3.1 迭代器协议

  在Python中,迭代器(Iterator)是一种用于遍历可迭代对象(Iterable)元素的对象,而可迭代对象是那些可以被迭代(遍历)的对象,如列表、元组、字典、集合等。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。

  迭代器是一种特殊的对象,它有两个基本的方法——iter()next(),这是通过实现魔法方法 __iter__()__next__()来实现的。

  1. __iter__() 方法:这是迭代器必须实现的方法,用于返回一个带有 __next__() 方法的迭代器对象

  2. __next__() 方法:用于获取迭代器中的下一个元素。遍历结束后再次调用会 引发 StopIteration 异常,表示迭代结束。

  3. iter(object) :用于生成迭代器,通常由 for 循环隐式调用。当你使用 iter() 函数来获取一个可迭代对象的迭代器时,实际上就是调用了该对象的 __iter__() 方法,从而获取了迭代器。

  4. next(iterator, default) :内置函数,用来显式调用迭代器对象的 __next__() 方法,从而逐一获取容器中的元素。

    • iterator:要获取下一个元素的迭代器对象。
    • default(可选):如果迭代器已遍历完最后一个元素,则返回 default 值(可以是任何合法的Python对象)。如果不提供 default 参数,那么继续会引发 StopIteration 异常。

  next() 函数中的 default 参数可以是任何合法的 Python 数据类型,例如整数、浮点数、字符串、 列表、元组、集合、字典等数据结构,以及自定义对象(只要是合法的)。

  下面是一个简单的示例,演示了如何使用类创建一个迭代器。

# 定义一个自定义迭代器类 MyIterator
class MyIterator:
    def __init__(self, start, end):
        self.current = start  							# 初始化当前值为起始值
        self.end = end        							# 存储结束值

    # 实现 __iter__() 方法,返回迭代器对象本身
    def __iter__(self):
        return self

    # 实现 __next__() 方法,用于获取下一个元素
    def __next__(self):        
        if self.current < self.end:						# 如果当前值小于结束值,生成下一个元素
            result = self.current  						# 保存当前值到 result
            self.current += 1      						# 更新当前值为下一个值
            return result          						# 返回当前值
        else:
            raise StopIteration    						# StopIteration 用于标识迭代的完成,防止出现无限循环的情况

my_iterator = MyIterator(1, 5)

# 使用迭代器遍历元素
for item in my_iterator:
    print(item)  										# 输出1,2,3,4

  定义 __iter__() 方法是为了返回一个带有 __next__() 方法的对象。 如果类已定义了 __next__(),而且这个方法返回了下一个迭代值,那么 __iter__() 可以简单地返回 self,因为这个类的实例本身已经充当了迭代器。

10.5.3.2 for 循环原理

  大多数内置的可迭代对象(如字符串、列表、元组、集合、字典的键、字典的值)都支持迭代器功能,因此您可以使用 iter()next() 函数来显示地进行迭代。

my_list = [1, 2, 3]
my_iterator = iter(my_list)  # 获取列表的迭代器

print(next(my_iterator))  # 获取下一个元素,输出:1
print(next(my_iterator))  # 获取下一个元素,输出:2
print(next(my_iterator))  # 获取下一个元素,输出:3

print(next(my_iterator))  # 迭代器耗尽,再次调用next会引发StopIteration异常

你也可以设置 default 参数,例如:

print(next(my_iterator,0))						# 输出:0
print(next(my_iterator,"No more items"))		# 输出:"No more items"
print(next(my_iterator,[1, 2, 3]))				# 输出:[1, 2, 3]

但通常情况下,使用 for 循环通常更加简洁和直观。在使用for循环时:

  1. 当你使用 for 语句遍历一个容器对象时,例如 for item in container,Python 首先会在容器对象上调用 iter() 函数。

  2. iter() 函数返回一个迭代器对象,这个迭代器对象包含了一个特殊的方法 __next__(),用于逐一访问容器中的元素。

  3. 在每次循环迭代中,for 循环会自动调用迭代器对象的 __next__() 方法,从而实现逐一访问容器中的元素。当迭代到容器末尾时,__next__() 方法会引发 StopIteration 异常,告知循环停止迭代。

  这个过程是 Python 遍历容器对象的基本机制,它使得 for 循环可以用来迭代许多不同类型的数据结构,而无需知道底层实现的细节。

10.5.3.3 自定义迭代行为

  用户可以轻松地自定义对象的迭代行为,只需实现 __iter__()__next__() 方法即可。下面是一个用于生成斐波那契数列的自定义迭代器:

# 定义一个名为 Fibs 的自定义迭代器类,用于生成斐波那契数列
class Fibs:
    def __init__(self, n=10):
        self.a = 0           							 # 初始化第一个斐波那契数为 0
        self.b = 1           							 # 初始化第二个斐波那契数为 1
        self.n = n          		   					 # 存储生成斐波那契数列的上限值 n

    def __iter__(self):
        return self

    # 实现 __next__() 方法,用于生成下一个斐波那契数
    def __next__(self):
        self.a, self.b = self.b, self.a + self.b  		 # 计算下一个斐波那契数
        if self.a > self.n:                       		 # 如果当前斐波那契数大于上限值 n
            raise StopIteration                   		 # 则抛出 StopIteration 异常,标识迭代结束
        return self.a                             		 # 返回当前斐波那契数


fibs = Fibs(100)										 # 创建一个名为 fibs 的斐波那契数列迭代器,上限值为 100

for each in fibs:
    print(each, end=' ')  								 # 打印每个斐波那契数,并以空格分隔
1 1 2 3 5 8 13 21 34 55 89 

10.5.4 生成器

  生成器(Generator)是一种特殊类型的迭代器,它允许你在迭代过程中逐个生成值,而不是一次性生成并存储所有值,其特点有:

  1. 延迟生成、节省内存:生成器一次生成一个值,而不是一次性生成所有值,即不需要保存所有值在内存中,降低了内存的占用,更适用于处理大型数据集或无限数据流。

  2. 语法简洁,易于实现:在函数中使用 yield 关键字就可以定义生成值的规则,而不需要显式地编写 __iter__()__next__() 方法,实现相对简单。

  3. 简化迭代过程:生成器隐藏了迭代的复杂性,使代码更简洁易读。

  4. 局部变量和执行状态自动保存:在生成器函数中,局部变量和执行状态会在每次生成值时自动保存和恢复。这意味着你可以在生成器函数中使用普通的局部变量,而不需要使用类的实例变量(如 self.index 和 self.data)。这让代码编写更容易理解和维护。

10.5.4.1 生成器的生成方式

生成器可以通过两种方式来定义:

  1. 使用生成器表达式:类似于列表推导式,但使用圆括号而不是方括号。

    # 使用生成器表达式创建生成器
    generator_expr = (x * 2 for x in range(5))
    
    # 使用生成器表达式生成值
    for value in generator_expr:
        print(value)  					  # 输出 0, 2, 4, 6, 8
    
  2. 使用函数和 yield 关键字:在函数中使用 yield 关键字可以将函数转变为生成器。每次调用生成器的 __next__() 方法时,函数将从上一次 yield 语句的位置恢复执行,然后继续执行直到下一个 yield 语句或函数结束。这样就允许你在迭代过程中逐个生成值,而不会从头开始执行函数。

    def my_generator():
        yield 1
        yield 2
        yield 3
    
    gen = my_generator()
    
    print(next(gen))  					 # 第一次调用 __next__(),执行第一个 yield 语句,生成值 1
    print(next(gen)) 					 # 第二次调用 __next__(),从上一次的位置继续执行,生成值 2
    print(next(gen))  					 # 第三次调用 __next__(),从上一次的位置继续执行,生成值 3
    
    # 使用函数和 yield 创建生成器
    def my_generator():
        for x in range(5):
            yield x * 2
    
    # 使用函数和 yield 生成值
    gen = my_generator()
    for value in gen:
        print(value)  					# 0, 2, 4, 6, 8
    
10.5.4.2 特性:延迟生成,节省内存

  下面例子说明了生成器表达式逐个生成值的特点,以及与列表等可迭代对象在处理大数据或无限数据流时的区别。

  1. 生成器:

    • 创建时只是生成器对象:这个生成器对象包含了生成元素的规则和状态信息,不会立即生成或存储所有的元素。
    • 迭代时逐个生成元素:当你迭代生成器时,它会根据定义的规则逐个生成元素,每次生成一个元素,并在需要时(例如for循环中)按需生成下一个元素。
    • 我们使用生成器表达式来创建一个生成器,但是在创建时,它并不会立即计算并存储所有的元素,只会在需要时(比如for循环中),随着一次次迭代来生成一个个的值。即生成器只在需要时生成值,而不会一次性生成并存储所有值
    import sys
    
    # 创建一个生成器,用于生成一组偶数
    generator_expr = (x * 2 for x in range(10**6))
    # 生成器只包含规则和状态信息,不占用大量内存
    gen_size = sys.getsizeof(generator_expr)
    
    print(f"生成器类型:{
            
            type(generator_expr)} , 生成器的大小(字节):{
            
            gen_size}")
    
    生成器类型:<class 'generator'> , 生成器的大小(字节):208
    
    # 迭代生成器表达式,逐个生成值
    	for value in generator_expr:
    	    print(value)
    
  2. 使用列表:我们使用列表推导式创建了一个列表,这个列表在一开始创建的时候,就会立即生成并存储所有的元素,这会占用大量内存,特别是当数据集很大时。

    # 列表推导式,用于生成一组偶数
    list_comp = [x * 2 for x in range(10**6)]
    print("列表占用的内存:", big_list.__sizeof__(), "字节")
    
    列表占用的内存: 8448712 字节
    

  所以,生成器非常适合处理大型数据集或需要延迟生成值的情况,而列表等序列则适用于小型数据集或需要一次性访问所有值的情况。

10.5.4.3 特性:简洁优雅

  生成器不仅语法简洁,而且生成器函数允许你在函数内部使用普通的局部变量来管理状态,而不需要像类的实例方法那样使用实例变量。下面通过斐波那契数列的例子来说明。

def fibonacci_generator():
    a, b = 1, 1  # 使用局部变量 a 和 b 来保存斐波那契数列的前两个元素
    while True:
        yield a  # 生成当前斐波那契数列的值
        a, b = b, a + b  # 更新局部变量 a 和 b,计算下一个斐波那契数列的值

# 创建斐波那契数列生成器
fib_gen = fibonacci_generator()

# 逐个生成并打印斐波那契数列的值
for _ in range(11):
    print(next(fib_gen),end=' ')
1 1 2 3 5 8 13 21 34 55 89 

  在上述示例中,我们使用局部变量 ab 来管理斐波那契数列的前两个元素。生成器函数 fibonacci_generator() 使用 yield 语句生成当前的斐波那契数值,并在每次迭代中更新局部变量 ab 来计算下一个值。这使得我们可以使用普通的局部变量来管理状态,而不需要使用类的实例变量,从而让代码更加清晰和易于理解。

如果是用类来实现,代码为:

# 定义一个名为 Fibs 的自定义迭代器类,用于生成斐波那契数列
class Fibs:
    def __init__(self, n=10):
        self.a = 0           							 # 初始化第一个斐波那契数为 0
        self.b = 1           							 # 初始化第二个斐波那契数为 1
        self.n = n          		   					 # 存储生成斐波那契数列的上限值 n

    def __iter__(self):
        return self

    # 实现 __next__() 方法,用于生成下一个斐波那契数
    def __next__(self):
        self.a, self.b = self.b, self.a + self.b  		 # 计算下一个斐波那契数
        if self.a > self.n:                       		 # 如果当前斐波那契数大于上限值 n
            raise StopIteration                   		 # 则抛出 StopIteration 异常,标识迭代结束
        return self.a                             		 # 返回当前斐波那契数


fibs = Fibs(100)										 # 创建一个名为 fibs 的斐波那契数列迭代器,上限值为 100

for each in fibs:
    print(each, end=' ')  								 # 打印每个斐波那契数,并以空格分隔
1 1 2 3 5 8 13 21 34 55 89

  总之,生成器提供了一种更简洁、更易于编写和理解的方式来实现迭代器。生成器的语法和特性使得处理迭代任务更加方便和优雅。

10.5.5 类和属性相关操作

  Python中有一些内置函数,用于类继承关系的判断、对象类型的检查,以及对象属性的操作。这些函数可以让开发者更好地管理和操作类和对象的行为,使代码更具表现力和可读性。

函数 描述
type(obj) 获取对象的类型。
issubclass(class, classinfo) 检查一个类是否是另一个类的子类。
isinstance(obj, classinfo) 检查对象是否是指定类型的实例。
hasattr(obj, name) 检查对象是否包含指定名称的属性。
getattr(obj, name[, default]) 获取对象的指定属性的值,可选地提供默认值。
setattr(obj, name, value) 设置对象的属性值,如果属性不存在则创建新属性。
delattr(obj, name) 删除对象的指定属性。
property([fget[, fset[, fdel[, doc]]]]) 创建属性,允许定义属性的访问、设置和删除操作。

下面是一个简单的使用示例:

# 定义一个简单的类
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def say_hello(self):
        print(f"Hello, my name is {
      
      self.name} and I am {
      
      self.age} years old.")

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

# 使用内置函数 type() 获取对象的类型
print(type(person1))  										# 输出:<class '__main__.Person'>

# 使用内置函数 isinstance() 检查对象是否是特定类型的实例
print(isinstance(person1, Person)) 						    # 输出:True

# 使用内置函数 hasattr() 检查对象是否包含指定属性
print(hasattr(person1, 'name'))  							# 输出:True

# 使用内置函数 getattr() 获取对象的属性值
name = getattr(person1, 'name')
print(name)  												# 输出:Alice

# 使用内置函数 setattr() 设置对象的属性值
setattr(person1, 'age', 35)
person1.say_hello()  										# 输出:Hello, my name is Alice and I am 35 years old.

# 使用内置函数 delattr() 删除对象的属性
delattr(person1, 'age')
print(hasattr(person1, 'age'))  							# 输出:False

# 使用内置函数 issubclass() 检查类之间的继承关系
class Student(Person):
    def __init__(self, name, age, student_id):
        super().__init__(name, age)
        self.student_id = student_id

print(issubclass(Student, Person))  						# 输出:True

猜你喜欢

转载自blog.csdn.net/qq_56591814/article/details/132765313