Python面向对象编程:从类与对象基础到封装继承多态实战
2026/8/17 9:52:04 网站建设 项目流程

1. 从“蓝图”到“实体”:理解Python类与对象的本质

如果你刚开始学Python,可能觉得“类”和“对象”这两个词有点抽象,像是编程里的黑话。但说穿了,它们就是用来组织代码、模拟现实世界的一种超级工具。你可以把类(Class)想象成一张设计蓝图,而对象(Object)就是根据这张蓝图建造出来的、实实在在的房子

蓝图(类)定义了房子要有几间卧室、几个卫生间、门窗怎么开。但蓝图本身不能住人,它只是一套规则。只有根据蓝图建好的那栋具体的房子(对象),你才能进去装修、居住。在Python里,我们通过定义类来创建一种新的数据类型,然后用这个类去“实例化”(也就是建造)出一个个独立的对象。每个对象都拥有类里定义的属性和功能,但各自的数据可以完全不同。比如,我们定义一个“学生”类,那么“张三”和“李四”就是两个不同的学生对象,他们都有姓名、学号这些属性,但具体的值各不相同。

为什么这如此重要?因为一旦你掌握了类和对象,你的代码就能从一堆零散的函数和变量,升级为结构清晰、易于管理和扩展的“乐高积木”。无论是开发一个Web应用、进行数据分析,还是写一个小工具,面向对象编程(OOP)的思想都能让你事半功倍。接下来,我们就抛开那些枯燥的定义,直接动手,看看怎么把这张“蓝图”画出来,并用它盖起“高楼大厦”。

2. 类的定义与对象的创建:从零开始搭建

万事开头难,但定义第一个类其实非常简单。在Python中,我们使用class关键字来声明一个类。

2.1 定义一个最简单的类

我们从一个最基础的例子开始:创建一个表示“狗”的类。

class Dog: pass

是的,你没看错,两行代码就定义了一个类。pass是一个占位符,表示“这里暂时什么都不做”。现在,Dog就是一个空壳类,但它已经具备了被实例化的能力。

2.2 创建你的第一个对象

有了蓝图,就可以盖房子了。创建对象的过程叫做“实例化”,语法是使用类名后跟一对圆括号,看起来就像调用一个函数。

my_dog = Dog() # 创建了一个Dog类的对象,并赋值给变量my_dog your_dog = Dog() # 创建了另一个独立的Dog对象

这里,my_dogyour_dog就是两个不同的对象。虽然它们现在内部都是空的(因为类定义里只有pass),但它们是两个独立的存在。你可以用type()函数来验证它们的类型:

print(type(my_dog)) # 输出:<class '__main__.Dog'> print(my_dog is your_dog) # 输出:False,这是两个不同的对象

2.3 初始化对象:__init__方法

一个空的狗对象没什么用。我们通常希望在创建对象的同时,就给它赋予一些初始特征,比如名字和年龄。这就需要用到类中一个非常特殊的方法:__init__()。你可以把它理解为对象的“构造函数”或“初始化方法”。

class Dog: def __init__(self, name, age): self.name = name # 为这个对象创建一个属性叫name,赋值为传入的name参数 self.age = age # 为这个对象创建一个属性叫age,赋值为传入的age参数 print(f"一只名叫{name}的小狗诞生了!")

这里有几个关键点:

  1. self参数:这是Python类方法的第一个参数,而且必须要有。它代表对象实例本身。当我们调用my_dog = Dog(“Buddy”, 2)时,self指的就是my_dog这个即将被创建出来的对象。
  2. 属性赋值self.name = name这行代码的作用是,给当前这个对象(self)绑定一个名为name的属性,并把外部传入的name参数值赋给它。这样,每个狗对象就拥有了自己独立的nameage

现在,我们可以创建有血有肉的狗对象了:

dog1 = Dog("Buddy", 2) # 输出:一只名叫Buddy的小狗诞生了! dog2 = Dog("Lucy", 5) # 输出:一只名叫Lucy的小狗诞生了! print(dog1.name, dog1.age) # 输出:Buddy 2 print(dog2.name, dog2.age) # 输出:Lucy 5

注意__init__方法并不是必须的。如果你不需要在创建对象时进行任何初始化操作,可以省略它,Python会调用一个默认的。但实践中,几乎所有的类都会定义__init__来设置初始状态。

