Python 面向对象
Python 从设计之初就是一门面向对象的语言,创建一个类和对象非常容易。
一、核心概念
先搞清楚几个术语的含义:
- 类(Class):用来描述具有相同属性和方法的对象的集合。比如"汽车"就是一个类
- 对象(Object):类的具体实例。比如"我家那辆红色的 Model 3"就是一个对象
- 属性(Attribute):对象的特征,比如汽车的颜色、品牌
- 方法(Method):类中定义的函数,描述对象能做什么,比如汽车能"启动"、能"刹车"
- 实例化(Instantiate):从类创建一个具体对象的过程
二、类的定义与实例化
class MyClass:
"""一个简单的类"""
i = 12345 # 类变量(所有实例共享)
def f(self):
return 'hello world'
x = MyClass() # 实例化
print(x.i) # 12345 — 通过对象访问类变量
print(x.f()) # hello world — 调用方法
类对象支持两种操作:
- 属性引用:
obj.name,访问类中定义的属性和方法
- 实例化:
MyClass(),创建一个类的实例
三、init 构造方法
很多类都希望在创建对象时带上初始状态,所以会定义 __init__() 方法。实例化时会自动调用它。
class Complex:
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
x = Complex(3.0, -4.5)
print(x.r, x.i) # 3.0 -4.5
参数通过实例化操作传入:Complex(3.0, -4.5) 等价于调用 __init__(x, 3.0, -4.5),其中 x 是自动创建的实例。
__init__() 不能有返回值,返回非 None 值会报 TypeError。
四、self 参数
类的方法与普通函数的唯一区别:第一个参数必须是 self,代表类的实例本身。
class Test:
def prt(self):
print(self) # <__main__.Test object at 0x...>
print(self.__class__) # <class '__main__.Test'>
t = Test()
t.prt()
self 代表当前对象的地址,不是类本身
self 不是关键字,换成别的名字也能跑,但约定用 self
class Test:
def prt(runoob): # 用 runoob 代替 self,也能正常执行
print(runoob)
print(runoob.__class__)
五、类变量 vs 实例变量
class people:
name = '' # 类变量 — 类中函数体之外定义,所有实例共享
age = 0
def __init__(self, n, a):
self.name = n # 实例变量 — 用 self.xxx 定义,每个实例独有
self.age = a
p1 = people('Alice', 25)
p2 = people('Bob', 30)
print(p1.name) # Alice
print(p2.name) # Bob
关键区别: 类变量是"共享"的,实例变量是"各自独立"的。当实例变量和类变量同名时,实例变量会"遮蔽"类变量,但类变量本身不会被修改:
class Test:
x = 10 # 类变量
t1 = Test()
t2 = Test()
t1.x = 20 # 这是给 t1 创建了一个实例变量 x,遮蔽了类变量
print(t1.x) # 20(实例变量)
print(t2.x) # 10(还是类变量,没被影响)
print(Test.x) # 10
六、私有属性和私有方法
两个下划线 __ 开头的属性或方法为私有,外部不能直接访问。
6.1 私有属性
class people:
def __init__(self, n, a, w):
self.name = n # 公有
self.age = a # 公有
self.__weight = w # 私有 — 外部无法直接访问
def speak(self):
# 类内部可以正常访问私有属性
print(f"{self.name} 说: 我 {self.age} 岁,体重 {self.__weight} 斤")
p = people('Tom', 10, 30)
p.speak() # Tom 说: 我 10 岁,体重 30 斤
# p.__weight # ❌ AttributeError
6.2 私有方法
class Site:
def __init__(self, name, url):
self.name = name
self.__url = url # 私有属性
def __foo(self): # 私有方法
print('这是私有方法')
def foo(self): # 公共方法 — 通过它间接调用私有方法
print('这是公共方法')
self.__foo()
x = Site('菜鸟教程', 'www.runoob.com')
x.foo() # ✅ 正常输出
# x.__foo() # ❌ AttributeError
6.3 名称修饰(Name Mangling)
Python 并不是真正禁止访问私有属性,只是做了一个名称重命名。__weight 实际上被改名为 _类名__weight:
class people:
def __init__(self):
self.__weight = 30
p = people()
# print(p.__weight) # ❌ AttributeError
print(p._people__weight) # ✅ 30(不推荐,但技术上可以访问)
所以 Python 的私有是"约定性"的,不是强制的。通过 dir() 可以看到重命名后的属性名。
七、方法重写(Override)
如果父类的方法不能满足需求,可以在子类中重新定义同名方法:
class Parent:
def myMethod(self):
print('调用父类方法')
class Child(Parent):
def myMethod(self):
print('调用子类方法')
c = Child()
c.myMethod() # 调用子类方法
用 super() 可以在子类中调用父类被覆盖的方法:
class Child(Parent):
def myMethod(self):
print('调用子类方法')
super(Child, self).myMethod() # 调用父类方法
c = Child()
c.myMethod()
# 调用子类方法
# 调用父类方法
八、继承
继承是面向对象的核心特性之一,子类可以复用父类的属性和方法。
8.1 单继承
class people:
def __init__(self, n, a, w):
self.name = n
self.age = a
self.__weight = w
def speak(self):
print(f"{self.name} 说: 我 {self.age} 岁。")
class student(people): # student 继承 people
def __init__(self, n, a, w, g):
people.__init__(self, n, a, w) # 调用父类的构造方法
self.grade = g
def speak(self): # 重写父类方法
print(f"{self.name} 说: 我 {self.age} 岁了,我在读 {self.grade} 年级")
s = student('ken', 10, 60, 3)
s.speak() # ken 说: 我 10 岁了,我在读 3 年级
除了 people.__init__(self, n, a, w) 这种写法,更推荐用 super():
class student(people):
def __init__(self, n, a, w, g):
super().__init__(n, a, w) # 推荐写法,自动传递 self
self.grade = g
8.2 多继承
Python 支持多继承,即一个子类可以同时继承多个父类:
class speaker():
def __init__(self, n, t):
self.name = n
self.topic = t
def speak(self):
print(f"我叫 {self.name},我演讲的主题是 {self.topic}")
class sample(speaker, student): # 同时继承 speaker 和 student
def __init__(self, n, a, w, g, t):
student.__init__(self, n, a, w, g)
speaker.__init__(self, n, t)
test = sample("Tim", 25, 80, 4, "Python")
test.speak()
# 我叫 Tim,我演讲的主题是 Python
多继承的坑: 如果多个父类有同名方法,Python 从左到右搜索,找到第一个就用。上面 sample(speaker, student) 中 speak 调用的是 speaker 的版本,因为 speaker 排在前面。所以父类的顺序很重要。
8.3 从模块导入基类
基类可以定义在另一个模块中:
from module_name import BaseClassName
class DerivedClassName(BaseClassName):
pass
九、类方法、静态方法、实例方法
这是面试常考的知识点,三种方法的核心区别在于参数和调用方式:
class Test:
def instance_fun(self): # 实例方法 — 第一个参数是 self
print("实例方法", self)
@classmethod
def class_fun(cls): # 类方法 — 第一个参数是 cls(类本身)
print("类方法", cls)
@staticmethod
def static_fun(): # 静态方法 — 没有隐含参数
print("静态方法")
t = Test()
# 实例方法:只能通过实例调用(通过类调用需要手动传实例)
t.instance_fun() # ✅ 输出实例方法,打印对象地址
# Test.instance_fun() # ❌ TypeError
# 类方法:可以通过类或实例调用
Test.class_fun() # ✅ 输出 <class '__main__.Test'>
t.class_fun() # ✅ 同样输出 <class '__main__.Test'>
# 静态方法:可以通过类或实例调用
Test.static_fun() # ✅ 输出静态方法
t.static_fun() # ✅ 同样输出静态方法
类方法的典型用途:工厂方法,用来替代 __init__ 构造函数:
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def from_string(cls, date_str):
year, month, day = map(int, date_str.split('-'))
return cls(year, month, day) # 等同于 Date(year, month, day)
d = Date.from_string("2025-12-25") # 不需要手动写 Date(2025, 12, 25)
print(d.year) # 2025
十、@property 属性装饰器
@property 让你把方法调用变成属性访问,同时还能控制读写权限:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self): # getter — 读取时调用
return self._radius
@radius.setter
def radius(self, value): # setter — 赋值时调用
if value < 0:
raise ValueError("半径不能为负数")
self._radius = value
@property
def area(self): # 只有 getter 没有 setter → 只读属性
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5 — 像属性一样访问,不用加括号
print(c.area) # 78.54 — 只读,不能赋值
c.radius = 10 # 通过 setter 赋值,会触发校验
print(c.area) # 314.16
# c.radius = -1 # ❌ ValueError: 半径不能为负数
# c.area = 100 # ❌ AttributeError: can't set attribute
十一、运算符重载
Python 通过魔术方法(双下划线方法)实现运算符重载,让自定义对象支持 +、-、print() 等操作:
class Vector:
def __init__(self, a, b):
self.a = a
self.b = b
def __str__(self): # print() 时调用
return f'Vector ({self.a}, {self.b})'
def __repr__(self): # 交互式环境 / repr() 调用
return f'Vector({self.a}, {self.b})'
def __add__(self, other): # + 运算符
return Vector(self.a + other.a, self.b + other.b)
def __sub__(self, other): # - 运算符
return Vector(self.a - other.a, self.b - other.b)
def __eq__(self, other): # == 运算符
return self.a == other.a and self.b == other.b
v1 = Vector(2, 10)
v2 = Vector(5, -2)
print(v1 + v2) # Vector (7, 8)
print(v1 - v2) # Vector (-3, 12)
print(v1 == v2) # False
str vs repr 的区别
两个网站和社区笔记中都强调了这点:
__str__:给人看的,print(obj) 和 str(obj) 时调用
__repr__:给开发者看的,交互式终端显示和 repr(obj) 时调用
如果只定义了其中一个,print() 会优先用 __str__,找不到才用 __repr__。
常用魔术方法速查
反向运算符
当 a + b 中 a 没有定义 __add__ 时,Python 会尝试调用 b.__radd__(a):
class Vector:
def __init__(self, a, b):
self.a = a
self.b = b
def __add__(self, other):
if isinstance(other, int):
return Vector(self.a + other, self.b)
return Vector(self.a + other.a, self.b + other.b)
def __radd__(self, other): # 反向加法 — 处理 int + Vector 的情况
if isinstance(other, int):
return Vector(self.a + other, self.b)
raise ValueError("值错误")
v = Vector(2, 10)
print(v + 5) # Vector (7, 10) — 调用 __add__
print(5 + v) # Vector (7, 10) — 调用 __radd__
十二、抽象类
当一个类不希望被直接实例化,而是作为"模板"让子类继承时,可以用 abc 模块定义抽象类:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): # 子类必须实现这个方法
pass
@abstractmethod
def perimeter(self):
pass
# s = Shape() # ❌ TypeError: Can't instantiate abstract class
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
c = Circle(5)
print(c.area()) # 78.54
print(c.perimeter()) # 31.42
如果子类没有实现所有抽象方法,实例化时会报错:
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
# 没有实现 perimeter
# r = Rectangle(3, 4) # ❌ TypeError: Can't instantiate abstract class
十三、dataclass(Python 3.7+)
当类主要是用来存数据的时候,__init__ 写起来很啰嗦。@dataclass 装饰器可以自动生成 __init__、__repr__、__eq__ 等方法:
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1) # Point(x=1, y=2) — 自动生成了 __repr__
print(p1 == p2) # True — 自动生成了 __eq__
等价于手动写:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f'Point(x={self.x}, y={self.y})'
def __eq__(self, other):
return self.x == other.x and self.y == other.y