楼层: 首页/ 软件技术/ Python 基础/ 面向对象 OOP
5

面向对象 OOP

Classes, Inheritance & dataclass

面向对象(OOP)听着唬人,其实就一句话:把"数据"和"操作数据的方法"打包成一个东西。比如"狗"这个类,有"名字、年龄"这些数据,也有"叫、跑"这些动作。你 new 一条狗出来,它就是个对象。Python 的 OOP 比 Java 简洁得多。

class、__init__、self

class Dog: # __init__ 是构造器,new 对象时自动调用 def __init__(self, name, age): self.name = name # 实例属性:每个狗各有各的名字 self.age = age # 第一个参数永远是 self,代表"这个对象自己" def bark(self): return f"{self.name}:汪汪!" # new 两个对象 d1 = Dog("旺财", 3) d2 = Dog("来福", 5) print(d1.bark()) # 旺财:汪汪! print(d2.name) # 来福

论实例属性 vs 类属性

实例属性写在 __init__ 里、用 self. 挂——每个对象各有一份,改 d1 不影响 d2。类属性写在 class 里、方法外面——所有对象共享一份。想给所有狗加一个"物种=犬科",就写成类属性;想让每只狗有自己的名字,就写成实例属性。

继承、super()、多态

class Animal: def __init__(self, name): self.name = name def speak(self): return "..." # Dog 继承 Animal:自动获得 Animal 的所有属性和方法 class Dog(Animal): def __init__(self, name, breed): super().__init__(name) # 调父类的构造器,别重复写 self.name=name self.breed = breed # 重写父类的方法(多态) def speak(self): return f"{self.name}:汪!" d = Dog("旺财", "柴犬") print(d.speak(), d.breed) # 旺财:汪! 柴犬

封装:_、__、@property

class BankAccount: def __init__(self, balance): self._balance = balance # 单下划线:约定"内部用,别在外面乱碰"(Python 不强制) # @property:把一个方法伪装成属性,读的时候不用写括号 @property def balance(self): return self._balance # 可写版本:设置时可以加校验 @balance.setter def balance(self, value): if value < 0: raise ValueError("余额不能为负") self._balance = value acc = BankAccount(100) print(acc.balance) # 100(读的时候像属性,不是方法调用) acc.balance = 200 # 写的时候走 setter,会校验 # 双下划线 __name:触发"名字修饰",子类不容易意外覆盖 # 新手用单下划线 _ 就够了,双下划线别炫技

魔术方法 & 类方法 & 静态方法

class Vector: def __init__(self, x, y): self.x, self.y = x, y # __str__:print(对象) 时显示什么 def __str__(self): return f"Vector({self.x}, {self.y})" # __repr__:开发者看到的正式表示(调试时用) def __repr__(self): return f"Vector({self.x!r}, {self.y!r})" # __eq__:定义 == 怎么比较 def __eq__(self, other): return self.x == other.x and self.y == other.y v = Vector(1, 2) print(v) # Vector(1, 2) # @classmethod:第一个参数是类本身 cls,常用来做"备用构造器" class Date: def __init__(self, y, m, d): self.y, self.m, self.d = y, m, d @classmethod def today(cls): import datetime t = datetime.date.today() return cls(t.year, t.month, t.day) # @staticmethod:跟类和实例都没关系的普通函数,只是挂在类名下 @staticmethod def is_valid(y, m, d): return 1 <= m <= 12

@dataclass:写数据类再也不啰嗦

Python 3.7+ 内置的 @dataclass,自动帮你写 __init__、__repr__、__eq__——以前要写 10 行,现在 3 行。

from dataclasses import dataclass @dataclass class Student: name: str age: int score: float = 0.0 # 默认值 s = Student("小明", 18, 95.5) print(s) # Student(name='小明', age=18, score=95.5) print(s == Student("小明", 18, 95.5)) # True

MRO 与 super():多继承到底谁先谁后

Python 支持多继承(一个类同时继承多个父类),但父类们都有同名方法时,到底调谁的?答案是 MRO(Method Resolution Order,方法解析顺序)。Python 3 用 C3 线性化算法算出一条唯一的查找链,super() 就是沿着这条链往后走。用 类名.__mro__ 或 类名.mro() 能看这条链。

class A: def hello(self): print("A") class B(A): def hello(self): print("B") super().hello() class C(A): def hello(self): print("C") super().hello() class D(B, C): def hello(self): print("D") super().hello() print([c.__name__ for c in D.__mro__]) # ['D', 'B', 'C', 'A', 'object'] d = D() d.hello() # D # B # C # A

元类 metaclass:连"类"本身都能定制