2.4 实例属性与类属性:分清“个人的”和“公共的”

上面例子中的nameage,我们是通过self.属性名来定义的,这叫做实例属性。它属于单个对象,不同对象的实例属性值互不影响。

与之相对的是类属性,它属于类本身,所有由这个类创建的对象都共享同一个类属性。类属性直接在类内部、方法外部定义。

class Dog: species = "Canis familiaris" # 类属性,所有狗都属于这个物种 def __init__(self, name, age): self.name = name # 实例属性 self.age = age # 实例属性 dog1 = Dog("Buddy", 2) dog2 = Dog("Lucy", 5) print(dog1.species) # 输出:Canis familiaris print(dog2.species) # 输出:Canis familiaris print(Dog.species) # 输出:Canis familiaris,通过类名也能访问 # 修改类属性会影响所有实例和类本身 Dog.species = "Canis lupus familiaris" print(dog1.species) # 输出:Canis lupus familiaris print(dog2.species) # 输出:Canis lupus familiaris

实操心得:类属性通常用于定义该类所有对象共有的常量或默认值。一个常见的坑是,如果类属性是可变对象(如列表、字典),通过一个实例修改它,会影响所有其他实例。例如,Dog.toys = [],如果dog1.toys.append(“ball”),那么dog2.toys也会看到这个球。为了避免这种混淆,除非确有必要,否则建议将可变状态定义为实例属性。

3. 让对象“动”起来:实例方法详解

属性让对象有了状态(是什么),而方法让对象有了行为(能做什么)。在类中定义的函数,我们称之为方法。最常见的就是实例方法,它的第一个参数永远是self

3.1 定义与调用实例方法

让我们给Dog类增加一些行为:

class Dog: def __init__(self, name, age): self.name = name self.age = age def bark(self): """狗叫的方法""" print(f"{self.name} says: Woof! Woof!") def have_birthday(self): """过生日,年龄加一""" self.age += 1 print(f"Happy Birthday, {self.name}! Now you are {self.age} years old.") # 创建对象并调用方法 my_dog = Dog("Buddy", 2) my_dog.bark() # 输出:Buddy says: Woof! Woof! my_dog.have_birthday() # 输出:Happy Birthday, Buddy! Now you are 3 years old. print(my_dog.age) # 输出:3

调用实例方法时,Python会自动将调用该方法的对象(my_dog)作为第一个参数self传入。所以my_dog.bark()等价于Dog.bark(my_dog)。这种语法糖让代码读起来更自然:“我的狗,叫一下”。

3.2 特殊方法(魔术方法):赋予对象“超能力”

__init__就是一种特殊方法,它以双下划线开头和结尾。Python提供了很多这样的魔术方法,用于实现或改变对象的某些内置行为。理解它们能极大提升你对Python对象的掌控力。

__str____repr__:对象的字符串表示

  • __str__:当你使用print(obj)str(obj)时调用,返回一个对用户友好的、可读的字符串描述。
  • __repr__:当你直接在交互式环境输入对象名,或使用repr(obj)时调用,目标是返回一个明确的、无歧义的字符串,理想情况下能用来重新创建这个对象。
class Dog: def __init__(self, name, age): self.name = name self.age = age def __str__(self): return f"A dog named {self.name}, {self.age} years old." def __repr__(self): return f"Dog('{self.name}', {self.age})" my_dog = Dog("Buddy", 3) print(my_dog) # 输出:A dog named Buddy, 3 years old. (调用 __str__) print(repr(my_dog)) # 输出:Dog('Buddy', 3) (调用 __repr__) # 在交互环境中直接输入 my_dog,会显示:Dog('Buddy', 3)

__len__:定义对象的长度如果你的对象有“长度”的概念(比如一个自定义的集合),可以定义__len__方法。

class Pack: def __init__(self): self.dogs = [] def add_dog(self, dog): self.dogs.append(dog) def __len__(self): return len(self.dogs) pack = Pack() pack.add_dog(Dog("Buddy", 2)) pack.add_dog(Dog("Lucy", 5)) print(len(pack)) # 输出:2,这里调用了 pack.__len__()

__add__:实现对象的加法让你自定义的对象支持+运算符。

