第22课:Python|类的定义、实例化、属性与方法全方位精讲

在这里插入图片描述


📖 开篇导读

上一节课我们学习了面向对象的基本概念:类与对象、属性和方法、构造函数、封装等。你已经能够定义简单的类并创建对象了。但你是否好奇:当我们写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执行以下步骤:

  1. 调用__new__(cls, *args, **kwargs):创建并返回一个空的实例对象(通常是cls的实例)。
  2. 如果__new__返回的是cls的实例,则自动调用__init__(self, *args, **kwargs)初始化该实例。
  3. 返回初始化后的实例。

__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按照以下顺序查找:

  1. 实例的__dict__(实例属性字典)
  2. 类的__dict__(类属性)
  3. 基类的__dict__(按照MRO顺序查找)
  4. 如果仍未找到,且定义了__getattr__方法,则调用它。
  5. 否则抛出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,包含属性nameprice。分别使用__slots__和不使用,创建100万个实例,测量内存占用(使用psutiltracemalloc模块)和实例化速度。比较差异。

第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()练习

定义以下类结构:

  • Animalmove()打印“动物移动”
  • Bird(Animal)move()打印“鸟飞翔”,并调用super().move()
  • Fish(Animal)move()打印“鱼游泳”,并调用super().move()
  • Duck(Bird, Fish)move()打印“鸭子走路”,并调用super().move()
    创建Duck实例调用move(),解释输出顺序。再修改使得BirdFishmove不调用super(),观察变化。

🧠 知识点思维导图总结

第22课:类定义与实例化深入

类的定义

新式类(Python 3默认)

类也是对象

实例化过程

new 创建实例

init 初始化实例

单例模式实现

属性查找

实例__dict__ → 类__dict__ → MRO

__dict__查看属性字典

MRO确定继承顺序

方法类型

实例方法 self

类方法 @classmethod cls

静态方法 @staticmethod

属性管理

slots 内存优化

动态属性 getattr/setattr

__getattribute__全拦截

可调用对象

call 使实例可调用

描述符初步

get/set/delete

@property原理

面试考点

new vs init

__slots__利弊

MRO计算

三种方法区别


🔜 下节课预告

本课我们深入探讨了类的定义、实例化、属性与方法的各种细节。下一节课我们将重点学习构造与析构、私有属性、封装落地

第23课:构造方法、析构方法、私有属性与封装思想落地实现

内容包括:

  • 构造方法__init__的高级用法
  • 析构方法__del__与资源释放
  • 私有属性的更多应用场景
  • 封装的最佳实践
  • @property深入与属性描述符
  • 实战:封装一个线程安全的计数器

封装是面向对象的三大特性之一,学好它才能写出真正安全、易用的类。

🌟 学习鼓励:本课内容较为深入,涉及Python底层机制。不要急于一次性掌握所有要点,建议逐个知识点配合案例调试。特别是__slots__和描述符,是Python进阶的重要标志。坚持下来,你离Python高手又近了一步!


🔗《50节课 Python 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas.Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值