目录
- [1. 前言](#1. 前言)
- [2. 加法运算](#2. 加法运算)
-
- [2.1 Python:通过魔法方法__add__实现](#2.1 Python:通过魔法方法__add__实现)
- [2.2 C++:通过operator+函数实现](#2.2 C++:通过operator+函数实现)
- [3. 其他运算的实现](#3. 其他运算的实现)
-
- [3.1 Python 实现](#3.1 Python 实现)
- [3.2 C++ 实现](#3.2 C++ 实现)
- [3.3 数值运算与比较运算的综合对比](#3.3 数值运算与比较运算的综合对比)
- [4. 其他魔法方法](#4. 其他魔法方法)
- [4.1 radd反向加法](#4.1 radd反向加法)
- [4.2 new和init方法](#4.2 new和init方法)
- [4.3 迭代器和生成器](#4.3 迭代器和生成器)
- [4.4 getitem索引和切片](#4.4 getitem索引和切片)
- [5. 后记](#5. 后记)
1. 前言
运算符重载是面向对象编程(OOP)的核心特性之一,它允许开发者为自定义类赋予内置运算符(如+、-、*、==等)的语义,让自定义对象能像内置类型(int、float)一样使用运算符,大幅提升代码的可读性和简洁性。
为什么我的很多文章中写Python的相关知识,总是引入C++,主要有以下2个方面的原因:
-
💊C++是使用最多和最为广泛的编程语言之一,比较容易让大众接受和理解。
-
💊C++的语言虽然细碎且繁琐,但能更加全面地展示语言设计的较多细节,因而能更好地帮助我们理解Python语言特性设计的精妙之所在。
本文将深度介绍Python常见的魔法方法,并将部分魔法方法与C++运算符重载进行对比,探讨其在语法结构和实现方式上的差异。
2. 加法运算
2.1 Python:通过魔法方法__add__实现
下面的程序中,__add__和__str__都是魔法方法,一个实现了类的加法运算,一个实现了类的打印输出。
python
class Point:
def __init__(self, x, y):
# 构造魔法方法,初始化坐标
self.x = x
self.y = y
def __add__(self, other):
# 重载+运算符的魔法方法,other为运算符右侧的对象
# 第一步:类型检查(Python需手动做,否则运行时可能报错)
if not isinstance(other, Point):
raise TypeError("只能与Point类型对象相加")
# 第二步:实现加法逻辑
new_x = self.x + other.x
new_y = self.y + other.y
# 第三步:返回新对象(避免修改原对象,符合Python设计习惯)
return Point(new_x, new_y)
def __str__(self):
# 重载字符串输出魔法方法,方便打印
return f"Point({self.x}, {self.y})"
# 测试代码
if __name__ == "__main__":
p1 = Point(1, 2)
p2 = Point(3, 4)
p3 = p1 + p2 # 隐式调用p1.__add__(p2)
print(p3) # 输出:Point(4, 6)
# 错误测试(类型不匹配)
try:
p1 + 10
except TypeError as e:
print(e) # 输出:只能与Point类型对象相加
上面的
运行,打印输出:

2.2 C++:通过operator+函数实现
C++实现起来就相对比较繁琐,需定义以operator为关键字、后跟运算符的成员函数 / 全局函数,编译期会严格检查参数类型、返回值,语法规则更严谨。
cpp
#include <iostream>
// 静态类型检查,必须包含头文件、声明类
class Point {
private:
// 私有成员变量,需通过接口访问
int x;
int y;
public:
// 构造函数:初始化坐标(对应Python的__init__)
Point(int x = 0, int y = 0) : x(x), y(y) {}
// 1. 成员函数形式重载+运算符(最常用)
// 语法:返回值类型 operator 运算符(参数列表)
Point operator+(const Point& other) const {
// const保证不修改自身对象,引用传递避免拷贝
int new_x = this->x + other.x;
int new_y = this->y + other.y;
return Point(new_x, new_y); // 返回新对象
}
// 辅助函数:打印坐标(对应Python的__str__)
void print() const {
std::cout << "Point(" << x << ", " << y << ")" << std::endl;
}
};
// 测试代码
int main() {
Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1 + p2; // 隐式调用p1.operator+(p2)
p3.print(); // 输出:Point(4, 6)
return 0;
}
运行,打印输出:

3. 其他运算的实现
现在的需求是:实现自定义 Vector 类,支持加法、减法、相等判断、下标访问。我们来综合对比一下两种语言的实现差异。
3.1 Python 实现
python
class Vector:
def __init__(self, *args):
self.elements = list(args) # 存储向量元素
# 重载+运算符
def __add__(self, other):
if not isinstance(other, Vector):
raise TypeError("只能与Vector类型相加")
if len(self.elements) != len(other.elements):
raise ValueError("向量维度必须相同")
new_elems = [a + b for a, b in zip(self.elements, other.elements)]
return Vector(*new_elems)
# 重载-运算符
def __sub__(self, other):
if not isinstance(other, Vector):
raise TypeError("只能与Vector类型相减")
if len(self.elements) != len(other.elements):
raise ValueError("向量维度必须相同")
new_elems = [a - b for a, b in zip(self.elements, other.elements)]
return Vector(*new_elems)
# 重载==运算符
def __eq__(self, other):
if not isinstance(other, Vector):
return False
return self.elements == other.elements
# 重载[]下标访问
def __getitem__(self, idx):
if idx < 0 or idx >= len(self.elements):
raise IndexError("下标越界")
return self.elements[idx]
def __str__(self):
return f"Vector({', '.join(map(str, self.elements))})"
# 测试
v1 = Vector(1, 2, 3)
v2 = Vector(4, 5, 6)
v3 = v1 + v2
print(v3) # Vector(5, 6, 9)
print(v1 - v2) # Vector(-3, -2, -1)
print(v1 == v2) # False
print(v3[0]) # 5
运行,打印输出:

🚩需要注意的是,在对对象元素进行下标访问的时候,需要进行越界判断。一般访问越界而产生的bug非常隐晦,而且测试比较难发现,尤其是在C/C++语言中,这种现象更为普遍,因为C/C++语言本身的设计哲学就是将更多底层的实现权利到开发者手上,而Python很多容易出现异常的地方,底层都会有相关的机制进行"兜底",隐藏实现细节,因此这种问题不太常见。以前我们组就因为一个数组访问越界的问题查了好几个小时!
3.2 C++ 实现
cpp
#include <iostream>
#include <vector>
#include <stdexcept>
class Vector {
private:
std::vector<int> elements;
public:
// 构造函数
template <typename... Args> Vector(Args... args) : elements({args...}) {}
// 重载+运算符
Vector operator+(const Vector& other) const {
if (elements.size() != other.elements.size()) {
throw std::invalid_argument("向量维度必须相同");
}
Vector res;
for (size_t i = 0; i < elements.size(); ++i) {
res.elements.push_back(elements[i] + other.elements[i]);
}
return res;
}
// 重载-运算符
Vector operator-(const Vector& other) const {
if (elements.size() != other.elements.size()) {
throw std::invalid_argument("向量维度必须相同");
}
Vector res;
for (size_t i = 0; i < elements.size(); ++i) {
res.elements.push_back(elements[i] - other.elements[i]);
}
return res;
}
// 重载==运算符
bool operator==(const Vector& other) const {
return elements == other.elements;
}
// 重载[]下标访问
int operator[](size_t idx) const {
if (idx >= elements.size()) {
throw std::out_of_range("下标越界");
}
return elements[idx];
}
// 打印函数
void print() const {
std::cout << "Vector(";
for (size_t i = 0; i < elements.size(); ++i) {
std::cout << elements[i];
if (i != elements.size() - 1) {
std::cout << ", ";
}
}
std::cout << ")" << std::endl;
}
};
// 测试
int main() {
Vector v1(1, 2, 3);
Vector v2(4, 5, 6);
Vector v3 = v1 + v2;
v3.print(); // Vector(5, 6, 9)
(v1 - v2).print(); // Vector(-3, -2, -1)
std::cout << (v1 == v2) << std::endl; // 0(false)
std::cout << v3[0] << std::endl; // 5
return 0;
}
运行,打印输出:

为了实现灵活的参数接收,我们采用了模版构造函数。同时,也定义了类的打印方法。
以上的语法,可以理解为:
🔶 先给工厂(编译器)定一个 "生产规则"(template <typename... Args>):能接收任意个数、任意类型的零件(参数);
🔶 再告诉工厂 "怎么组装产品"(Vector(Args... args)):把收到的零件组装成 Vector 对象。
或者也可以用std::initializer_list构造函数。
cpp
#include <iostream>
#include <vector>
#include <stdexcept>
#include <initializer_list>
class Vector {
private:
std::vector<int> elements;
public:
// 1. 显式声明默认构造函数
Vector() = default;
// 2. initializer_list构造函数(保持不变)
Vector(std::initializer_list<int> args)
: elements(args)
{}
Vector operator+(const Vector& other) const {
if (elements.size() != other.elements.size()) {
throw std::invalid_argument("向量维度必须相同");
}
Vector res; // 现在可以调用默认构造函数了
for (size_t i = 0; i < elements.size(); ++i) {
res.elements.push_back(elements[i] + other.elements[i]);
}
return res;
}
void print() const {
std::cout << "Vector(";
for (size_t i = 0; i < elements.size(); ++i) {
std::cout << elements[i];
if (i != elements.size() - 1) {
std::cout << ", ";
}
}
std::cout << ")" << std::endl;
}
};
int main() {
// 3. 用花括号{}来触发initializer_list构造
Vector v1{1, 2, 3};
Vector v2{4, 5, 6};
Vector v3 = v1 + v2;
v3.print(); // 输出:Vector(5, 6, 9)
return 0;
}
但需要注意,此时的初始化方法,就只能用{},()会报错。
3.3 数值运算与比较运算的综合对比
通过以下表格,可以清晰地看出, Python 魔法方法与C++ 重载函数在绝大多数情况下,作用是一致的,都是为了实现类的实例所需要的数值运算 和比较运算。
| 运算符 | Python 魔法方法 | C++ 重载函数 | 适用场景 |
|---|---|---|---|
+ |
__add__(self, other) |
operator+(...) |
加法(如坐标、向量相加) |
- |
__sub__(self, other) |
operator-(...) |
减法 |
* |
__mul__(self, other) |
operator*(...) |
乘法 |
/ |
__truediv__(self, other) |
operator/(...) |
除法 |
== |
__eq__(self, other) |
operator==(...) |
相等判断 |
!= |
__ne__(self, other) |
operator!=(...) |
不等判断 |
< |
__lt__(self, other) |
operator<(...) |
小于判断 |
[] |
__getitem__(self, idx) |
operator[](...) |
下标访问(如自定义容器) |
= |
无(Python 赋值是引用) | operator=(...) |
赋值重载(C++ 特有) |
🔦赋值运算符二者是有点区别的,Python 中 = 没有对应的魔法方法,因为赋值是引用绑定,a = b 只是让 a 指向 b 的内存地址,不涉及对象拷贝。C++ 中 = 必须重载(尤其是管理动态内存的类),否则会导致浅拷贝问题(如指针悬挂、内存泄漏)。
比如说,下面的C++程序:
cpp
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
// 动态内存成员:指针指向堆上的字符串
string* name;
int age;
// 构造函数:分配堆内存
Student(const string& n, int a) : name(new string(n)), age(a) {}
// 析构函数:释放堆内存
~Student() {
delete name; // 释放name指向的内存
cout << "析构函数:释放name内存" << endl;
}
};
int main() {
Student s1("张三", 18);
Student s2 = s1; // 浅拷贝:只复制指针name的值(地址),不复制字符串内容
// 坑1:修改s2的name,s1的name也会变(指向同一内存)
*s2.name = "李四";
cout << *s1.name << endl; // 输出:李四 → 原对象被改
// 坑2:程序结束时,s1和s2的析构函数都会delete同一个name指针 → 双重释放,程序崩溃!
return 0;
}
程序结束时,s2 先析构,delete 了这块内存;接着 s1 析构,又 delete 一次已释放的内存 → 双重释放(但实际上只申请了一块类内存),触发程序崩溃。所以如果C++要用到对象的赋值,需要手动实现深拷贝。关于深拷贝和浅拷贝,这个稍微复杂一些,有时间我再专门写一篇,敬请关注💖。
Python的对象直接赋值是引用,C++也有引用,比方说引用传参(其他引用不是非常常用)。
python
#include <iostream>
using namespace std;
void func_with_ref(int& ref) {
ref += 10; // 语法简洁,无需解引用
}
void func_with_ptr(int* ptr) {
if (ptr != nullptr) { // 必须判空,否则崩溃
*ptr += 10; // 需要解引用
}
}
int main() {
int a = 10;
// 引用:语法简洁,无需判空,更安全
func_with_ref(a);
cout << a << endl; // 输出20
// 指针:需要传地址,必须判空,灵活性高(可指向NULL)
func_with_ptr(&a);
cout << a << endl; // 输出30
return 0;
}
运行,打印输出:

无论是引用传参,还是指针传参,都是对应同一块内存地址进行访问,都实现了变量a在原有的基础上+10。
4. 其他魔法方法
与C++的运算符重载相比,Python的魔法方法还有其他更为丰富的功能。通过下表,我们可以清晰地看到二者的差异。
| 比较项 | C++运算符重载 | Python魔法方法 | 描述 |
|---|---|---|---|
| 算术/比较运算符重载 | ✅ 支持 | ✅ 支持 | 二者都能重载+、==、>等,Python 还细分了__add__/ __radd__(反向加法) |
| 对象生命周期管理 | ❌ 不支持 | ✅ 支持 | Python 的__init__/__del__/__new__管理对象创建 / 销毁 |
| 属性访问控制 | ❌ 需用成员函数 | ✅ 原生支持 | Python 的__getattr__/__setattr__控制.访问,C++ 需手动写 get/set 函数 |
| 容器行为(索引 / 切片) | ✅ 仅[]重载 | ✅ 全面支持 | Python 的__getitem__支持索引、切片、迭代,C++ 仅能重载[]单运算符 |
| 上下文管理(with 语句) | ❌ 无对应机制 | ✅ 支持 | Python 的__enter__/__exit__实现 with 自动资源管理 |
| 迭代器 / 生成器 | ❌ 需手动实现 | ✅ 原生支持 | Python 的__iter__/__next__直接支持迭代,C++ 需手写迭代器类 |
| 字符串表示(print/str) | ❌ 需写 toString | ✅ 支持 | Python 的__str__/__repr__自定义打印格式 |
| 函数调用(对象 ()) | ❌ 不支持 | ✅ 支持 | Python 的__call__让对象像函数一样被调用 |
🎨需要说明以下几点:
- C++的对象生命周期管理是通过构造和析构函数来实现的。
- C++有迭代器,但没有生成器,C++的迭代器并不是通过运算符重载实现的,而是通过一套较为复杂的语法。
4.1 radd反向加法
顾名思义,反向加法与正向加法的反向正好相反。
正向加法:__add__(self, other) 处理 self + other(左边是自定义对象,右边是其他对象/数值);
反向加法:__radd__(self, other) 处理 other + self(左边是其他对象/数值,右边是自定义对象);
看一个实际的例子:
python
class Number:
def __init__(self, val):
self.val = val
# 正向加法:self + other
def __add__(self, other):
if isinstance(other, (Number, int)):
other_val = other.val if isinstance(other, Number) else other
return self.val + other_val
raise TypeError("不支持的加法类型")
# 反向加法:other + self(other是左边的对象,self是右边的Number)
def __radd__(self, other):
# 反向加法的逻辑和正向加法完全一致(因为加法交换律)
# 直接复用__add__即可,不用重复写逻辑
return self.__add__(other)
# 测试1:正向加法(正常)
n1 = Number(5)
print(n1 + 3) # 8
# 测试2:反向加法(now正常)
# 3 + n1 → 先调用3.__add__(n1)失败 → 调用n1.__radd__(3) → 复用__add__逻辑
print(3 + n1) # 8
💥第一个测试,直接打印正向加法的计算结果,没有问题。反向加法的过程就非常有意思了,最先调用3.__add__(n1),但很显然,会返回NotImplemented,紧接着会反向操作,调用n1.__radd__(3)将二者交换位置,而在反向加法的魔法方法中,直接调用了正向加法,这时候就与正向加法一模一样了,只是将参与加法运算的两个数值交换了位置。
4.2 new和init方法
Python的new和init方法,是重点,也是面试最常问到的问题之一。二者类似于C++的构造函数。一般意义上,__new__ + __init__ 才等价于 C++ 构造函数的完整功能。
🌍 实际工程中,几乎见不到__new__ 的影子,这是为什么呢?主要是因为我们在创建类对象的时候,会默认调用__new__ 方法,而这个方法只是创建个"空壳子",并没有什么实际的功能和逻辑需要手动实现,所以就很少出现。
🌍 __init__则不同,是要在创建对象后初始化类属性,必须手动实现,如果默认实现,则根本无法预知业务属性所需的值,以及对象创建初期的一些其他准备工作。
cpp
# 写__init__:对象有定制化属性,有业务意义
class User:
# 手动实现__init__,初始化业务属性
def __init__(self, id, name):
self.id = id
self.name = name
u2 = User(1, "张三")
print(u2.name) # 输出:张三(对象有了实际用途)
4.3 迭代器和生成器
通过上表可以发现,对于类对象,Python原生支持迭代器和生成器,而C++需要手动实现。
🍭先来看C++。
cpp
#include <iostream>
// 包含迭代器相关的基础定义(比如std::iterator)
#include <iterator>
// 自定义类:存储一组固定的数字
class MyNumbers {
private:
int data[5] = {1, 2, 3, 4, 5}; // 内部存储的数组
int size = 5; // 数组大小
public:
// 1. 定义迭代器类型(嵌套在类内部,和Python的__iter__对应)
class Iterator {
private:
int* ptr; // 指向当前元素的指针(迭代器的核心:记录遍历位置)
public:
// 构造函数:初始化指针
Iterator(int* p) : ptr(p) {}
// 迭代器必备:解引用(获取当前元素,对应Python的__next__返回值)
int operator*() const {
return *ptr;
}
// 迭代器必备:自增(移动到下一个元素)
Iterator& operator++() {
ptr++; // 指针后移,模拟迭代
return *this;
}
// 迭代器必备:判断是否遍历结束(和end()比较)
bool operator!=(const Iterator& other) const {
return ptr != other.ptr;
}
};
// 2. 提供begin():返回指向第一个元素的迭代器
Iterator begin() {
return Iterator(data); // 指向数组第一个元素
}
// 3. 提供end():返回指向最后一个元素下一位的迭代器(结束标志)
Iterator end() {
return Iterator(data + size); // 指向数组末尾的下一个位置
}
};
int main() {
// 自定义类对象
MyNumbers nums;
// 像遍历vector一样遍历自定义类对象(底层是我们实现的迭代器)
for (int num : nums) {
std::cout << num << " "; // 输出:1 2 3 4 5
}
return 0;
}
从定义指针到内存访问,几乎全都是手动实现,较为复杂。
🍭再来看python。为方便比较,我们用Python实现相同的功能。
cpp
# Python:通过魔法方法让自定义类支持迭代(对应上面C++的迭代器)
class MyNumbers:
def __init__(self):
self.data = [1,2,3,4,5]
self.index = 0
# 魔法方法__iter__:返回迭代器对象(对应C++的begin())
def __iter__(self):
self.index = 0 # 重置索引
return self
# 魔法方法__next__:返回下一个元素(对应C++迭代器的operator* + operator++)
def __next__(self):
if self.index < len(self.data):
val = self.data[self.index]
self.index += 1
return val
else:
raise StopIteration # 遍历结束(对应C++的operator!=)
# 遍历自定义类对象
nums = MyNumbers()
for num in nums:
print(num, end=" ") # 输出:1 2 3 4 5
一般Python的 __iter_()方法与__next__()方法配合使用,共同完成对象属性的遍历。
4.4 getitem索引和切片
索引和切片是python的一大特色,也是一大亮点,在将类对象当作容易一样进行索引和切片操作的时候,会自动调用__getitem__方法。
python
class MyContainer:
def __init__(self, data):
self.data = data
def __getitem__(self, key):
if isinstance(key, int):
# 自定义索引逻辑:限制索引范围
if key < 0 or key >= len(self.data):
raise IndexError("索引超出范围")
return self.data[key]
elif isinstance(key, slice):
# 自定义切片逻辑:只返回偶数位置的元素
start, stop, step = key.start or 0, key.stop or len(self.data), key.step or 1
result = []
for i in range(start, stop, step):
if i % 2 == 0:
result.append(self.data[i])
return result
else:
raise TypeError("仅支持索引和切片访问")
container = MyContainer([10,20,30,40,50])
print(container[1:4]) # 输出:[30](偶数位置:i=2)
print(container[1:6]) # 输出:[30,50](偶数位置:i=2,4)
其他的一些魔法方法要么过于简单(比方说call方法),要么很少用到,不再赘述!
5. 后记
整体看来,Python的魔法方法比C++的运算符重载内容更加丰富,功能更加多样,除了承担自定义对象常见的数值运算 与比较运算任务外,还囊括了类的生命周期管理,迭代器生成器,函数调用等丰富功能,大大提升了开发的灵活性与便利性。
写这篇文章后半部分的时候,书桌旁的床上,躺着风烛残年,日薄西山的奶奶,我清楚地知道,她的生命进入了倒计时,高中学过的《陈情表》,字字深情,力透纸背,跨越千年仍然与我产生了阵阵共鸣。
一直在想,是什么力量支撑我在成长的道路上风雨无阻,披荆斩棘,或许是苏东坡的乐观豁达,是刘禹锡的碧霄诗情,是范仲淹的逆境重生,抑或是韩愈的不平则鸣,他们都给予了我不同的精神养料。感谢与屏幕前的你以这种方式相遇!