class Dog: def __init__(self, name, age, friendliness=0): self.name = name self.age = age self.friendliness = friendliness # 友好度,0-10 def __add__(self, other_dog): """两只狗在一起,产生一个‘玩耍能量’值""" return self.friendliness + other_dog.friendliness dog1 = Dog("Buddy", 2, 8) dog2 = Dog("Lucy", 5, 6) play_energy = dog1 + dog2 # 等价于 dog1.__add__(dog2) print(f"Play energy between {dog1.name} and {dog2.name}: {play_energy}") # 输出:14

注意事项:魔术方法非常强大,但不要滥用。只在确实需要改变对象的内置行为时才使用它们。例如,为一个“银行账户”类定义__add__来合并余额可能是合理的,但为一个“用户”类定义__add__可能就令人困惑了。

4. 面向对象三大支柱:封装、继承与多态

这是面向对象编程的核心思想,理解了它们,你才算真正入门。

4.1 封装:隐藏内部细节,提供公共接口

封装的核心思想是将数据(属性)和操作数据的方法捆绑在一起,并对外部隐藏对象内部状态的实现细节。在Python中,我们主要通过“命名约定”来实现一种非强制的封装。

  • 公开属性:像self.name这样,可以直接从外部访问和修改。
  • 受保护属性:以一个下划线开头,如self._internal_data。这是一种约定,告诉其他程序员“这是一个内部属性,请不要在类外部直接访问它”,但Python并不会阻止你访问。
  • 私有属性:以两个下划线开头,如self.__secret。Python会对其进行“名称改写”,使得在类外部难以直接访问,主要用于避免子类意外重写父类属性。
class BankAccount: def __init__(self, owner, balance): self.owner = owner # 公开属性 self._account_number = “123456” # 受保护属性(约定) self.__balance = balance # 私有属性 def deposit(self, amount): if amount > 0: self.__balance += amount print(f"Deposited {amount}. New balance: {self.__balance}") else: print(“Invalid deposit amount.”) def get_balance(self): # 提供公共方法来访问私有属性 return self.__balance account = BankAccount(“Alice”, 1000) print(account.owner) # 可以访问:Alice print(account._account_number) # 可以访问(但不建议):123456 # print(account.__balance) # 直接访问会报错:AttributeError print(account.get_balance()) # 正确方式:通过公共接口,输出:1000 account.deposit(500) # 通过方法修改状态,输出:Deposited 500. New balance: 1500

封装的好处是提高了代码的安全性和可维护性。你可以自由修改内部实现(比如将__balance的存储方式从变量改为数据库查询),只要不改变公共方法(如deposit,get_balance)的对外行为,调用这些方法的代码就完全不需要修改。

4.2 继承:代码复用与层次化设计

继承允许我们定义一个类(子类)来继承另一个类(父类或超类)的属性和方法。子类可以扩展修改父类的行为。这实现了代码的复用和逻辑的层次化。

# 父类(基类) class Animal: def __init__(self, name): self.name = name def speak(self): raise NotImplementedError(“Subclass must implement abstract method”) # 抽象方法 # 子类(派生类) class Dog(Animal): # Dog类继承自Animal类 def __init__(self, name, breed): super().__init__(name) # 调用父类的__init__方法初始化name属性 self.breed = breed # 子类特有的属性 def speak(self): # 重写父类的抽象方法 return f“{self.name} says: Woof!” class Cat(Animal): def __init__(self, name, is_indoor): super().__init__(name) self.is_indoor = is_indoor def speak(self): # 重写父类的抽象方法 return f“{self.name} says: Meow!” # 使用 animals = [Dog(“Buddy”, “Golden Retriever”), Cat(“Whiskers”, True)] for animal in animals: print(animal.speak()) # 输出: # Buddy says: Woof! # Whiskers says: Meow!

关键点解析:

  • super():这是一个内置函数,用于调用父类(超类)的方法。在子类的__init__中调用super().__init__(name)是非常标准的做法,确保父类的初始化逻辑得以执行。
  • 方法重写:子类可以定义与父类同名的方法,这样就会覆盖父类的方法。这是实现多态的基础。
  • 抽象基类:像上面Animal中的speak方法,它只是抛出一个错误,并不实现具体功能。这种设计的目的是定义一个接口,强制要求所有子类必须实现speak方法。Python中更正式的做法是使用abc模块。

