
1. 项目概述为什么我们需要SWIG如果你在C/C和Python之间做过项目大概率遇到过这样的场景核心算法用C写得飞起性能拉满但一到要跟业务逻辑结合或者想快速做个演示界面就不得不面对一个头疼的问题——怎么让Python调用这些C代码手动写Python的C扩展那意味着你要跟Python.h头文件、PyObject*引用计数、模块初始化函数这些底层细节搏斗一个不小心就是内存泄漏或者解释器崩溃调试起来极其痛苦。这就是SWIGSimplified Wrapper and Interface Generator的价值所在。它不是一个新工具但在跨语言集成这个老问题上它依然是最锋利的那把“瑞士军刀”。简单说SWIG是一个编译器它吃进去你的C/C头文件.h吐出来一堆“胶水代码”——这些代码自动帮你处理了两种语言间数据类型转换、内存管理、函数调用等所有繁琐的细节最终生成一个可以直接被Pythonimport的模块。你不再需要手动编写那些脆弱的包装代码可以把精力完全集中在核心逻辑上。我最近的一个项目里有一个用C17写的实时信号处理库计算密集对延迟极其敏感。业务端需要用Flask快速搭建一个Web API来接收数据并调用这个库。如果没有SWIG我可能要多花两周时间去啃CPython的C API文档还得担心包装层的性能损耗。用了SWIG我一下午就搞定了接口生成和初步测试剩下的时间全用来优化业务逻辑和做压力测试了。这种效率提升在追求快速迭代的项目里是决定性的。所以无论你是想给遗留的C/C库赋予Python的生命力还是在新项目中用C写核心引擎、用Python做粘合剂和上层应用SWIG都能让你事半功倍。这篇教程我就结合自己踩过的坑和积累的经验带你从零开始彻底掌握用SWIG搭建C/C与Python之间的桥梁。2. 环境准备与SWIG工作流全景工欲善其事必先利其器。在开始写第一行接口定义之前我们需要把环境和整个工作流搞清楚。SWIG不是魔法它有自己的“输入-处理-输出”流程理解这个流程是避免后续混乱的关键。2.1 核心工具链安装SWIG本身只是一个代码生成器它需要本地有C/C编译器和Python开发环境才能工作。1. 安装SWIGmacOS:最简单的方式是使用Homebrewbrew install swig。Linux (Ubuntu/Debian):sudo apt-get install swigWindows:建议使用MSYS2或从SWIG官网下载预编译的二进制文件并将其路径添加到系统环境变量PATH中。用MSYS2的话命令是pacman -S mingw-w64-x86_64-swig。安装后在终端运行swig -version确认安装成功。2. 确保Python开发环境你需要的不只是Python解释器还有Python的开发头文件和库。这是编译扩展模块所必需的。如果你从python.org下载安装的通常已经包含。在Linux上你可能需要安装python3-dev或python-devel包例如sudo apt-get install python3-dev。可以通过检查是否存在Python.h头文件来验证find /usr -name Python.h 2/dev/null。3. C/C编译器Linux/macOS: GCC 或 Clang通常已安装。Windows: 推荐使用Visual Studio的MSVC编译器或者MinGW-w64。如果你打算生成能在Visual Studio中使用的项目文件MSVC是必须的。一个关键的心得我强烈建议在开发初期就固定好你的Python版本和编译器版本。特别是Windows下用哪个版本的Visual Studio编译的扩展通常只能被对应版本的Python如基于VC14构建的Python 3.8导入。版本不匹配会导致ImportError: DLL load failed这种令人沮丧的错误。在团队协作中用一份requirements.txt或environment.yml来锁定Python版本和关键库版本能省去无数麻烦。2.2 理解SWIG的核心工作流很多新手一开始就被SWIG生成的一堆文件搞懵了。我们先把它的工作流程画清楚用文字描述你的C/C源代码 (.cpp, .c) 和 头文件 (.h) | v SWIG接口文件 (.i 或 .swg) | ---[SWIG工具]--- | | v v Python包装代码 (.py) C/C包装代码 (.cxx 或 _wrap.c) | | --------------- | v C/C编译器 (如 g, cl) | v 动态链接库 (.so, .dll, .pyd) | v Python: import your_module流程详解编写接口文件 (.i):这是你与SWIG对话的“说明书”。你不需要在这里重写所有C代码而是告诉SWIG“我的example.h头文件里这些类、这些函数、这些变量是我想要暴露给Python的。” SWIG会去解析这个头文件。运行SWIG生成包装代码执行命令如swig -python -c example.i。这个命令会做两件事生成一个example_wrap.cxxC或example_wrap.cC文件。这是一大坨“胶水代码”里面充满了将Python类型如PyObject*转换为C/C类型如intstd::string 或自定义类指针的逻辑。生成一个example.py文件。这个Python文件才是你最终import的对象。它内部会去导入编译好的原生模块通常叫_example并提供一个更符合Python习惯的接口。编译包装代码为原生模块这是最易出错的一步。你需要将生成的example_wrap.cxx和你原始的example.cpp一起编译成一个Python可以导入的共享库。在Linux/macOS上后缀是.so在Windows上通常是.pyd本质也是DLL。这个模块的名字有约定比如_example.cpython-38-x86_64-linux-gnu.so。在Python中导入使用现在你可以import example了。example.py会自动找到对应的_example原生模块让你像使用普通Python模块一样调用背后的C/C函数和类。一个重要的认知你最终import的是SWIG生成的.py文件而不是直接import那个.so或.pyd。那个.py文件是面向用户的、经过包装的接口而原生的动态库是底层实现。这种设计提供了额外的灵活性你可以在.py文件中添加纯Python的辅助函数或进行额外的错误处理。3. 从零开始你的第一个SWIG项目理论说再多不如动手做一遍。我们从一个最简单的例子开始用C实现一个计算斐波那契数列的函数然后通过SWIG让Python调用它。3.1 创建项目结构与源代码首先创建一个干净的项目目录比如swig_demo。1. C头文件 (fibonacci.h):// fibonacci.h #ifndef FIBONACCI_H #define FIBONACCI_H class FibonacciCalculator { public: FibonacciCalculator(); ~FibonacciCalculator(); // 计算第n个斐波那契数 long long calculate(int n); // 获取最后一次计算的时间纳秒用于简单性能演示 long long getLastComputeTime() const; private: long long lastComputeTime; }; #endif这个头文件定义了一个简单的类包含计算函数和一个记录时间的成员。2. C实现文件 (fibonacci.cpp):// fibonacci.cpp #include fibonacci.h #include chrono FibonacciCalculator::FibonacciCalculator() : lastComputeTime(0) {} FibonacciCalculator::~FibonacciCalculator() default; long long FibonacciCalculator::calculate(int n) { auto start std::chrono::high_resolution_clock::now(); if (n 1) { lastComputeTime 0; return n; } long long a 0, b 1, c; for (int i 2; i n; i) { c a b; a b; b c; } auto end std::chrono::high_resolution_clock::now(); lastComputeTime std::chrono::duration_caststd::chrono::nanoseconds(end - start).count(); return b; } long long FibonacciCalculator::getLastComputeTime() const { return lastComputeTime; }这里用循环而非递归实现避免递归在C中可能导致的栈溢出并且更符合高性能场景的需求。同时我们记录了计算耗时。3.2 编写SWIG接口定义文件这是SWIG项目的核心。在项目根目录创建fibonacci.i。// fibonacci.i - SWIG接口文件 %module fibonacci %{ // 这部分代码会原封不动地插入到SWIG生成的包装代码_wrap.cxx的开头 // 通常用于包含必要的头文件 #include fibonacci.h %} // 告诉SWIG解析这个头文件并为其中的所有声明生成包装代码 %include fibonacci.h这个.i文件简单到极致%module fibonacci 指定生成的Python模块名为fibonacci。%{ ... %} 里面的代码会直接复制到生成的C包装代码里。这里我们包含了原始头文件确保包装代码能编译。%include fibonacci.h 这是最关键的一行。它指示SWIG去读取并解析fibonacci.h文件然后为里面所有它能理解的内容类、函数、变量等生成Python接口。注意这里有一个初学者常犯的错误在.i文件中使用#include而不是%include。#include只是C/C预处理器指令SWIG不会去解析它包含的文件内容。而%include是SWIG指令意味着“请解析这个文件并为其生成包装器”。务必分清。3.3 生成包装代码与编译现在进入实操环节。我们将演示两种最常用的编译方式手动命令行编译和使用setuptools自动化编译。后者更贴近真实项目。方式一手动命令行编译理解原理在项目目录下打开终端。步骤1运行SWIG生成包装代码swig -c -python fibonacci.i执行后你会看到生成了两个新文件fibonacci_wrap.cxx: 巨大的C包装源文件。fibonacci.py: Python端的接口模块。步骤2编译C代码生成动态库编译命令因平台而异。你需要知道你的Python包含路径和库路径。在Python交互环境中可以获取import sysconfig print(sysconfig.get_path(include)) # 头文件路径 print(sysconfig.get_path(platlib)) # 库文件路径通常放编译好的模块Linux/macOS 示例 (使用g):g -O2 -fPIC -c fibonacci.cpp g -O2 -fPIC -c fibonacci_wrap.cxx -I/usr/include/python3.8 # 请替换为你的Python include路径 g -shared fibonacci.o fibonacci_wrap.o -o _fibonacci.so -lpython3.8 # 链接生成共享库Windows 示例 (使用MSVC 假设在VS开发人员命令提示符下):cl /c /EHsc /MD /I C:\Python38\include fibonacci.cpp fibonacci_wrap.cxx link /DLL /OUT:_fibonacci.pyd /LIBPATH:C:\Python38\libs fibonacci.obj fibonacci_wrap.obj python38.lib编译成功后会生成_fibonacci.so(Linux/macOS) 或_fibonacci.pyd(Windows)。步骤3测试确保fibonacci.py和_fibonacci.so(或.pyd) 在同一个目录下然后在Python中测试import fibonacci calc fibonacci.FibonacciCalculator() result calc.calculate(50) print(fThe 50th Fibonacci number is: {result}) print(fComputation took {calc.getLastComputeTime()} nanoseconds)方式二使用setup.py自动化编译推荐手动编译太麻烦且不易移植。Python的setuptools可以完美地自动化这个过程。创建setup.py文件# setup.py from setuptools import setup, Extension # 定义扩展模块 fibonacci_module Extension( _fibonacci, # 注意下划线开头这是原生模块的名字必须和.i文件中的%module名对应但加了下划线 sources[fibonacci.cpp, fibonacci_wrap.cxx], # 所有需要编译的源文件 include_dirs[], # 额外的头文件搜索路径如果需要 languagec, extra_compile_args[-stdc11, -O3], # 额外的编译参数如C标准、优化级别 ) setup( namefibonacci, version1.0, authorYour Name, descriptionA simple Fibonacci calculator using SWIG, ext_modules[fibonacci_module], # 要编译的扩展模块列表 py_modules[fibonacci], # 纯Python模块列表这里指SWIG生成的fibonacci.py )然后在项目目录下执行python setup.py build_ext --inplace--inplace参数会将编译好的扩展模块_fibonacci.so直接生成在当前目录方便测试。这条命令会自动处理所有编译和链接的细节包括找到正确的Python头文件和库。这是最省心、最专业的方式。一个重要的避坑技巧在setup.py中Extension的第一个参数模块名必须是_fibonacci下划线模块名而py_modules里列出的纯Python模块名是fibonacci。这对应了SWIG生成的两个文件_fibonacci是编译后的二进制模块fibonacci是导入它的Python包装器。名字必须严格对应否则导入时会失败。4. 进阶配置处理复杂数据类型与内存管理第一个例子展示了基本流程但现实世界的C/C代码要复杂得多我们有STL容器std::vector,std::map、自定义结构体、指针、数组、内存所有权问题等等。SWIG通过一个强大的概念——“类型映射Typemaps”和内置的“库文件”来处理这些。4.1 使用SWIG库处理STL容器默认情况下SWIG不知道std::vectorint在Python里应该长什么样。你需要显式地告诉它。幸运的是SWIG为常用的STL容器提供了现成的接口库。修改你的fibonacci.i文件// fibonacci.i %module fibonacci // 在包含头文件之前先引入STL类型映射库 %include std_vector.i %include std_string.i // 如果需要处理std::string // 实例化模板。这告诉SWIG请为 std::vectorint 和 std::vectordouble 生成Python接口。 namespace std { %template(IntVector) vectorint; %template(DoubleVector) vectordouble; } %{ #include fibonacci.h #include vector #include string %} %include fibonacci.h现在假设我们在C头文件中添加一个返回std::vectorint的方法// fibonacci.h 新增 std::vectorint calculateSequence(int start, int end);在C中实现它。之后在Python中你就可以这样使用import fibonacci calc fibonacci.FibonacciCalculator() seq calc.calculateSequence(5, 10) # 返回一个Python列表不是一个特殊对象 print(seq) # 可能显示类似 fibonacci.IntVector; proxy ... print(list(seq)) # 可以转换为Python列表: [5, 8, 13, 21, 34] # 你也可以像列表一样索引和迭代 for num in seq: print(num) print(seq[2]) # 13SWIG生成的IntVector对象在Python中表现得像一个序列支持迭代、索引、len()但其底层是C的std::vector在容器很大时避免了数据在两种语言间复制带来的开销。这是SWIG处理复杂类型的一个巨大优势。注意%template(IntVector)中的IntVector是你在Python中使用的类型名你可以自由命名但最好能见名知义。std_vector.i库文件里定义了如何将std::vector的方法如push_back,size映射到Python操作。4.2 处理指针、数组与内存所有权这是跨语言调用中最棘手的问题之一。当C函数返回一个指向内部数据的指针或者接受一个指针作为输出参数时谁来负责分配和释放内存场景一返回指向内部数组的指针// 不推荐的做法 const double* getInternalData() { static double data[] {1.1, 2.2, 3.3}; return data; // SWIG会将其包装但Python端如何管理这个指针的生命周期 }对于这种情况更好的做法是让C代码管理内存并通过SWIG的%newobject指令告诉Python解释器“这个函数返回了一个新分配的对象你需要负责在适当的时候删除它。”但更安全、更Pythonic的方式是避免直接暴露指针而是返回一个拷贝如std::vector或使用SWIG的“carrays.i”库来包装数组。场景二输出参数指针或引用bool parseString(const std::string input, int outValue, std::string outError);C常用引用或指针作为输出参数。SWIG能很好地处理这个。在Python端这个函数会返回一个元组success, value, error_msg parseString(123) if success: print(fParsed value: {value}) else: print(fError: {error_msg})SWIG自动将输出参数转换为返回值的多个部分。这是非常方便的特性。一个关键的经验内存所有权必须清晰。一个黄金法则是让分配内存的一方也负责释放内存。如果C函数用new分配了内存并返回指针那么应该提供一个对应的delete函数让Python调用或者使用智能指针如std::shared_ptrSWIG对智能指针有很好的支持通过%include std_shared_ptr.i。尽量避免在Python代码中直接面对裸指针。4.3 自定义类型映射Typemaps应对特殊场景有时内置的转换规则不够用。比如你的C函数接受一个FILE*或者一个自定义的枚举标志位你想在Python中用不同的类型比如Python的io.FileIO对象或字符串来传递。这时就需要自定义类型映射。类型映射是SWIG最强大也最复杂的特性之一。它本质上是一套规则告诉SWIG“当在C/C和Python之间传递这个特定类型时请按我写的代码来转换。”一个简单的例子假设我们有一个C函数它接受一个int数组和其长度。我们希望在Python中只传递一个列表。// mylib.h int sum_array(int* arr, int len);SWIG接口文件可以这样写// mylib.i %module mylib %include typemaps.i // 包含一些预定义的typemap工具 %apply int* INPUT { int* arr }; // 应用预定义的INPUT typemap将Python列表转换为int数组 %apply int INPUT { int len }; // 通常长度可以从列表获取这里我们假设由Python提供 %{ int sum_array(int* arr, int len); %} int sum_array(int* arr, int len);%apply指令应用了一个预定义的 typemap。INPUTtypemap 告诉SWIG这个参数是输入参数请从Python对象列表中提取数据转换为C数组并临时分配内存函数调用结束后释放。在Python中调用就变得非常简洁import mylib result mylib.sum_array([1, 2, 3, 4, 5], 5)实际上更高级的做法是使用%typemap(in)直接编写转换代码并自动从Python列表推导出长度。这需要更深入的SWIG知识。对于初学者建议先尝试使用内置的STL容器或carrays.i、cdata.i库来避免直接操作指针和数组。5. 构建与调试实战打造健壮的跨语言模块掌握了基本和进阶用法后我们需要关注如何将SWIG集成到现代开发流程中以及如何调试这个“黑盒”般的跨语言调用。5.1 集成CMake实现跨平台构建对于稍大的项目手动写setup.py或者一堆编译命令很难维护尤其是需要支持多个平台和配置时。CMake是目前C/C项目构建的事实标准它可以和SWIG很好地集成。创建一个CMakeLists.txt文件cmake_minimum_required(VERSION 3.12) project(FibonacciSwig LANGUAGES CXX) # 查找Python和SWIG find_package(Python3 COMPONENTS Interpreter Development REQUIRED) find_package(SWIG REQUIRED) include(${SWIG_USE_FILE}) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 添加头文件搜索路径 include_directories(${Python3_INCLUDE_DIRS}) # 设置源文件 set(SRC_FILES fibonacci.cpp) set(SWIG_INTERFACE_FILE fibonacci.i) # 使用SWIG添加模块 swig_add_module(fibonacci python ${SWIG_INTERFACE_FILE} ${SRC_FILES}) swig_link_libraries(fibonacci ${Python3_LIBRARIES}) # 设置输出目录让编译好的模块和生成的.py文件在一起 set_target_properties(${SWIG_MODULE_fibonacci_REAL_NAME} PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} SUFFIX .so ) # 将生成的.py文件也复制到输出目录 add_custom_command(TARGET ${SWIG_MODULE_fibonacci_REAL_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/fibonacci.py ${CMAKE_CURRENT_BINARY_DIR}/ )使用CMake构建mkdir build cd build cmake .. -DPYTHON_EXECUTABLE$(which python3) # 指定Python解释器 make构建完成后在build目录下你会找到_fibonacci.so和fibonacci.py。CMake自动处理了所有依赖和平台差异是大型项目的首选。5.2 调试技巧当Python调用C崩溃时跨语言调试是痛苦的。一个Python的segmentation fault错误通常意味着C/C代码中发生了内存错误。以下是几个实用的调试策略1. 启用符号调试信息在编译时无论是setup.py还是CMake确保添加调试标志-gfor GCC/Clang,/Zifor MSVC。这样崩溃时才能看到有意义的堆栈跟踪。2. 在C代码中使用打印语句这是最朴素但有效的方法。在怀疑出问题的C函数开头和结尾添加std::cerr或printf输出。确保输出刷新std::endl或\n。在Python中调用时观察这些打印是否出现可以定位崩溃发生的位置。3. 使用Python的faulthandler模块在Python脚本开头添加import faulthandler faulthandler.enable()当程序发生段错误时faulthandler会尝试将C级别的堆栈跟踪打印到标准错误流。这能告诉你崩溃发生在哪个C/C函数里。4. 使用GDB/LLDB调试Python进程这是最强大的方法。你可以像调试普通C程序一样调试嵌入了Python解释器的进程。# Linux/macOS 使用 gdb/lldb gdb --args python your_script.py # 在gdb中运行 (gdb) run # 当崩溃发生时使用 backtrace (bt) 查看堆栈 (gdb) bt # 或者使用lldb lldb -- python your_script.py (lldb) run (lldb) bt关键步骤你需要确保Python解释器和你的扩展模块都带有调试符号。对于系统Python可能需要安装python3-dbg之类的调试包。5. 检查SWIG生成的包装代码有时问题出在SWIG的类型转换上。查看生成的*_wrap.cxx文件找到对应你Python调用的那个函数的包装函数。仔细检查输入参数是如何从PyObject*转换为你C函数期望的类型的。一个常见的错误是SWIG误解了函数重载或默认参数。一个真实的踩坑案例我曾遇到一个崩溃Python调用一个返回std::string的C函数时总是段错误。最后发现是C函数返回了一个指向局部变量的引用const std::string get_name()但内部返回了局部变量std::string的引用。在C中这是未定义行为但可能偶尔工作。当SWIG尝试使用这个已经销毁的字符串时就崩溃了。解决方法是将返回值改为按值返回std::string。教训确保你的C代码本身是健壮和正确的SWIG不会修复你代码中的bug。5.3 性能考量与最佳实践使用SWIG会引入一定的调用开销因为每次函数调用都需要经过参数转换和代理层。但对于大多数场景尤其是计算密集的逻辑在C侧完成单次调用执行大量工作的情况下这点开销微不足道。性能优化建议减少跨界调用次数避免在紧密循环中频繁调用简单的C函数。应该设计一个接口一次调用完成更多工作。例如不要用Python循环调用一万次add_one(int)而应该提供一个add_one_to_array(std::vectorint)函数。使用numpy.i进行数组操作如果你的C代码大量处理数值数组并且Python端使用NumPy那么SWIG的numpy.i库是神器。它允许你在C中直接访问NumPy数组的内存缓冲区无需复制数据性能极高。注意字符串转换std::string和 Pythonstr之间的转换涉及内存分配和编码特别是处理中文等非ASCII字符时。对于频繁传递的字符串考虑使用字节串bytes或检查是否有不必要的转换。剖析性能瓶颈使用Python的cProfile模块和系统的性能分析工具如perfon Linux, Instruments on macOS来确定时间到底花在了Python层、SWIG转换层还是C计算层。6. 常见问题排查与解决方案速查即使按照教程操作你也可能会遇到各种问题。这里汇总了一些最常见的问题及其解决方法。问题现象可能原因解决方案ImportError: dynamic module does not define module export function (PyInit_xxx)1. 编译的扩展模块名与Python期望的不符。2. 使用了错误的Python版本如用Python3.7编译用Python3.8导入。3. C代码编译时缺少-DPy_LIMITED_API等宏定义不常见。1. 确保setup.py中Extension的名字是_模块名且与.i文件中%module后的名字对应前面加下划线。2. 使用python setup.py build_ext --inplace重新编译确保编译环境和运行环境Python版本一致。清理旧的.so/.pyd文件。ImportError: DLL load failed: The specified module could not be found.(Windows)扩展模块依赖的运行时库如MSVCRxxx.dll未找到。1. 确保使用相同版本的Visual Studio编译器和Python如都是VS2019构建的。2. 可以将必要的VC运行时库如vcruntime140.dll复制到模块所在目录或系统PATH路径。AttributeError: module object has no attribute xxxSWIG未能成功包装你想要的函数或类。1. 检查.i文件中的%include指令是否包含了正确的头文件。2. 检查C头文件中的函数/类声明是否有拼写错误或者被#ifdef宏包裹了。3. 运行SWIG时查看是否有警告信息。TypeError: in method xxx, argument y of type zPython传递的参数类型与C函数期望的不匹配。1. 检查Python调用时传递的参数类型和数量。2. 检查SWIG是否为该类型特别是自定义类型或复杂模板生成了正确的包装。可能需要额外的%include或%template指令。3. 对于指针或数组确保传递的是正确的对象。程序运行中随机崩溃 (Segmentation Fault)内存管理问题如悬垂指针、访问已释放内存、缓冲区溢出等。1. 使用调试器GDB/LLDB捕捉崩溃现场查看堆栈。2. 检查C代码的内存管理确保所有权清晰。3. 检查SWIG是否正确地管理了对象的生命周期。对于返回新对象指针的函数考虑使用%newobject。SWIG编译警告Nothing known about std::shared_ptr...SWIG没有包含智能指针的支持库。在.i文件中添加%include std_shared_ptr.i并对你的类使用%shared_ptr(YourClassName)指令。Python中无法继承C类默认情况下SWIG生成的代理类在Python中是不可直接继承的。在.i文件中对需要被Python继承的类使用%feature(director)指令。注意这会增加复杂性和开销。编译错误Python.h: No such file or directory编译器找不到Python开发头文件。1. 确保已安装python3-dev或类似包。2. 在编译命令或setup.py/CMakeLists.txt中正确指定头文件路径-I/path/to/python/include。最后再分享一个维护上的小技巧将SWIG接口文件.i视为项目的公共API契约。尽量保持接口的稳定和简洁。如果C内部实现变了但接口不变只需要重新编译扩展模块Python代码无需改动。反之如果接口需要变更要做好版本管理和向后兼容。在.i文件中使用%ignore指令可以隐藏不打算暴露给Python的内部方法保持接口的清晰。