前面说类是创建对象的模板。那类又是谁创建的?答案是元类(metaclass)。这是 Python 对象模型最底层的一块,日常业务几乎用不到,但读 ORM、读框架源码时一定会撞见,所以这里讲透。

论对象 → 类 → 元类,一层套一层

大白话:普通对象是"包子",类是"包子模具",元类就是"造模具的机床"。type 就是 Python 默认那台机床——你平时写 class Foo:,解释器背地里就是调 type() 把这个类"造出来"。

链条:对象.__class__ 是它的类;类.__class__(即 type(类))就是它的元类。一切类最终的元类都是 type,除非你自己换一台机床。

不用 class 关键字,手动用 type 造一个类

# type(类名, 父类元组, 属性/方法字典) —— 这就是 class 语句的本质 def greet(self): return f"我是{self.name}" Dog = type("Dog", (), {"name": "旺财", "greet": greet}) d = Dog() print(d.greet()) # 我是旺财 print(type(Dog)) # <class 'type'> Dog 这个类,它的类型是 type print(type(type)) # <class 'type'> 连 type 自己的类型也是 type(递归终点)

自定义元类:在类"被创建"那一刻插手

# 用途:所有子类一诞生,就自动把它的字段名转大写注册一遍 class UpperMeta(type): # __new__ 是"造类"的方法,比类方法更早执行 def __new__(cls, name, bases, namespace): # 把所有不以双下划线开头的属性名改成大写 new_ns = {k.upper(): v for k, v in namespace.items() if not k.startswith("_")} return super().__new__(cls, name, bases, new_ns) class Cfg(metaclass=UpperMeta): host = "127.0.0.1" port = 8000 print(hasattr(Cfg, "host")) # False 原名被改走了 print(Cfg.HOST) # 127.0.0.1 print(Cfg.PORT) # 8000

应用场景:这正是 Django ORM 的魔法来源——你写 class User(models.Model): name = CharField(),ORM 用元类在"类定义时"拦截每个字段,自动建字段表、注册模型、拼 SQL。框架作者用它一劳永逸,业务代码别用。

防坑:能用普通方案就别上元类

元类是"黑魔法",调试时栈很深、报错信息绕。99% 的场景,用类装饰器、__init_subclass__、或者基类就能达到同样效果,可读性好得多。Python 之禅说"Explicit is better than implicit"——除非你在写框架,否则别自己造元类。

练习:元类小测(点开对答案)

问:type(1) 和 type(int) 分别是什么?
答:type(1) 是 <class 'int'>——1 这个对象属于 int;type(int) 是 <class 'type'>——int 这个类是元类 type 造出来的。一条规律:问对象找谁,问类找 type。

本章面试题 · 面向对象

1.(概念题)类属性和实例属性有什么区别?下面代码最后输出什么?

class C:
    x = []
c1 = C(); c2 = C()
c1.x.append(1)
print(c2.x)

查看答案

答案:输出 [1]。x=[] 是类属性,所有实例共享同一个列表;c1.x.append(1) 改的是共享的那个列表,所以 c2.x 也变了。教训:类属性不要放可变对象,要放就放在 __init__ 里当实例属性。

2.(概念题)什么是 MRO?super() 一定是调"父类"吗?

查看答案

答案:MRO 是多继承时方法查找的顺序(C3 算法),可用 __mro__ 查看。super() 调的不是"父类",而是 MRO 链里当前类的下一个类。在多继承钻石结构里,super() 可能调到一个看起来不是直接父类的类——这正是它保证每个类只调一次的关键。

3.(概念题)什么是元类(metaclass)?谁是所有类的"类"?

查看答案

答案:类是创建对象的模板,而元类是创建类的模板。type 就是 Python 里所有类的默认元类——int("x") 创建对象,type("Foo", (), {}) 创建类。元类很少直接写,ORM(如 Django ORM)用它来在类定义时拦截字段、注册表。一句话:元类是"类的构造函数",99% 的业务代码用不到。

4.(代码题)__str__ 和 __repr__ 有什么区别?

查看答案

答案:__str__ 给人看(print()、str()),友好可读;__repr__ 给开发者看(调试、REPL 显示), ideally 是一句能重建该对象的 Python 表达式。定义了 __repr__ 没定义 __str__,print 会 fallback 到 __repr__。

5.(思考题)什么时候用 @property,什么时候用普通方法?

查看答案

答案:属性是"数据"(年龄、余额),方法是"动作"(存钱、跑)。如果一个计算很轻量、读起来像属性(比如 area、full_name),用 @property 让调用方写 obj.area 而不是 obj.area();如果有副作用或开销大(查数据库、发网络请求),用普通方法。原则:property 不该让人以为很贵。