4.3 多态:同一接口,不同实现

多态指的是不同类型的对象可以对同一方法调用做出不同的响应。在上面的例子中,animal.speak()这行代码就是多态的体现。animal变量在运行时可能是Dog对象,也可能是Cat对象,但调用speak()方法时,Python会自动找到该对象实际类型(DogCat)中定义的方法来执行。

多态的好处是提高了代码的通用性和灵活性。我们写一个处理Animal列表的函数,不需要关心列表里具体是狗还是猫,只要它们都有speak()方法就行。未来即使新增一个Bird类,这个函数也完全不需要修改。

def animal_concert(animals_list): for animal in animals_list: print(animal.speak()) # 这里就是多态调用 # 这个函数可以处理任何实现了 `speak` 方法的“动物”对象

5. 类方法与静态方法:不依赖于实例的操作

我们之前讲的都是实例方法,它们操作的是具体的对象实例(self)。但有时,我们需要一些与类相关,但与任何特定实例无关的操作。这时就需要类方法和静态方法。

5.1 类方法(@classmethod)

类方法的第一个参数是cls,代表类本身,而不是实例。它通常用于:

  1. 作为替代构造函数,提供不同的方式来创建对象。
  2. 操作类属性。

定义类方法需要使用@classmethod装饰器。

class Dog: species = “Canis familiaris” all_dogs = [] # 类属性,记录所有创建过的狗 def __init__(self, name, age): self.name = name self.age = age Dog.all_dogs.append(self) # 每创建一个实例,就添加到列表中 @classmethod def get_population(cls): """类方法:获取创建的狗的总数""" return len(cls.all_dogs) @classmethod def create_from_birth_year(cls, name, birth_year): """替代构造函数:通过出生年份创建狗""" from datetime import datetime current_year = datetime.now().year age = current_year - birth_year return cls(name, age) # 注意这里调用的是 cls(name, age),而不是 Dog(...) # 使用类方法 dog1 = Dog(“Buddy”, 2) dog2 = Dog.create_from_birth_year(“Lucy”, 2019) # 使用类方法创建对象 print(Dog.get_population()) # 输出:2 print(dog2.age) # 输出:根据当前年份计算的年龄

create_from_birth_year方法接收cls参数,并最终调用cls(name, age)来创建新实例。这意味着即使Dog类被继承,子类调用这个类方法时,cls会是子类,创建出来的也是子类的实例,这非常灵活。

5.2 静态方法(@staticmethod)

静态方法既不需要self参数,也不需要cls参数。它就像一个定义在类命名空间里的普通函数,与类和实例都没有强制性的绑定关系。它用于组织代码,将逻辑上属于这个类的功能放在一起。

定义静态方法需要使用@staticmethod装饰器。

class StringUtils: @staticmethod def is_palindrome(text): """静态方法:判断字符串是否是回文""" cleaned_text = ”.join(char.lower() for char in text if char.isalnum()) return cleaned_text == cleaned_text[::-1] @staticmethod def count_vowels(text): """静态方法:统计元音字母数量""" vowels = “aeiouAEIOU” return sum(1 for char in text if char in vowels) # 使用静态方法,无需创建实例 print(StringUtils.is_palindrome(“A man, a plan, a canal: Panama”)) # 输出:True print(StringUtils.count_vowels(“Hello World”)) # 输出:3 # 也可以通过实例调用(但不常见) util = StringUtils() print(util.is_palindrome(“racecar”)) # 输出:True

实操心得:如何选择?

  • 如果你的方法需要访问或修改实例的状态,用实例方法
  • 如果你的方法需要访问或修改类的状态,或者作为替代构造函数,用类方法
  • 如果你的方法只是一个工具函数,逻辑上属于这个类,但既不操作实例也不操作类,用静态方法。静态方法让代码结构更清晰,也便于测试。

6. 属性装饰器:精细化控制属性访问

有时候,我们希望对属性的访问和修改进行更精细的控制,比如在赋值时进行验证,或者将某个方法“伪装”成属性来访问。Python的@property装饰器家族就是干这个的。

6.1 @property:将方法变为“只读属性”

假设我们有一个Circle类,有半径radius属性。我们想方便地获取它的面积,但面积不应该被直接设置(它应由半径计算得出)。这时可以用@property

