6 minutes
类与对象基础
面向对象编程(Object-Oriented Programming,OOP)是现代编程中最重要的编程范式之一。Python 从设计之初就是一门面向对象的语言——在 Python 中,一切皆对象。本篇将从最基础的概念开始,带你彻底掌握 Python 中的类与对象。
面向对象编程概述
面向对象编程的核心思想是:将数据和操作数据的方法封装在一起,形成"对象"。与面向过程编程(以函数为中心)不同,OOP 以"类"和"对象"为中心。
四大基本特性
- 封装(Encapsulation):将数据和内部实现细节隐藏,对外只暴露必要的接口
- 继承(Inheritance):子类可以复用父类的属性和方法
- 多态(Polymorphism):同一接口在不同对象上表现不同行为
- 抽象(Abstraction):隐藏复杂实现,只暴露简化接口
类的定义与实例化
最简单的类
class Dog:
"""一个简单的 Dog 类"""
pass
# 实例化对象
my_dog = Dog()
print(type(my_dog)) # <class '__main__.Dog'>
print(isinstance(my_dog, Dog)) # True
class 关键字用于定义一个类。类名通常使用 PascalCase 命名规范。pass 表示空语句块,用来占位。
构造函数 __init__
__init__ 是一个特殊方法(魔法方法),在创建对象时自动调用,用于初始化实例属性:
class Dog:
def __init__(self, name: str, age: int):
self.name = name # 实例属性
self.age = age # 实例属性
def bark(self) -> str:
return f"{self.name} says: Woof!"
dog1 = Dog("Buddy", 3)
dog2 = Dog("Lucy", 5)
print(dog1.bark()) # Buddy says: Woof!
print(dog2.bark()) # Lucy says: Woof!
print(dog1.name, dog1.age) # Buddy 3
要点:
__init__的第一个参数必须是self,代表实例本身self是约定名称,传参时不用写,Python 自动传入__init__不能有return语句(除了返回None)
self 参数详解
self 指向实例本身,所有实例方法都必须以 self 作为第一个参数:
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
def get_count(self):
return self.count
c = Counter()
c.increment()
c.increment()
print(c.get_count()) # 2
# self 的等价调用方式
Counter.increment(c) # 手动传实例,效果等同 c.increment()
print(c.get_count()) # 3
实例属性 vs 类属性
class Employee:
# 类属性 —— 所有实例共享
company_name = "TechCorp"
employee_count = 0
def __init__(self, name: str, salary: float):
# 实例属性 —— 每个实例独有
self.name = name
self.salary = salary
Employee.employee_count += 1
e1 = Employee("Alice", 8000)
e2 = Employee("Bob", 9000)
# 访问类属性
print(Employee.company_name) # TechCorp
print(e1.company_name) # TechCorp(实例也能访问)
# 类属性被所有实例共享
print(Employee.employee_count) # 2
# 小心:通过实例修改类属性
e1.company_name = "NewCorp" # 这实际上创建了一个实例属性!
print(e1.company_name) # NewCorp(实例属性覆盖了类属性)
print(e2.company_name) # TechCorp(类属性未受影响)
print(Employee.company_name) # TechCorp(类属性未受影响)
# 正确修改类属性的方式
Employee.company_name = "BigCorp"
print(e2.company_name) # BigCorp
属性的查找顺序
Python 查找属性时遵循 LEGB 规则在类上的变体:
- 实例属性(
instance.__dict__) - 类属性(
class.__dict__) - 父类属性(沿 MRO 链查找)
class Parent:
value = "parent"
class Child(Parent):
pass
obj = Child()
print(obj.value) # parent
obj.value = "instance"
print(obj.value) # instance(实例属性覆盖了类属性)
del obj.value
print(obj.value) # parent(删除了实例属性,回到类属性)
实例方法、类方法和静态方法
Python 类中有三种方法:
class MethodDemo:
class_var = "class var"
def __init__(self, value: str):
self.value = value
# 1. 实例方法 —— 操作实例数据
def instance_method(self):
return f"Instance: {self.value}"
# 2. 类方法 —— 操作类数据
@classmethod
def class_method(cls):
return f"Class: {cls.class_var}"
# 3. 静态方法 —— 工具函数,与类相关但不依赖实例或类
@staticmethod
def static_method(x: int, y: int) -> int:
return x + y
obj = MethodDemo("hello")
# 调用方式对比
print(obj.instance_method()) # Instance: hello
print(MethodDemo.class_method()) # Class: class var
print(obj.class_method()) # Class: class var(实例也能调用类方法)
print(MethodDemo.static_method(3, 5)) # 8
print(obj.static_method(3, 5)) # 8
典型使用场景
class Date:
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
# 实例方法
def display(self) -> str:
return f"{self.year}-{self.month:02d}-{self.day:02d}"
# 类方法 —— 替代构造函数
@classmethod
def from_string(cls, date_str: str) -> "Date":
parts = date_str.split("-")
return cls(int(parts[0]), int(parts[1]), int(parts[2]))
# 静态方法 —— 校验工具
@staticmethod
def is_valid_date(year: int, month: int, day: int) -> bool:
if month < 1 or month > 12:
return False
if day < 1 or day > 31:
return False
return True
d = Date.from_string("2026-05-15")
print(d.display()) # 2026-05-15
print(Date.is_valid_date(2026, 13, 1)) # False
访问控制与命名约定
Python 没有真正的 private / protected 关键字,而是通过命名约定来控制访问:
class AccessDemo:
def __init__(self):
self.public = "任何人都可以访问"
self._protected = "子类可以访问(约定)"
self.__private = "不要直接访问(名称修饰)"
def get_private(self):
return self.__private
obj = AccessDemo()
# 公开属性 —— 自由访问
print(obj.public) # 任何人都可以访问
# 保护属性 —— 约定,但不强制执行
print(obj._protected) # 能访问,但不建议
# 私有属性 —— 名称修饰(name mangling)
# print(obj.__private) # AttributeError!
print(obj.get_private()) # 不要直接访问(名称修饰)
print(obj._AccessDemo__private) # 能访问,但强烈不建议!
名称修饰(Name Mangling)机制
Python 会将 __双下划线开头的属性名 改写为 _ClassName__属性名:
class Parent:
def __init__(self):
self.__secret = "parent secret"
def reveal(self):
return self.__secret
class Child(Parent):
def __init__(self):
super().__init__()
self.__secret = "child secret" # 实际上是 _Child__secret
c = Child()
print(c._Parent__secret) # parent secret(父类的私有属性)
print(c._Child__secret) # child secret(子类的私有属性)
这种机制主要是为了避免子类意外覆盖父类的私有属性。
__str__ 与 __repr__
这两个方法控制对象的字符串表示:
class Book:
def __init__(self, title: str, author: str, pages: int):
self.title = title
self.author = author
self.pages = pages
# 面向开发者的表示 —— 调试用,尽可能包含足够信息
def __repr__(self) -> str:
return f"Book('{self.title}', '{self.author}', {self.pages})"
# 面向用户的表示 —— 可读性强
def __str__(self) -> str:
return f"《{self.title}》by {self.author}"
book = Book("Python从入门到精通", "张三", 500)
print(repr(book)) # Book('Python从入门到精通', '张三', 500)
print(str(book)) # 《Python从入门到精通》by 张三
print(book) # 《Python从入门到精通》by 张三(自动调用 str)
最佳实践:
__repr__应该返回一个可以用来重建对象的表达式字符串__str__返回用户友好的描述- 如果只实现一个,实现
__repr__——Python 在缺少__str__时会回退到__repr__
isinstance 与 issubclass
class Animal:
pass
class Mammal(Animal):
pass
class Dog(Mammal):
pass
dog = Dog()
# isinstance:检查对象是否属于某个类(或父类)
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Mammal)) # True(继承关系)
print(isinstance(dog, Animal)) # True(继承关系)
print(isinstance(dog, (Dog, Cat))) # True(元组中的任意一个匹配即可)
# issubclass:检查类之间的继承关系
print(issubclass(Dog, Mammal)) # True
print(issubclass(Mammal, Animal)) # True
print(issubclass(Dog, Animal)) # True(传递性)
print(issubclass(Dog, Dog)) # True(类是其自身的子类)
对象比较:__eq__
默认情况下,== 比较的是对象的**身份(id)**而非内容:
class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = p1
print(p1 == p2) # False(不同对象,即使内容相同)
print(p1 == p3) # True(同一对象)
print(p1 is p2) # False(身份不同)
通过 __eq__ 自定义相等逻辑:
class Point:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
def __eq__(self, other) -> bool:
if not isinstance(other, Point):
return NotImplemented
return self.x == other.x and self.y == other.y
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)
print(p1 == p2) # True(内容相同)
print(p1 == p3) # False(内容不同)
print(p1 == 42) # False(类型不同,返回 NotImplemented)
Getter/Setter 模式
在简单的场景下,直接访问属性就足够了。当需要控制访问逻辑时,可以用方法:
class Temperature:
def __init__(self, celsius: float = 0):
self._celsius = celsius
# Getter
def get_celsius(self) -> float:
return self._celsius
# Setter(带校验)
def set_celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError(f"温度不能低于绝对零度: {value}")
self._celsius = value
# 计算属性
def get_fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.get_celsius()) # 25
print(t.get_fahrenheit()) # 77.0
t.set_celsius(100)
print(t.get_celsius()) # 100
# t.set_celsius(-500) # ValueError!
在后面的文章中,我们会学习使用
@property装饰器来实现更优雅的 getter/setter。
综合示例:银行账户系统
将上述知识点整合到一个完整的示例中:
class BankAccount:
"""银行账户类"""
# 类属性
bank_name = "Python Bank"
interest_rate = 0.03 # 3% 年利率
_total_accounts = 0
def __init__(self, owner: str, initial_balance: float = 0):
self.owner = owner
self._balance = initial_balance
self._transactions: list[str] = []
BankAccount._total_accounts += 1
self._account_number = f"PB{BankAccount._total_accounts:06d}"
# 实例方法
def deposit(self, amount: float) -> float:
if amount <= 0:
raise ValueError("存款金额必须为正数")
self._balance += amount
self._transactions.append(f"存款: +{amount}")
return self._balance
def withdraw(self, amount: float) -> float:
if amount <= 0:
raise ValueError("取款金额必须为正数")
if amount > self._balance:
raise ValueError("余额不足")
self._balance -= amount
self._transactions.append(f"取款: -{amount}")
return self._balance
def get_balance(self) -> float:
return self._balance
def get_transactions(self) -> list[str]:
return self._transactions.copy() # 返回副本防止外部修改
# 类方法
@classmethod
def create_savings_account(cls, owner: str) -> "BankAccount":
"""创建一个有初始存款的储蓄账户"""
return cls(owner, 1000)
@classmethod
def get_total_accounts(cls) -> int:
return cls._total_accounts
@classmethod
def update_interest_rate(cls, new_rate: float) -> None:
if new_rate < 0 or new_rate > 1:
raise ValueError("利率必须在 0-1 之间")
cls.interest_rate = new_rate
# 静态方法
@staticmethod
def validate_account_number(account_num: str) -> bool:
return account_num.startswith("PB") and len(account_num) == 8
# 字符串表示
def __repr__(self) -> str:
return f"BankAccount('{self.owner}', {self._balance})"
def __str__(self) -> str:
return f"账户[{self._account_number}] {self.owner}: ¥{self._balance:.2f}"
def __eq__(self, other) -> bool:
if not isinstance(other, BankAccount):
return NotImplemented
return self._account_number == other._account_number
# ---- 使用示例 ----
# 创建账户
alice_acc = BankAccount("Alice", 5000)
bob_acc = BankAccount.create_savings_account("Bob")
# 存款取款
alice_acc.deposit(2000)
alice_acc.withdraw(1500)
# 查看信息
print(alice_acc) # 账户[PB000001] Alice: ¥5500.00
print(bob_acc) # 账户[PB000002] Bob: ¥1000.00
# 类属性和类方法
print(BankAccount.bank_name) # Python Bank
print(BankAccount.get_total_accounts()) # 2
# 静态方法
print(BankAccount.validate_account_number("PB000001")) # True
# 比较
print(alice_acc == bob_acc) # False
print(alice_acc == alice_acc) # True
常见陷阱与最佳实践
陷阱 1:可变默认参数
class Bad:
def __init__(self, items=[]): # 危险!
self.items = items
class Good:
def __init__(self, items=None):
self.items = items if items is not None else []
a = Bad()
a.items.append(1)
b = Bad()
print(b.items) # [1] —— 共享了同一个列表!
c = Good()
c.items.append(1)
d = Good()
print(d.items) # [] —— 互不影响
陷阱 2:混淆实例属性和类属性
class Student:
grades = [] # 类属性,所有实例共享
def __init__(self, name: str):
self.name = name
def add_grade(self, grade: int):
self.grades.append(grade) # 修改的是类属性!
s1 = Student("Alice")
s1.add_grade(90)
s2 = Student("Bob")
print(s2.grades) # [90] —— 受到了影响!
正确做法:
class Student:
def __init__(self, name: str):
self.name = name
self.grades: list[int] = [] # 实例属性
def add_grade(self, grade: int):
self.grades.append(grade)
陷阱 3:用 __ 双下划线过于积极
class TooPrivate:
def __init__(self):
self.__value = 42 # 实际很少需要真正的 name mangling
def __do_something(self):
pass
大多数时候,_ 单下划线就足够了。__ 只在需要避免子类覆盖时才使用。
小结
本篇我们系统学习了 Python 面向对象编程的基础知识:
- 类的定义:使用
class关键字定义类,__init__作为构造函数初始化实例 self参数:实例方法的第一个参数,代表实例本身- 实例属性 vs 类属性:实例属性每个对象独有,类属性所有实例共享
- 三种方法:实例方法(操作实例)、类方法(
@classmethod操作类)、静态方法(@staticmethod工具函数) - 访问控制:
_保护属性约定、__名称修饰机制 - 字符串表示:
__repr__面向开发者、__str__面向用户 - 类型检查:
isinstance检查对象类型、issubclass检查继承关系 - 对象比较:通过
__eq__实现基于内容的相等判断
类是 Python 面向对象编程的基石,理解这些概念将为后续的继承、多态、魔法方法等内容做好充分准备。
Summary: 类与对象基础:定义、属性、方法、封装