Python 魔法方法 vs C++ 运算符重载全方位深度对比

目录

  • [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个方面的原因:

  1. 💊C++是使用最多和最为广泛的编程语言之一,比较容易让大众接受和理解。

  2. 💊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__让对象像函数一样被调用

🎨需要说明以下几点:

  1. C++的对象生命周期管理是通过构造和析构函数来实现的。
  2. 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++的运算符重载内容更加丰富,功能更加多样,除了承担自定义对象常见的数值运算比较运算任务外,还囊括了类的生命周期管理,迭代器生成器,函数调用等丰富功能,大大提升了开发的灵活性与便利性。

写这篇文章后半部分的时候,书桌旁的床上,躺着风烛残年,日薄西山的奶奶,我清楚地知道,她的生命进入了倒计时,高中学过的《陈情表》,字字深情,力透纸背,跨越千年仍然与我产生了阵阵共鸣。

一直在想,是什么力量支撑我在成长的道路上风雨无阻,披荆斩棘,或许是苏东坡的乐观豁达,是刘禹锡的碧霄诗情,是范仲淹的逆境重生,抑或是韩愈的不平则鸣,他们都给予了我不同的精神养料。感谢与屏幕前的你以这种方式相遇!

相关推荐
csbysj20201 小时前
Java 发送邮件
开发语言
加成BUFF2 小时前
基于DeepSeek+Python开发软件并打包为exe(VSCode+Anaconda Prompt实操)
vscode·python·prompt·conda·anaconda
不吃鱼的猫7482 小时前
【从零手写播放器:FFmpeg 音视频开发实战】04-封装格式与多媒体容器
c++·ffmpeg·音视频
星火开发设计2 小时前
异常规范与自定义异常类的设计
java·开发语言·前端·c++
xyq20242 小时前
SQL Mid() 函数详解
开发语言
小卓(friendhan2005)2 小时前
高并发内存池
c++
52Hz1182 小时前
力扣46.全排列、78.子集、17.电话号码的字母组合
python·leetcode
子午2 小时前
【宠物识别系统】Python+深度学习+人工智能+算法模型+图像识别+TensorFlow+2026计算机毕设项目
人工智能·python·深度学习
好家伙VCC2 小时前
# 发散创新:用Python+Pandas构建高效BI数据清洗流水线在现代数据分析领域,**BI(商业智能)工具的核心竞
java·python·数据分析·pandas