class Circle: def __init__(self, radius): self.radius = radius @property def area(self): """将area方法作为属性访问,计算圆的面积""" return 3.14159 * self.radius ** 2 @property def diameter(self): """直径也是只读的,由半径决定""" return self.radius * 2 circle = Circle(5) print(circle.radius) # 输出:5 print(circle.area) # 输出:78.53975 (注意是属性访问,不是方法调用 circle.area()) print(circle.diameter) # 输出:10 # circle.area = 100 # 这行会报错:AttributeError: can‘t set attribute

现在area看起来就像一个普通的属性,但实际上是调用了一个方法。这提供了更好的封装性和接口一致性。

6.2 @属性名.setter:控制属性赋值

如果我们想在给属性赋值时进行验证或触发其他操作,可以定义 setter 方法。

class Circle: def __init__(self, radius): self._radius = radius # 使用“受保护”的属性名存储实际值 @property def radius(self): """获取半径""" return self._radius @radius.setter def radius(self, value): """设置半径,并确保其为正数""" if value <= 0: raise ValueError(“Radius must be positive.”) self._radius = value # 可以在这里触发其他操作,比如更新UI、记录日志等 print(f“Radius updated to {value}”) @property def area(self): return 3.14159 * self._radius ** 2 circle = Circle(5) print(circle.area) # 输出:78.53975 circle.radius = 10 # 输出:Radius updated to 10 (触发了setter) print(circle.area) # 输出:314.159 # circle.radius = -1 # 会抛出 ValueError: Radius must be positive.

通过这种方式,我们对外暴露了一个简洁的radius属性,但内部却实现了复杂的控制和验证逻辑。外部代码像使用普通属性一样使用它,完全感知不到背后的细节。

6.3 @属性名.deleter:控制属性删除

类似地,可以定义 deleter 方法来控制删除属性时的行为(比如清理资源)。

class ManagedFile: def __init__(self, filename): self.filename = filename self._file = open(filename, ‘r’) # 假设打开一个文件 @property def content(self): self._file.seek(0) return self._file.read() @content.deleter def content(self): print(“Closing the file...“) self._file.close() self._file = None file_obj = ManagedFile(“example.txt”) print(file_obj.content[:100]) # 读取文件前100个字符 del file_obj.content # 输出:Closing the file... (触发了deleter,关闭文件) # 之后再次访问 file_obj.content 可能会出错,因为文件已关闭

注意事项:属性装饰器非常强大,但不要过度使用。对于简单的、不需要额外逻辑的属性,直接使用公开的实例属性是最清晰、最Pythonic的做法。只有当你需要在获取、设置或删除属性时加入特定逻辑时,才考虑使用@property及其 setter/deleter。

7. 高级主题与实战技巧

掌握了基础之后,我们来看一些更深入的概念和实际编码中会遇到的技巧。

7.1 组合与继承的选择:“有一个” vs “是一个”

继承代表“是一个”的关系(Dog是一个Animal)。但面向对象设计中,组合(“有一个”的关系)往往比继承更灵活、更推荐。组合是指一个类将其他类的对象作为自己的属性。

何时用继承?当子类确实是父类的一种特殊类型,并且需要重用或扩展父类的大部分功能时。何时用组合?当你想利用其他类的功能,但并不是其子类型时。

# 使用继承(是一个) class Engine: def start(self): return “Engine started.” class Car(Engine): # 不自然:Car 是一个 Engine? pass # 使用组合(有一个) class Engine: def start(self): return “Engine started.” class Car: def __init__(self): self.engine = Engine() # Car 有一个 Engine def start(self): return self.engine.start() + ” Car is ready to go.” my_car = Car() print(my_car.start()) # 输出:Engine started. Car is ready to go.

组合降低了类之间的耦合度。你可以轻易地更换Car的引擎(比如换成ElectricEngine),而不需要修改Car类的继承结构。遵循“组合优于继承”的原则,能让你的代码更模块化、更易维护。

7.2 多重继承与方法解析顺序(MRO)

Python支持一个类继承自多个父类,这就是多重继承。这带来了强大的灵活性,但也引入了“菱形继承”等复杂性。Python使用C3线性化算法来确定方法解析顺序(MRO),即当调用一个方法时,Python按照什么顺序在继承链中查找。

