前言
写一个能用的类不难,写一个"经得起扩展"的类却需要理解不少语法细节:class语句的完整形式、类文档串(docstring)的位置、构造函数的约束、继承与方法重写、以及双下划线开头的名字为什么会"消失"。
常见误解有两个:一是以为__name这种双下划线前缀的属性是"私有成员",外部绝对访问不到;二是以为重写父类方法时不需要调用super()。前者是误解,Python 只是做名字改写(name mangling),不是访问控制;后者要看场景,__init__的重写几乎一定要调super().__init__(),否则父类的初始化逻辑会被跳过。
本文按"怎么定义、怎么用、怎么继承、怎么封装"的顺序,把类的语法细节讲全。
一、class语句的完整形式
class语句的基本形式是class 类名(基类列表):,类体就是缩进块。类体第一条语句如果是一个字符串字面量,会被当作该类的文档串。
# 适用于 Python 3.8+
class Circle:
"""表示一个圆。
属性:
radius: 半径,必须为正数。
"""
pi = 3.141592653589793 # 类属性
def __init__(self, radius):
if radius <= 0:
raise ValueError("半径必须为正")
self.radius = radius
def area(self):
return Circle.pi * self.radius ** 2
c = Circle(2)
print(c.area()) # 12.566370614359172
print(Circle.__doc__.splitlines()[0]) # 表示一个圆。几个语法要点:
- 类名习惯用大驼峰(CapWords),函数用蛇形(snake_case);
- 基类列表可以为空,此时隐式继承
object; - 类体里可以写任意语句(赋值、条件、循环、嵌套函数),它们在定义时执行;
- 类体的文档串必须紧跟在
class行之后,否则不会被识别为__doc__。
二、__init__与实例化
__init__不是"构造函数",它只负责初始化已经被创建出来的实例,真正的创建由__new__完成。__init__必须返回None,返回别的值会抛TypeError。
# 适用于 Python 3.8+
class User:
def __init__(self, name, roles=None):
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 f"User({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.8+
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "..."
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # 别忘了这一行
self.breed = breed
def speak(self):
return f"{self.name} 说:汪汪"
d = Dog("Rex", "柴犬")
print(d.speak()) # Rex 说:汪汪
print(isinstance(d, Animal)) # True
print(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.8+
class Vault:
def __init__(self):
self.__secret = 42
self._single = "ok" # 单下划线只是约定:内部使用
self.__dunder__ = "特殊方法名,不改写"
def reveal(self):
return self.__secret
v = Vault()
print(v.reveal()) # 42
print(v._Vault__secret) # 42,改写后照样能访问
print(vars(v))
# {'_Vault__secret': 42, '_single': 'ok', '__dunder__': '特殊方法名,不改写'}结论:双下划线前缀只能防止"子类不小心重名",阻止不了任何人访问。真正的封装靠的是清晰的接口约定,而不是名字前缀。
五、用抽象基类定义接口
如果一个类只用来规定"子类必须实现哪些方法",可以用abc模块的ABC和@abstractmethod。含未实现抽象方法的类不能被实例化。
# 适用于 Python 3.8+
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""子类必须实现。"""
def describe(self):
return f"面积是 {self.area()}"
class Square(Shape):
def __init__(self, side):
self.side = side
def area(self):
return self.side ** 2
print(Square(3).describe()) # 面积是 9
try:
Shape() # TypeError: Can't instantiate abstract class
except TypeError as e:
print("TypeError:", e)六、综合示例
# 适用于 Python 3.8+
class Stack:
"""一个简单的后进先出栈。"""
def __init__(self, items=None):
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 f"Stack({self._items!r})"
s = Stack([1, 2])
s.push(3)
print(len(s)) # 3
print(s.pop()) # 3
print(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, items=None): 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 = 1
def get_v(self): return self._v ```
- 重写方法时签名不一致
❌
```python # 适用于 Python 3.8+ class Base: def save(self, path, overwrite=False): ...
class Sub(Base): def save(self, path): # 少了一个参数,替换使用时会崩 ... ```
✅
``python # 适用于 Python 3.8+ class Sub(Base): def save(self, path, overwrite=False): ...``
- 抽象基类忘了实现抽象方法就想实例化
❌
```python # 适用于 Python 3.8+ class Square(Shape): pass
Square() # TypeError ```
✅
``python # 适用于 Python 3.8+ class Square(Shape): def area(self): return 1``
- 类属性与实例属性同名造成困惑
❌
```python # 适用于 Python 3.8+ class A: count = 0
def bump(self): self.count += 1 # 生成实例属性,类属性不变 ```
✅
```python # 适用于 Python 3.8+ class A: count = 0
def bump(self): type(self).count += 1 ```
总结
| 语法点 | 要点 |
|---|
class语句 | 类体在定义时执行一次 |
| 文档串 | 必须是类体第一条语句 |
__init__ | 只初始化,必须返回None |
| 继承 | super().__init__()不要漏 |
| 名字改写 | __x变_类名__x,非私有 |
ABC | 未实现抽象方法不能实例化 |
定义类时,最容易出错的不是语法,而是几个约定:可变默认参数、super().__init__()、以及把双下划线当私有。把接口用文档串和公开方法表达清楚,比堆名字前缀更可靠。