
文章目录
📖 开篇导读
上一节课我们学习了面向对象的基本概念:类与对象、属性和方法、构造函数、封装等。你已经能够定义简单的类并创建对象了。但你是否好奇:当我们写obj = MyClass()时,背后到底发生了什么?实例属性是如何存储和查找的?类方法和静态方法到底有什么区别?如何控制实例的属性访问?
💡 工作场景:在实际项目开发中,理解类的底层机制可以帮助你写出更高效的代码。例如,使用
__slots__可以大幅节省内存(适合创建大量对象的场景);掌握属性查找顺序可以避免意外覆盖;正确使用@property可以让API更加优雅。
本课将深入讲解:
- 类的定义:新式类与旧式类(Python 3统一为新式类)
- 实例化过程:
__new__与__init__的协同工作 - 属性查找顺序:实例属性→类属性→父类属性
- 方法类型:实例方法、类方法、静态方法的本质区别
- 属性管理:
__dict__与__slots__、__getattr__与__setattr__ - 可调用对象:
__call__使实例像函数一样调用
学完本课,你将彻底掌握类的内部工作机制,写出更专业、更高效的面向对象代码。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解类的定义本质,了解Python 3中的新式类 | 面试基础 |
| 2️⃣ | 掌握实例化流程:__new__和__init__的作用 | 理解对象创建全过程 |
| 3️⃣ | 深入理解属性查找顺序(MRO) | 解决属性覆盖问题 |
| 4️⃣ | 区分实例方法、类方法、静态方法的本质 | 写出清晰的方法类型 |
| 5️⃣ | 掌握__dict__和__slots__,优化内存 | 处理大量对象时优化性能 |
| 6️⃣ | 理解属性描述符初步与@property原理 | 面试常考,进阶必备 |
🔥 面试考点:“
__new__和__init__的区别?”“Python中属性查找的顺序是什么?”“__slots__的作用及优缺点?”“类方法和静态方法的区别?”
📚 知识点理论精讲
一、类的定义:从旧式类到新式类
1.1 Python 2 与 Python 3 的区别
- Python 2:存在旧式类(classic class)和新式类(new-style class)。旧式类不继承
object,功能受限;新式类需要显式继承object。 - Python 3:所有类默认继承
object,都是新式类。因此直接写class MyClass:即可。
# Python 3 中,以下两种写法等价
class Person:
pass
class Person(object):
pass
1.2 类的本质
类是对象的蓝图,但类本身也是对象(类的类称为元类,后续课程讲解)。类在定义时,会创建一个类对象,可以动态地添加属性。
class MyClass:
pass
# 类也是对象,可以动态添加属性
MyClass.version = "1.0"
print(MyClass.version) # 1.0
二、实例化过程:从类到对象
当我们调用类名()时,Python执行以下步骤:
- 调用
__new__(cls, *args, **kwargs):创建并返回一个空的实例对象(通常是cls的实例)。 - 如果
__new__返回的是cls的实例,则自动调用__init__(self, *args, **kwargs)初始化该实例。 - 返回初始化后的实例。
__new__ 是一个静态方法(特殊处理),用于控制对象的创建过程,通常用于实现单例模式或不可变类型的子类(如继承元组、字符串等)。
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
self.value = value
s1 = Singleton(1)
s2 = Singleton(2)
print(s1 is s2) # True
print(s1.value, s2.value) # 2 2 (因为第二次初始化覆盖了)
注意:绝大多数情况下你只需要定义__init__,无需重写__new__。
三、属性查找顺序
访问实例属性时(如obj.attr),Python按照以下顺序查找:
- 实例的
__dict__(实例属性字典) - 类的
__dict__(类属性) - 基类的
__dict__(按照MRO顺序查找) - 如果仍未找到,且定义了
__getattr__方法,则调用它。 - 否则抛出
AttributeError。
3.1 __dict__ 属性
每个对象(实例、类)都有一个__dict__字典,用于存储属性。
class Student:
school = "一中" # 类属性
def __init__(self, name):
self.name = name # 实例属性
s = Student("张三")
print(s.__dict__) # {'name': '张三'}
print(Student.__dict__) # 包含school、__init__等
3.2 实例属性覆盖类属性
class A:
x = 10
a = A()
print(a.x) # 10(从类中找到)
a.x = 20 # 创建实例属性,覆盖类属性
print(a.x) # 20
print(A.x) # 10(类属性未变)
3.3 方法解析顺序 MRO
MRO(Method Resolution Order)决定了属性在多继承中的查找顺序。使用类名.__mro__或类名.mro()查看。
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
四、方法的本质
4.1 实例方法
实例方法的第一个参数是self,调用时自动传入实例。实例方法可以访问实例属性和类属性。
class MyClass:
def instance_method(self):
return f"实例方法,self={self}"
obj = MyClass()
obj.instance_method()
# 等价于 MyClass.instance_method(obj)
4.2 类方法(@classmethod)
类方法的第一个参数是cls(类本身),可以通过类或实例调用。适用于需要访问类变量或创建工厂方法。
class Person:
count = 0
def __init__(self, name):
self.name = name
Person.count += 1
@classmethod
def get_count(cls):
return cls.count
print(Person.get_count()) # 0
4.3 静态方法(@staticmethod)
静态方法没有默认的第一个参数,既不需要self也不需要cls。相当于普通函数,但放在类的命名空间中组织代码。
class MathUtil:
@staticmethod
def add(a, b):
return a + b
print(MathUtil.add(3, 5)) # 8
4.4 三种方法的对比
| 类型 | 第一个参数 | 可通过实例调用 | 可通过类调用 | 访问实例属性 | 访问类属性 |
|---|---|---|---|---|---|
| 实例方法 | self | 是 | 是(需手动传实例) | 是 | 是 |
| 类方法 | cls | 是 | 是 | 否 | 是 |
| 静态方法 | 无 | 是 | 是 | 否(通过类名间接) | 否(通过类名间接) |
五、属性管理:__slots__ 与 __dict__
5.1 __dict__ 的缺点
每个实例默认有一个__dict__字典,用于存储属性。字典占用内存较大,当创建成千上万个实例时,内存开销会很显著。
5.2 __slots__ 优化
通过在类中定义__slots__元组,可以限制实例只能拥有指定的属性,并移除__dict__,大幅节省内存。
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(p.x) # 1
# p.z = 3 # AttributeError: 'Point' object has no attribute 'z'
优点:
- 节省内存(每个实例节省几十到几百字节)
- 属性访问速度略快
缺点:
- 不能动态添加未在
__slots__中声明的属性 - 多继承时需注意各个父类的
__slots__合并
💡 工作应用:在需要创建大量对象(如游戏中的粒子、图形学中的点)时使用
__slots__。
5.3 __getattr__ 和 __setattr__
__getattr__(self, name):当通过正常方式找不到属性时调用。__getattribute__(self, name):每次访问属性都会调用(慎用,易导致递归)。__setattr__(self, name, value):每次设置属性时调用。
class Dynamic:
def __init__(self):
self._data = {}
def __getattr__(self, name):
if name in self._data:
return self._data[name]
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name == '_data':
super().__setattr__(name, value)
else:
self._data[name] = value
d = Dynamic()
d.name = "张三"
print(d.name) # 张三
# print(d.age) # AttributeError
六、可调用对象:__call__ 方法
通过在类中定义__call__,可以让实例像函数一样被调用。
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, x):
return x * self.factor
double = Multiplier(2)
print(double(5)) # 10
print(callable(double)) # True
应用场景:装饰器类、带状态的函数、策略模式。
七、属性描述符初步
描述符是实现了__get__、__set__、__delete__的类,用于管理另一个类的属性访问。@property就是基于描述符实现的。
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.name} must be positive")
obj.__dict__[self.name] = value
class Order:
quantity = PositiveNumber()
price = PositiveNumber()
def __init__(self, quantity, price):
self.quantity = quantity
self.price = price
o = Order(10, 99.9)
# o.quantity = -5 # ValueError
描述符是Python中非常强大的特性,后续课程会深入讲解。
💻 代码案例实操
案例1:__new__ 实现单例模式
"""
singleton_new.py
使用__new__实现单例模式
"""
class DatabaseConnection:
"""数据库连接单例"""
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
print("创建新的数据库连接实例")
cls._instance = super().__new__(cls)
else:
print("复用已有实例")
return cls._instance
def __init__(self, host="localhost", port=3306):
# 注意:__init__每次调用都会执行,需要避免重复初始化
if not hasattr(self, '_initialized'):
self.host = host
self.port = port
self._initialized = True
print(f"初始化连接: {host}:{port}")
def query(self, sql):
print(f"执行SQL: {sql}")
# 测试
conn1 = DatabaseConnection("192.168.1.1", 3306)
conn2 = DatabaseConnection()
print(conn1 is conn2) # True
conn1.query("SELECT * FROM users")
案例2:属性查找顺序演示
"""
attr_lookup.py
演示属性查找顺序:实例 -> 类 -> 父类 -> 祖父类...
"""
class GrandParent:
value = "GrandParent's value"
class Parent(GrandParent):
value = "Parent's value"
def __init__(self):
self.value = "Instance's value"
class Child(Parent):
pass
c = Child()
print(c.value) # "Instance's value"(实例属性优先)
# 删除实例属性后
del c.value
print(c.value) # "Parent's value"(类属性)
# 删除类属性(注意:这会删除Parent类的属性)
del Parent.value
print(c.value) # "GrandParent's value"(父类属性)
# 观察默认的__init__为实例添加属性
class Simple:
def __init__(self):
self.x = 100
s = Simple()
print(s.__dict__) # {'x': 100}
print(Simple.__dict__) # 包含__init__等
案例3:__slots__ 内存优化对比
"""
slots_memory.py
对比使用__slots__和不使用的内存占用
"""
import sys
class WithoutSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class WithSlots:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
# 创建大量实例并比较内存(粗略评估)
def memory_usage(cls, count=100000):
instances = [cls(i, i*2) for i in range(count)]
total_size = sum(sys.getsizeof(inst) for inst in instances)
# 注意:getsizeof不包含引用的对象的大小,只是粗略
return total_size / 1024 # KB
without_size = memory_usage(WithoutSlots, 100000)
with_size = memory_usage(WithSlots, 100000)
print(f"WithoutSlots 100k实例估算内存: {without_size:.2f} KB")
print(f"WithSlots 100k实例估算内存: {with_size:.2f} KB")
print(f"节省: {(1 - with_size/without_size)*100:.1f}%")
案例4:__getattr__ 与 __setattr__ 实现动态属性
"""
dynamic_attr.py
通过__getattr__和__setattr__实现动态属性存储
"""
class DynamicObject:
def __init__(self):
# 使用私有字典存储动态属性
self._dynamic_attrs = {}
def __getattr__(self, name):
if name in self._dynamic_attrs:
return self._dynamic_attrs[name]
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
def __setattr__(self, name, value):
if name == '_dynamic_attrs':
super().__setattr__(name, value)
else:
print(f"动态设置属性: {name} = {value}")
self._dynamic_attrs[name] = value
def __delattr__(self, name):
if name in self._dynamic_attrs:
del self._dynamic_attrs[name]
else:
super().__delattr__(name)
# 使用
obj = DynamicObject()
obj.name = "张三"
obj.age = 25
print(obj.name) # 张三
print(obj.age) # 25
# print(obj.xxx) # AttributeError
# 查看内部存储
print(obj._dynamic_attrs) # {'name': '张三', 'age': 25}
案例5:__call__ 实现可调用类
"""
callable_demo.py
使用__call__让类实例成为可调用对象,实现带状态的函数
"""
import time
class Timer:
"""计时器类,可多次调用累加时间"""
def __init__(self):
self.total = 0.0
def __call__(self, func):
"""装饰器:记录函数执行时间"""
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
self.total += elapsed
print(f"{func.__name__} 耗时: {elapsed:.4f}秒,累计: {self.total:.4f}秒")
return result
return wrapper
timer = Timer()
@timer
def task1():
time.sleep(0.5)
@timer
def task2():
time.sleep(0.3)
task1()
task2()
# 输出累计时间
案例6:MRO与多继承方法查找
"""
mro_demo.py
演示多继承中的方法解析顺序
"""
class A:
def method(self):
print("A.method")
class B(A):
def method(self):
print("B.method")
super().method()
class C(A):
def method(self):
print("C.method")
super().method()
class D(B, C):
def method(self):
print("D.method")
super().method()
# 查看MRO
print(D.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)
d = D()
d.method()
# 输出顺序: D.method -> B.method -> C.method -> A.method
案例7:描述符模拟@property
"""
descriptor_demo.py
通过描述符实现类似@property的功能
"""
class PositiveNumber:
"""非负数描述符"""
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} cannot be negative")
obj.__dict__[self.name] = value
class Student:
score = PositiveNumber()
height = PositiveNumber()
def __init__(self, name, score, height):
self.name = name
self.score = score
self.height = height
s = Student("张三", 85, 175)
print(s.score) # 85
try:
s.score = -10 # ValueError
except ValueError as e:
print(e)
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 在__new__中忘记返回实例 | __init__不会被调用,返回None | 返回super().__new__(cls) |
| 2 | __slots__中定义的属性名与实例方法名冲突 | 属性覆盖方法 | 避免属性名与方法名相同 |
| 3 | 在__getattr__中无限递归 | 访问自身不存在的属性导致无限循环 | 确保不触发__getattr__自身 |
| 4 | 在__setattr__中使用self.name = value | 无限递归 | 使用super().__setattr__(name, value) |
| 5 | 混淆类方法和实例方法,在类方法中访问实例属性 | 错误或不可预测行为 | 类方法只能访问类属性和其他类方法 |
| 6 | 多继承时方法查找顺序混乱(菱形继承) | 调用了预期外的父类方法 | 使用super()并理解MRO |
| 7 | 在__slots__子类中未重新定义__slots__ | 子类会有__dict__,失去内存优化 | 子类也需要定义__slots__ |
| 8 | 在__call__中修改实例状态但忘记返回值 | 调用结果可能是None | 确保返回有意义的值 |
| 9 | 将静态方法误认为类方法,试图访问类变量 | 静态方法中不能直接访问类变量 | 需要访问类变量时用类方法 |
| 10 | 在__init__中返回非None值 | TypeError | __init__只能返回None |
📝 课后实战练习题
第1题:使用__new__实现限制实例数量
定义一个类LimitedInstance,限制只能创建最多3个实例。当尝试创建第4个时,返回已存在的第1个实例(循环复用)。提示:使用类变量存储实例列表。
第2题:属性查找顺序分析
写出以下代码的输出结果,并解释原因。
class A:
x = 1
class B(A):
x = 2
class C(A):
x = 3
class D(B, C):
pass
d = D()
print(d.x)
d.x = 4
print(d.x, D.x, B.x, C.x, A.x)
del d.x
print(d.x)
第3题:实现__getattr__和__setattr__的只读属性
创建一个类ReadOnly,其所有属性在初始化后不能修改(只读)。如果尝试修改,抛出AttributeError。提示:在__setattr__中检查属性是否已存在。
第4题:__slots__实验
定义一个类Item,包含属性name和price。分别使用__slots__和不使用,创建100万个实例,测量内存占用(使用psutil或tracemalloc模块)和实例化速度。比较差异。
第5题:可调用对象实现累加器
编写一个类Accumulator,其初始化接收初始值。实例可以被调用,每次调用传入一个增量,返回累加后的值。例如acc = Accumulator(10); acc(5) -> 15; acc(3) -> 18。
第6题:使用描述符实现类型检查
实现描述符Typed,在赋值时检查值的类型。例如:
class Person:
name = Typed(str)
age = Typed(int)
当赋值为错误类型时,抛出TypeError。
第7题:MRO与super()练习
定义以下类结构:
Animal:move()打印“动物移动”Bird(Animal):move()打印“鸟飞翔”,并调用super().move()Fish(Animal):move()打印“鱼游泳”,并调用super().move()Duck(Bird, Fish):move()打印“鸭子走路”,并调用super().move()
创建Duck实例调用move(),解释输出顺序。再修改使得Bird和Fish的move不调用super(),观察变化。
🧠 知识点思维导图总结
🔜 下节课预告
本课我们深入探讨了类的定义、实例化、属性与方法的各种细节。下一节课我们将重点学习构造与析构、私有属性、封装落地。
第23课:构造方法、析构方法、私有属性与封装思想落地实现
内容包括:
- 构造方法
__init__的高级用法 - 析构方法
__del__与资源释放 - 私有属性的更多应用场景
- 封装的最佳实践
@property深入与属性描述符- 实战:封装一个线程安全的计数器
封装是面向对象的三大特性之一,学好它才能写出真正安全、易用的类。
🌟 学习鼓励:本课内容较为深入,涉及Python底层机制。不要急于一次性掌握所有要点,建议逐个知识点配合案例调试。特别是
__slots__和描述符,是Python进阶的重要标志。坚持下来,你离Python高手又近了一步!
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

9096

被折叠的 条评论
为什么被折叠?