class A: def do_something(self): print(“Method defined in A”) class B(A): def do_something(self): print(“Method defined in B”) super().do_something() # 调用父类A的方法 class C(A): def do_something(self): print(“Method defined in C”) super().do_something() # 调用父类A的方法 class D(B, C): def do_something(self): print(“Method defined in D”) super().do_something() # 根据MRO调用下一个类的方法 d = D() d.do_something() print(D.__mro__) # 查看D类的MRO

输出可能是:

Method defined in D Method defined in B Method defined in C Method defined in A (<class ‘__main__.D’>, <class ‘__main__.B’>, <class ‘__main__.C’>, <class ‘__main__.A’>, <class ‘object’>)

MRO顺序是 D -> B -> C -> A。super()并不是简单地调用“父类”,而是按照 MRO 调用“下一个类”。理解 MRO 对于设计和调试复杂的多重继承结构至关重要。你可以使用类名.__mro__类名.mro()来查看MRO。

7.3 使用__slots__优化内存

默认情况下,Python对象使用一个字典(__dict__)来存储其属性。这非常灵活,允许动态添加新属性,但也会消耗较多内存。如果你要创建大量(成千上万)的某个类的实例,并且它们的属性是固定的,可以使用__slots__来显著减少内存占用。

__slots__告诉Python为实例使用一个更紧凑的数组结构来存储属性,而不是字典。

class PointWithSlots: __slots__ = (‘x’, ‘y’) # 明确声明只有 x 和 y 两个属性 def __init__(self, x, y): self.x = x self.y = y class PointWithoutSlots: def __init__(self, x, y): self.x = x self.y = y # 测试内存占用 import sys p1 = PointWithSlots(1, 2) p2 = PointWithoutSlots(1, 2) print(sys.getsizeof(p1) + sys.getsizeof(p1.__slots__)) # 通常更小 print(sys.getsizeof(p2) + sys.getsizeof(p2.__dict__)) # 通常更大 # p1.z = 3 # 这行会报错:AttributeError: ‘PointWithSlots’ object has no attribute ‘z’ # p2.z = 3 # 这行是允许的

注意事项:使用__slots__的代价是失去了动态添加属性的能力。同时,如果类需要被继承,且子类也需要__slots__,则子类的__slots__只包含自己新增的属性。除非你确实面临内存瓶颈,并且实例属性结构稳定,否则一般不需要使用__slots__

7.4 枚举类(Enum):定义一组命名的常量

当你需要定义一组相关的、有限的常量时,比如星期几、状态码、颜色等,使用enum模块的Enum类比直接使用数字或字符串常量更清晰、更安全。

from enum import Enum, auto class Color(Enum): RED = 1 GREEN = 2 BLUE = 3 # 或者使用 auto() 自动赋值 # RED = auto() # GREEN = auto() # BLUE = auto() class Status(Enum): PENDING = “pending” PROCESSING = “processing” COMPLETED = “completed” # 使用 fav_color = Color.RED print(fav_color) # 输出:Color.RED print(fav_color.value) # 输出:1 print(fav_color.name) # 输出:RED if Status(request.status) == Status.PENDING: # 确保状态值有效 # do something pass # 遍历所有枚举成员 for color in Color: print(color.name, color.value)

枚举成员是单例的,不能重复创建。它们保证了值的唯一性和可读性,避免了魔法数字和字符串在代码中散落的问题。

8. 常见问题与避坑指南

在实际使用类和对象时,总会遇到一些坑。这里记录了几个最常见的问题和我的解决思路。

8.1 可变对象作为类属性的陷阱

这是一个经典陷阱。如果你将一个可变对象(如列表、字典)作为类属性,那么所有实例共享的是同一个对象。

class ProblematicClass: shared_list = [] # 类属性,可变对象 def add_item(self, item): self.shared_list.append(item) obj1 = ProblematicClass() obj2 = ProblematicClass() obj1.add_item(“A”) obj2.add_item(“B”) print(obj1.shared_list) # 输出:[‘A’, ‘B’] print(obj2.shared_list) # 输出:[‘A’, ‘B’] print(ProblematicClass.shared_list) # 输出:[‘A’, ‘B’]

解决方案:永远不要在类属性中初始化可变对象。应该在__init__方法中将可变对象初始化为实例属性。

