前言写一个能用的类不难写一个经得起扩展的类却需要理解不少语法细节class语句的完整形式、类文档串docstring的位置、构造函数的约束、继承与方法重写、以及双下划线开头的名字为什么会消失。常见误解有两个一是以为__name这种双下划线前缀的属性是私有成员外部绝对访问不到二是以为重写父类方法时不需要调用super()。前者是误解Python 只是做名字改写name mangling不是访问控制后者要看场景__init__的重写几乎一定要调super().__init__()否则父类的初始化逻辑会被跳过。本文按怎么定义、怎么用、怎么继承、怎么封装的顺序把类的语法细节讲全。一、class语句的完整形式class语句的基本形式是class 类名(基类列表):类体就是缩进块。类体第一条语句如果是一个字符串字面量会被当作该类的文档串。# 适用于 Python 3.8class Circle:表示一个圆。属性:radius: 半径必须为正数。pi 3.141592653589793 # 类属性def __init__(self, radius):if radius 0:raise ValueError(半径必须为正)self.radius radiusdef area(self):return Circle.pi * self.radius ** 2c Circle(2)print(c.area()) # 12.566370614359172print(Circle.__doc__.splitlines()[0]) # 表示一个圆。几个语法要点类名习惯用大驼峰CapWords函数用蛇形snake_case基类列表可以为空此时隐式继承object类体里可以写任意语句赋值、条件、循环、嵌套函数它们在定义时执行类体的文档串必须紧跟在class行之后否则不会被识别为__doc__。二、__init__与实例化__init__不是构造函数它只负责初始化已经被创建出来的实例真正的创建由__new__完成。__init__必须返回None返回别的值会抛TypeError。# 适用于 Python 3.8class User:def __init__(self, name, rolesNone):self.name name# 不要用可变对象当默认参数self.roles list(roles) if roles is not None else []def add_role(self, role):if role not in self.roles:self.roles.append(role)def __repr__(self):return fUser({self.name!r}, roles{self.roles!r})u User(Ada)u.add_role(admin)print(u) # User(Ada, roles[admin])print(User(Bob).roles) # []这里刻意避开了def __init__(self, roles[])这个经典陷阱默认参数在函数定义时求值一次可变默认参数会被所有调用共享。三、继承、重写与super()子类用class Sub(Base):声明。重写方法时如果希望保留父类行为用super()调用父类实现。super()走的是方法解析顺序MRO不是简单的父类。# 适用于 Python 3.8class Animal:def __init__(self, name):self.name namedef speak(self):return ...class Dog(Animal):def __init__(self, name, breed):super().__init__(name) # 别忘了这一行self.breed breeddef speak(self):return f{self.name} 说汪汪d Dog(Rex, 柴犬)print(d.speak()) # Rex 说汪汪print(isinstance(d, Animal)) # Trueprint(Dog.__mro__) # (Dog, Animal, object)如果子类__init__忘了super().__init__(name)self.name就不会被设置之后调用speak()会直接AttributeError。关系判断方式结果子类实例是父类实例吗isinstance(d, Animal)True子类是父类的子类吗issubclass(Dog, Animal)True子类继承父类私有名吗名字改写后仍可访问但名字被改写子类能覆盖类属性吗在子类里重新赋值即可可以四、名字改写不是私有以双下划线开头、且最多一个下划线结尾的名字例如__secret会被改写成_类名__secret。这叫做名字改写name mangling目的是避免子类意外覆盖它不是访问控制。# 适用于 Python 3.8class Vault:def __init__(self):self.__secret 42self._single ok # 单下划线只是约定内部使用self.__dunder__ 特殊方法名不改写def reveal(self):return self.__secretv Vault()print(v.reveal()) # 42print(v._Vault__secret) # 42改写后照样能访问print(vars(v))# {_Vault__secret: 42, _single: ok, __dunder__: 特殊方法名不改写}结论双下划线前缀只能防止子类不小心重名阻止不了任何人访问。真正的封装靠的是清晰的接口约定而不是名字前缀。五、用抽象基类定义接口如果一个类只用来规定子类必须实现哪些方法可以用abc模块的ABC和abstractmethod。含未实现抽象方法的类不能被实例化。# 适用于 Python 3.8from abc import ABC, abstractmethodclass Shape(ABC):abstractmethoddef area(self):子类必须实现。def describe(self):return f面积是 {self.area()}class Square(Shape):def __init__(self, side):self.side sidedef area(self):return self.side ** 2print(Square(3).describe()) # 面积是 9try:Shape() # TypeError: Cant instantiate abstract classexcept TypeError as e:print(TypeError:, e)六、综合示例# 适用于 Python 3.8class Stack:一个简单的后进先出栈。def __init__(self, itemsNone):self._items list(items) if items is not None else []def push(self, item):self._items.append(item)def pop(self):if not self._items:raise IndexError(pop from empty stack)return self._items.pop()def __len__(self):return len(self._items)def __repr__(self):return fStack({self._items!r})s Stack([1, 2])s.push(3)print(len(s)) # 3print(s.pop()) # 3print(s) # Stack([1, 2])常见坑点可变默认参数❌python # 适用于 Python 3.8 class A: def __init__(self, items[]): self.items items✅python # 适用于 Python 3.8 class A: def __init__(self, itemsNone): self.items [] if items is None else list(items)子类__init__不调super().__init__()❌python # 适用于 Python 3.8 class Dog(Animal): def __init__(self, name, breed): self.breed breed # 缺 name✅python # 适用于 Python 3.8 class Dog(Animal): def __init__(self, name, breed): super().__init__(name) self.breed breed文档串位置放错❌python # 适用于 Python 3.8 class A: pi 3.14 这是圆的说明 # 不是文档串✅python # 适用于 Python 3.8 class A: 这是圆的说明 pi 3.14以为双下划线是真私有❌python # 适用于 Python 3.8 class A: def __init__(self): self.__v 1 # 以为外部 a.__v 一定失败就一定安全✅python # 适用于 Python 3.8 # 用单下划线表达内部使用并提供公开方法访问 class A: def __init__(self): self._v 1def get_v(self): return self._v 重写方法时签名不一致❌python # 适用于 Python 3.8 class Base: def save(self, path, overwriteFalse): ...class Sub(Base): def save(self, path): # 少了一个参数替换使用时会崩 ... ✅python # 适用于 Python 3.8 class Sub(Base): def save(self, path, overwriteFalse): ...抽象基类忘了实现抽象方法就想实例化❌python # 适用于 Python 3.8 class Square(Shape): passSquare() # TypeError ✅python # 适用于 Python 3.8 class Square(Shape): def area(self): return 1类属性与实例属性同名造成困惑❌python # 适用于 Python 3.8 class A: count 0def bump(self): self.count 1 # 生成实例属性类属性不变 ✅python # 适用于 Python 3.8 class A: count 0def bump(self): type(self).count 1 总结语法点要点class语句类体在定义时执行一次文档串必须是类体第一条语句__init__只初始化必须返回None继承super().__init__()不要漏名字改写__x变_类名__x非私有ABC未实现抽象方法不能实例化定义类时最容易出错的不是语法而是几个约定可变默认参数、super().__init__()、以及把双下划线当私有。把接口用文档串和公开方法表达清楚比堆名字前缀更可靠。