十、python的类与对象
参考:Python 3.11官方文档《类》、《datawhale——PythonLanguage》、《Python3 面向对象》
10.1 面向对象编程
面向对象编程(Object-Oriented Programming,OOP)是一种编程范式或编程方法,在许多编程语言中得到广泛应用,包括Python、Java、C++、C#等。它以对象为核心,将数据和操作数据的方法(属性和方法)封装在一起,以模拟现实世界中的事物和交互。
OOP 的主要思想是将问题分解为对象,并通过这些对象之间的互动来解决问题,有助于构建更清晰、可维护和可扩展的代码。以下是面向对象编程的关键概念:
-
对象(Object):对象是现实世界中的实体或事物的抽象表示,是类的实例。它包括属性(数据)和操作数据的方法。对象可以是物理实体(例如,汽车、手机)或概念实体(例如,用户、订单)。
-
类(Class):类是对象的模板或结构,它定义了对象的属性和方法,它规定了对象应该有哪些特征和行为。类具有三大特性——封装、继承和多态。
-
封装(Encapsulation):封装是将数据(属性)和操作数据的方法(方法)捆绑在一起的概念。它隐藏了对象内部的细节,只提供了有限的接口供外部访问。封装使得对象的实现细节可以随时更改,而不会影响使用该对象的代码,这提高了数据的安全性和代码的可维护性
-
继承(Inheritance):继承允许一个子类继承另一个父类的属性和方法。子类可以重用父类的代码,并可以在其基础上进行扩展或修改。继承建立了类之间的层次关系。
-
多态(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
方法,然后创建了两个子类 Dog
和 Cat
,它们继承了 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
的类,该类具有name
和age
属性。我们创建了一个名为my_dog
的对象,并访问了它的属性。
请注意,self
是一个特殊的参数,它指代对象本身。在类的方法中,通常会看到 self
,以便访问和操作对象的属性。
10.2.1 类与对象的基本概念
-
属性: 在类的声明中,属性是用变量来表示的,这些属性用于存储对象的数据。
-
类属性(Class Attributes)或类变量(Class Variables):定义在类里面、方法外面的属性
-
实例属性(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
-
方法(Methods):类当中定义的函数称之为方法,它们定义了对象可以执行的操作,并使用
self
作为第一个参数,以引用对象实例。 -
方法重写:当一个子类继承自一个父类,并且在子类中定义了与父类同名的方法或属性时,子类中的方法会自动覆盖(重写)父类的方法。这意味着在子类中调用该方法时,将使用子类中的方法实现,而不是父类中的方法。
-
特殊方法(Special Methods): 以双下划线开头和结尾的方法称之为特殊方法,如
__init__
和__str__
,它们会在特定的情况下由Python调用。 -
构造函数(Constructor):构造函数是一个特殊的方法,通常称为
__init__
,它用于初始化对象的属性。 -
类方法(Class Methods):
- 类方法是一个装饰器,通常称为
@classmethod
。 - 它们是属于类而不是对象实例的方法。
- 类方法可以访问和修改类属性,但不能访问实例属性。
- 类方法是一个装饰器,通常称为
-
静态方法(Static Methods):
- 静态方法是一个装饰器,通常称为
@staticmethod
。 - 它们是不属于类或对象实例的方法,与类无关。
- 静态方法通常用于实现与类有关的功能,但不需要访问类或对象状态。
- 静态方法是一个装饰器,通常称为
-
实例化:创建一个类的实例,即类的具体对象。
10.2.2 类属性和实例属性区别及其访问方式
-
类属性(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
-
实例属性(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
在类中的作用是指代当前对象实例,实现封装和面向对象编程的基本原则:
-
指代当前实例:
self
允许你在类的方法内部引用和操作当前对象实例的属性和方法 -
实现封装: 使用
self
可以将对象的状态和行为封装在一起。这意味着你可以在类的内部定义属性和方法,同时可以确保这些属性和方法只对对象实例可见和可操作。 -
实现方法调用: 当你调用类的方法时,需要告诉方法是哪个对象在调用它。
self
在方法的定义中提供了这个信息,使得方法能够正确地操作调用它的对象。
self 不是 python 关键字,你可以用其他名称代替,但习惯上都使用 self
。
# 这段代码输出结果一样
class Test:
def prt(runoob):
print(runoob)
print(runoob.__class__)
t = Test()
t.prt()
10.3 继承
10.3.1 构造函数的继承
构造函数__init__()
的主要作用是初始化对象的属性(实例化类时自动调用),确保对象在创建时具有适当的初始状态。
当子类继承父类时,子类通常希望继承并初始化父类的属性,可以通过在子类的构造函数中调用父类的构造函数来实现,这分两种情况。
- 默认继承父类构造函数
如果子类没有显式定义构造函数(__init__
方法),它会默认继承父类的构造函数来初始化对象。这个默认的继承行为确保子类可以继承父类的属性和方法。例如:
class Parent:
def __init__(self):
self.attribute = 42
class Child(Parent):
pass
# 创建子类对象
child_obj = Child()
# 子类对象可以访问父类和子类的属性
print(child_obj.attribute) # 输出42
-
改写子类构造函数
如果显式地定义子类的构造函数,它将覆盖默认的继承行为。通常建议在覆写构造函数时,同时调用父类的构造函数,以确保父类的初始化逻辑也被执行。调用方式有两种:- 使用
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 的构造函数,则会发生以下问题:
- 父类属性不会被初始化:父类
people
中的属性name
、age
和私有属性__weight
不会被初始化。这意味着子类student
的对象将不具有这些属性的初始值 - 可能导致错误或不一致的行为:如果子类
student
的方法或其他代码依赖于这些属性的初始值,那么没有初始化这些属性可能导致错误或不一致的行为。例如,在student
类的speak
方法中,如果使用了self.name
或self.age
,并且这些属性没有被初始化,将导致NameError
或AttributeError
等错误。
总之,不调用父类的构造方法会导致父类属性未初始化,可能会破坏类的一致性,因此通常在子类的构造方法中应该调用父类的构造方法,以确保父类和子类的属性得到正确的初始化。
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 多继承的注意事项
多继承(组合继承)允许子类同时继承多个父类的属性和方法。这种继承方式结合了类继承和对象组合(将其他类的对象作为成员属性)两种方式,以实现更多灵活性和复用性。以下是一些组合继承的注意事项和示例:
-
潜在的方法冲突: 如果多个父类中具有相同名称的方法,子类可能会在调用时出现方法冲突。在这种情况下,子类必须明确指定要调用的方法,或者通过重写方法来解决冲突。
-
构造函数的调用: 子类通常需要在其构造函数中调用每个父类的构造函数,以确保父类的属性正确初始化。
super()
默认只调用一个父类的构造函数,如果使用super().__init__()
,它将只调用第一个父类的构造函数 -
深度继承链: 当使用多层次的组合继承时,需要小心继承链变得过于复杂,可能会导致不必要的复杂性和性能问题。尽量保持继承链的层次不要过深。
假设我们有两个父类,Animal
和 Machine
,它们分别代表动物和机器的特征和行为。然后,我们创建一个子类 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
类同时继承了 Animal
和 Machine
两个父类的属性和方法,然后定义了自己的属性和方法。子类初始化也可以写成:
class Robot(Animal, Machine):
def __init__(self, name, model):
# 方式2:直接初始化属性,不调用父类的初始化函数。推荐第一种方式
self.name=name
self.model=model
10.4 私有和公有
10.4.1 私有属性
私有属性是指在类中以双下划线 __
开头命名的属性,它在类的外部无法直接访问。私有属性具有以下特点:
-
增强数据的封装性: 私有属性在类的外部无法直接访问,所以它可以隐藏类内部的实现细节,使外部代码无法直接修改属性的值,提高了对数据的保护。通常,只允许通过类的方法来访问或修改这些属性。
-
名称重整: 在Python中,私有属性的名称会被重整,即会在属性名称前加上一个下划线和类名。例如,一个名为
__weight
的私有属性在类People
中,其实际名称会被重整为_People__weight
,这避免子类意外地覆盖父类的私有属性。 -
仍然可以访问: 你可以通过类的方法来访问和修改私有属性,这提供了一定程度的控制,以确保属性的访问和修改是通过类的接口进行的,从而确保数据的完整性和一致性。
# 一个电子设备类可能具有内部状态属性,但用户不需要知道这些细节 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
)在类之外直接访问私有属性,但一般不建议这么做,因为这违反了封装的原则。建议只通过类的公有方法来访问和修改私有属性,而不是直接在类外部访问它们。这样做有以下好处:
- 安全性: 通过公有方法,你可以在内部控制访问私有属性的逻辑,确保数据的合法性和安全性。如果直接在外部访问私有属性,就失去了这种控制能力,可能导致不可预测的错误。
- 可维护性: 当你需要修改类的内部实现时,如果使用了公有方法来访问属性,你可以在不影响外部代码的情况下更改内部实现。如果外部代码直接访问了私有属性,那么任何内部实现的更改都可能导致外部代码的破坏。
- 文档和接口: 公有方法提供了类的接口,它们可以被文档化,让其他开发人员更容易理解如何正确使用类。直接访问私有属性会使类的接口变得模糊,降低了代码的可读性。
- 继承和子类化: 在子类中,你可以覆盖父类的方法,而不必担心破坏父类的内部状态。如果直接访问父类的私有属性,可能会导致不希望的副作用。
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__
等。这些方法有特殊的用途和行为,用于自定义类的行为和操作,会在特定的时候被调用,它们和普通方法的差异,主要在于其调用方式和用途:
-
调用方式:普通方法是通过对象实例调用的,而魔法方法通常由Python解释器在特定的情况下自动调用。例如,
__init__
方法是在创建对象时由Python自动调用的,普通方法则是根据需要在代码中显式调用的。 -
用途:魔法方法用于实现对象的特殊行为和操作,例如初始化对象、自定义对象的字符串表示、支持对象的比较、支持算术操作等。它们允许你覆盖默认的操作,以满足你的特定需求。普通方法是类的一般行为,用于实现类的功能。
总的来说,魔法方法使你的类可以模拟内置数据类型的行为,从而使代码更具表现力和可读性。通过实现适当的魔法方法,你可以让你的自定义对象更自然地与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),根据需要来实现的魔法方法:
-
不可变容器:如果你希望你的容器对象是不可变的(元组、字符串),也就是说一旦创建就不能修改其内容,那么只需要定义两个魔法方法:
__len__()
:这个方法用于返回容器中元素的数量(长度)。__getitem__(self, key)
:这个方法用于获取容器中指定位置的元素。
不可变容器通常用于表示一组数据,这些数据在创建后不能被更改,比如元组(tuple)。
-
可变容器:如果你希望你的容器对象是可变的(列表、字典),也就是说你可以修改、添加、删除容器中的元素,那么除了上述两个方法,还需要定义以下两个魔法方法:
__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__()
来实现的。
-
__iter__()
方法:这是迭代器必须实现的方法,用于返回一个带有__next__()
方法的迭代器对象 -
__next__()
方法:用于获取迭代器中的下一个元素。遍历结束后再次调用会 引发StopIteration
异常,表示迭代结束。 -
iter(object)
:用于生成迭代器,通常由 for 循环隐式调用。当你使用iter()
函数来获取一个可迭代对象的迭代器时,实际上就是调用了该对象的__iter__()
方法,从而获取了迭代器。 -
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循环时:
-
当你使用
for
语句遍历一个容器对象时,例如for item in container
,Python 首先会在容器对象上调用iter()
函数。 -
iter()
函数返回一个迭代器对象,这个迭代器对象包含了一个特殊的方法__next__()
,用于逐一访问容器中的元素。 -
在每次循环迭代中,
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)是一种特殊类型的迭代器,它允许你在迭代过程中逐个生成值,而不是一次性生成并存储所有值,其特点有:
-
延迟生成、节省内存:生成器一次生成一个值,而不是一次性生成所有值,即不需要保存所有值在内存中,降低了内存的占用,更适用于处理大型数据集或无限数据流。
-
语法简洁,易于实现:在函数中使用
yield
关键字就可以定义生成值的规则,而不需要显式地编写__iter__()
和__next__()
方法,实现相对简单。 -
简化迭代过程:生成器隐藏了迭代的复杂性,使代码更简洁易读。
-
局部变量和执行状态自动保存:在生成器函数中,局部变量和执行状态会在每次生成值时自动保存和恢复。这意味着你可以在生成器函数中使用普通的局部变量,而不需要使用类的实例变量(如 self.index 和 self.data)。这让代码编写更容易理解和维护。
10.5.4.1 生成器的生成方式
生成器可以通过两种方式来定义:
-
使用生成器表达式:类似于列表推导式,但使用圆括号而不是方括号。
# 使用生成器表达式创建生成器 generator_expr = (x * 2 for x in range(5)) # 使用生成器表达式生成值 for value in generator_expr: print(value) # 输出 0, 2, 4, 6, 8
-
使用函数和
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 特性:延迟生成,节省内存
下面例子说明了生成器表达式逐个生成值的特点,以及与列表等可迭代对象在处理大数据或无限数据流时的区别。
-
生成器:
- 创建时只是生成器对象:这个生成器对象包含了生成元素的规则和状态信息,不会立即生成或存储所有的元素。
- 迭代时逐个生成元素:当你迭代生成器时,它会根据定义的规则逐个生成元素,每次生成一个元素,并在需要时(例如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)
-
使用列表:我们使用列表推导式创建了一个列表,这个列表在一开始创建的时候,就会立即生成并存储所有的元素,这会占用大量内存,特别是当数据集很大时。
# 列表推导式,用于生成一组偶数 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
在上述示例中,我们使用局部变量 a
和 b
来管理斐波那契数列的前两个元素。生成器函数 fibonacci_generator()
使用 yield
语句生成当前的斐波那契数值,并在每次迭代中更新局部变量 a
和 b
来计算下一个值。这使得我们可以使用普通的局部变量来管理状态,而不需要使用类的实例变量,从而让代码更加清晰和易于理解。
如果是用类来实现,代码为:
# 定义一个名为 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