class CorrectClass: def __init__(self): self.instance_list = [] # 实例属性,每个对象独立 def add_item(self, item): self.instance_list.append(item) obj1 = CorrectClass() obj2 = CorrectClass() obj1.add_item(“A”) obj2.add_item(“B”) print(obj1.instance_list) # 输出:[‘A’] print(obj2.instance_list) # 输出:[‘B’]

8.2 在方法中忘记使用self

新手常犯的错误是在实例方法内部引用其他方法或属性时,忘记了self

class MyClass: def method_a(self): print(“Method A”) def method_b(self): method_a() # 错误!这会被当作一个局部函数或全局函数,而不是实例方法 # 正确写法 class MyClass: def method_a(self): print(“Method A”) def method_b(self): self.method_a() # 正确:通过 self 调用实例方法

8.3 混淆==is运算符

  • ==比较的是两个对象的是否相等(通过调用__eq__方法)。
  • is比较的是两个对象的身份标识(内存地址)是否相同,即是否是同一个对象。
list1 = [1, 2, 3] list2 = [1, 2, 3] list3 = list1 print(list1 == list2) # True,值相同 print(list1 is list2) # False,不是同一个对象 print(list1 is list3) # True,list3是list1的引用,是同一个对象 # 对于小整数和短字符串,Python会缓存,可能导致 is 判断为True,但不要依赖这种行为。 a = 256 b = 256 print(a is b) # 在大多数Python实现中为 True (小整数缓存) c = 257 d = 257 print(c is d) # 通常为 False

在自定义类中,默认的==比较行为和is一样,比较的是内存地址。如果你需要根据内容判断相等,需要重写__eq__方法。

8.4 如何优雅地实现对象比较

除了__eq__(等于),你还可以重写其他比较魔术方法,如__lt__(小于)、__le__(小于等于)、__gt__(大于)、__ge__(大于等于)。Python 的functools.total_ordering装饰器可以帮你省事:只要你定义了__eq__和其中一个比较方法(如__lt__),它就能自动帮你生成其他比较方法。

from functools import total_ordering @total_ordering class Student: def __init__(self, name, grade): self.name = name self.grade = grade def __eq__(self, other): return self.grade == other.grade def __lt__(self, other): return self.grade < other.grade def __repr__(self): return f“Student({self.name}, {self.grade})” alice = Student(“Alice”, 85) bob = Student(“Bob”, 90) print(alice == bob) # False print(alice < bob) # True print(alice <= bob) # True (total_ordering 自动生成) print(alice > bob) # False (total_ordering 自动生成)

8.5 使用dataclasses简化数据类创建

如果你有一个类主要用来存储数据(比如配置、DTO、数据库记录),Python 3.7+ 的dataclasses模块可以极大减少样板代码。它会自动为你生成__init____repr____eq__等方法。

from dataclasses import dataclass, field from typing import List @dataclass(order=True) # order=True 会自动生成比较方法 class InventoryItem: """代表库存物品的类""" name: str unit_price: float quantity_on_hand: int = 0 # 可以设置默认值 tags: List[str] = field(default_factory=list) # 对于可变默认值,使用field def total_cost(self) -> float: return self.unit_price * self.quantity_on_hand # 自动生成了 __init__, __repr__, __eq__ 等方法 item1 = InventoryItem(“Screwdriver”, 9.99, 10) item2 = InventoryItem(“Hammer”, 19.99, 5) print(item1) # 输出:InventoryItem(name=‘Screwdriver’, unit_price=9.99, quantity_on_hand=10, tags=[]) print(item1 == item2) # False print(item1.total_cost()) # 99.9 # 因为 order=True,还可以比较 print(item1 < item2) # 比较基于字段定义的顺序,这里是 name (‘Screwdriver’ < ‘Hammer’? 按字母序)

dataclasses让创建清晰、简洁的数据容器变得异常简单,是现代Python代码中的常用工具。

理解并熟练运用Python的类和对象,是你从脚本编写者迈向软件工程师的关键一步。它不仅仅是语法,更是一种组织代码、抽象现实的思维方式。从今天起,尝试在你下一个项目里,有意识地将相关数据和功能封装成类,你会立刻感受到代码结构清晰带来的好处。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询