☰
Python魔法方法全解析:从__init__到运算符重载的实践指南
2026/10/22 11:56:15 网站建设 项目流程

聊到Python,很多人迟早会接触到一类看起来有点“另类”的方法——名字前后各带两个下划线,比如__init__、__str__、__len__。这门语言里管它们叫魔法方法(Magic Methods),也叫双下划线方法、特殊方法(Special Methods)。说“魔法”有点玄乎,但本质并不神秘:这些方法不是让你在日常编码里手动调用的,而是解释器在特定场景下会自动触发的行为钩子。比如你写len(obj),Python背后实际去找的就是obj.__len__();你用for循环遍历一个自定义对象,背后靠的也是对象实现的一套迭代协议。

这篇文章就是围绕魔法方法做一个从零到进阶的完整梳理。对刚接触类的新手来说,你会知道__init__之外还有哪些真正影响对象行为的“幕后开关”;对有经验的开发者来说,这里整理了一些平时容易踩坑的细节:__del__的真实执行时机、__hash__和__eq__的连带关系、__getattr__为什么会无限递归、反向运算符到底什么时候被调用。整篇文章按照“对象生命周期 → 展示表示 → 运算符重载 → 容器与迭代 → 上下文管理 → 属性访问与可调用对象 → 问题排查”这条线展开,每一步都有可直接复现的代码,希望能帮你把类写得顺手点、优雅点。

1. 先搞清楚魔法方法到底怎么回事

1.1 两个下划线“包裹”的约定

魔法方法的写法是固定的:方法名前后各有一对双下划线,比如__init__,所以也被称为dunder方法(double underscore)。这里要纠正一个容易产生的误区:不是所有双下划线开头的方法都是魔法方法。_private这种单下划线开头是“约定私有”,__name__这种在类内部还会触发命名修饰(name mangling),本意是为了避免子类意外覆盖。真正起特殊作用的,是Python语言规范里明确列出的那一批内置特殊方法。

为什么解释器能自动调用它们?因为Python对象的几乎所有基础行为,都定义在这套特殊方法的协议之上。obj == other不是简单的内存地址比较,它先尝试obj.__eq__(other);print(obj)会去找obj.__str__()或者obj.__repr__();obj[key]会去找obj.__getitem__(key)。你可以把这些方法理解为Python给类预留的“插槽”(slot),类实现了什么插槽,就拥有了对应能力。

1.2 从“调用方法”到“协议实现”

普通方法是你主动调用的:obj.方法名()。魔法方法恰恰相反,大多数情况下由Python解释器替你调用。举个例子:

class Point: def __init__(self, x, y): self.x = x self.y = y p = Point(3, 4) print(p)

这里__init__在Point(3, 4)执行的那一刻被自动触发,print(p)又会自动查找__str__。如果你没有实现__str__,输出就是类似<__main__.Point object at 0x7f...>这种默认结果。这个默认输出来自object基类的__repr__实现。

理解“协议”这个角度非常重要。Python强调鸭子类型——不关心对象具体属于哪个类,只看它是否实现了对应的行为协议。你写一个函数要求入参可迭代,那它只要实现了__iter__或__getitem__就行,不必强制继承某个迭代器类。魔术方法的本质,就是让你自定义的类也能融入Python原生语法,获得和内建类型同等待遇。

1.3 学习策略:按场景记,别按清单背

魔法方法加起来有几十个,一次全记下来不现实,也不必要。我建议按“使用场景”来分组掌握:

  • 对象怎么创建、怎么销毁:__new__、__init__、__del__
  • 对象怎么描述自己:__repr__、__str__、__bool__、__format__
  • 对象怎么参与运算:__add__、__eq__、__lt__、__hash__等
  • 对象怎么当容器用:__len__、__getitem__、__setitem__、__iter__、__contains__
  • 对象怎么管理资源:__enter__、__exit__
  • 对象怎么访问属性、能不能被调用:__getattr__、__setattr__、__call__

下面逐个展开,每个场景我都会给出能跑通的示例代码和对应的坑。

2. 对象的生命周期:不只是__init__

2.1 __init__和__new__的配合关系

你可能已经熟悉__init__——创建实例时自动执行的初始化方法。但很多人不知道,在__init__之前还有一步__new__。__new__是一个类方法,负责真正创建并返回实例;__init__拿到这个实例后负责初始化属性。两者的调用顺序是固定的:先__new__,再__init__。

class Demo: def __new__(cls, *args, **kwargs): print("1. 进入 __new__") instance = super().__new__(cls) print("2. 实例创建完成", instance) return instance def __init__(self, value): print("3. 进入 __init__") self.value = value d = Demo(10)

输出顺序就是1、2、3。注意一个关键点:__new__必须返回实例,否则__init__不会被执行。平时我们写类基本不需要动__new__,但在两种情况下它很有用:一是继承不可变类型(如int、tuple、str)时,因为这类对象在__new__这一步就定型了,__init__没法再改;二是想控制实例创建过程,比如做单例、实例缓存、对象池。

2.2 用__new__实现一个简单单例

单例模式的意思是:整个程序运行期间,这个类只能有一个实例。实现方式有不少,用__new__是比较经典的一种:

class Config: _instance = None def __new__(cls, *args, **kwargs): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __init__(self, env="dev"): # 注意:__init__每次调用都会执行,需要用其他方式保证初始化只做一次 if not hasattr(self, "_initialized"): self.env = env self._initialized = True c1 = Config("prod") c2 = Config("test") print(c1 is c2) # True print(c1.env) # prod print(c2.env) # prod

这里有个容易踩的坑:即使__new__每次都返回同一个实例,__init__依然会被反复调用。所以我在__init__里加了一个_initialized标志,确保只在第一次创建时写入初始配置。这是“用__new__做单例”最容易被忽略的细节。实际工程里我更推荐用元类或者模块级全局变量的方式做单例,但对于学习__new__来说,这个例子足够直观。

2.3 __del__为什么“靠不住”

__del__是析构方法,理论上在对象被垃圾回收前触发。但Python的垃圾回收基于引用计数,一旦对象的引用计数归零就会立即回收,而不像Java那样有不可预测的GC周期。听起来挺“及时”,问题在于你没法精确控制引用何时归零:局部变量提前被重新赋值、容器被整体释放、循环引用导致延迟回收,都会让__del__的触发时机变得很尴尬。

class FileHandler: def __del__(self): print("__del__ 被调用") def make_file(): f = FileHandler() return f f1 = make_file() f2 = f1 del f1 print("del之后,仍然有变量持有引用,所以__del__不会立刻执行")

我的建议是:不要依赖__del__做必须的资源清理,比如关闭文件、释放锁、断开数据库连接。资源释放应当用with语句或显式的close()方法,这个后面讲上下文管理器时会详细说。__del__可以拿来打印调试日志、做统计计数,但千万别把它当成“保证执行”的兜底逻辑。

3. 让对象“说人话”:repr__和__str

3.1 两个方法,一个给用户看,一个给开发者看

新手最容易混淆的就是__str__和__repr__。简单记住两条规则:

  • __str__是给人看的,追求可读性,print(obj)、str(obj)、f"{obj}"会用到它。
  • __repr__是给开发者看的,追求“无歧义”,最好能直接还原出对象本身,交互式命令行里直接输入变量并按回车,控制台显示的也是__repr__的返回值。

如果类只实现了__repr__没实现__str__,那么print会退而求其次调用__repr__。反过来只实现__str__,交互式环境则不会用它。所以实践上有个建议:__repr__尽量实现,最好能让eval(repr(obj))重建这个对象;__str__按需实现,只要可读即可。

class Book: def __init__(self, title, price): self.title = title self.price = price def __repr__(self): return f"Book({self.title!r}, {self.price!r})" def __str__(self): return f"《{self.title}》售价{self.price}元" b = Book("Python魔法方法", 59.0) print(repr(b)) # Book('Python魔法方法', 59.0) print(str(b)) # 《Python魔法方法》售价59.0元 print(b) # 《Python魔法方法》售价59.0元

{self.title!r}里的!r表示用repr()处理这个字段,这样字符串会带上引号,方便开发者一眼看出类型。

3.2 __bool__决定“真假”

每个对象在布尔上下文中都有真假值。默认情况下,自定义类的实例都是真,但只要实现了__bool__或__len__,真值判断就会改变。规则是这样的:if obj先找__bool__,找不到就找__len__,len(obj) == 0则为假;两个都没有,一律视为真。

class Task: def __init__(self, items): self.items = items def __len__(self): return len(self.items) def __bool__(self): return any(self.items) t1 = Task([]) t2 = Task([0, 1, -1]) print(bool(t1)) # False print(bool(t2)) # True

有个容易被忽略的细节:如果同时实现了__bool__和__len__,__bool__优先。逻辑上两者语义应当尽量一致,否则会出现bool(obj)和len(obj) > 0结论相反的尴尬情况。我建议要么只实现__len__让真值跟随元素数量,要么实现__bool__时保证它和业务含义一致。

4. 运算符重载实战:一个手写向量类

4.1 实现加减乘除和比较

运算符重载是魔法方法最直观的价值。你写v + w时,Python实际调用v.__add__(w);如果v没实现或返回了NotImplemented,再尝试反向调用w.__radd__(v)。下面用二维向量来演示:

class Vector: def __init__(self, x, y): self.x = x self.y = y def __repr__(self): return f"Vector({self.x!r}, {self.y!r})" def __add__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x + other.x, self.y + other.y) def __sub__(self, other): if not isinstance(other, Vector): return NotImplemented return Vector(self.x - other.x, self.y - other.y) def __mul__(self, scalar): if not isinstance(scalar, (int, float)): return NotImplemented return Vector(self.x * scalar, self.y * scalar) def __eq__(self, other): if not isinstance(other, Vector): return NotImplemented return (self.x, self.y) == (other.x, other.y) def __lt__(self, other): if not isinstance(other, Vector): return NotImplemented return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2) def __hash__(self): return hash((self.x, self.y))

这里有几个关键点:

  • 类型不匹配时返回NotImplemented,而不是抛出异常。解释器拿到NotImplemented后会尝试对方的反射方法(__radd__、__rsub__等),两边都处理不了才抛TypeError。
  • __eq__和__hash__必须成对出现。如果你定义了__eq__而不写__hash__,这个类的实例会变成不可哈希的,不能再放进set或当字典键。原因在于Python的哈希契约:相等的对象必须有相等的哈希值,禁止__hash__是为了防止你破坏这条规则。
  • 通过functools.total_ordering,你只需要实现__lt__和__eq__,就能自动获得__le__、__gt__、__ge__,省去重复代码。
from functools import total_ordering @total_ordering class VectorOrdered: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, VectorOrdered): return NotImplemented return (self.x, self.y) == (other.x, other.y) def __lt__(self, other): if not isinstance(other, VectorOrdered): return NotImplemented return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2) v1 = VectorOrdered(1, 1) v2 = VectorOrdered(3, 0) print(v1 <= v2) # True print(v2 > v1) # True

4.2 反向运算和就地运算

反过来看反向运算:5 * v这个表达式,int.__mul__不会处理Vector,于是Python尝试v.__rmul__(5)。所以如果你希望支持“数字在左边乘”,需要补一个__rmul__:

class Vector: # ... 省略其他方法 ... def __rmul__(self, scalar): return self.__mul__(scalar) v = Vector(2, 3) print(5 * v) # Vector(10, 15)

还有一个和赋值相关的魔法方法:__iadd__(就地加法)。区别在于:a += b时,如果没有__iadd__,Python会退化为a = a.__add__(b),也就是生成新对象再赋值;如果实现了__iadd__,你可以在原对象上修改。实现时要注意语义:

class Cart: def __init__(self): self.items = [] def __iadd__(self, other): self.items.append(other) return self cart = Cart() cart += "Python书" cart += "鼠标" print(cart.items) # ['Python书', '鼠标']

关键点在于:__iadd__必须返回self,否则+=的结果会变成返回值而不是原对象。这个坑是“可修改对象建议实现__iadd__,不可变对象不要实现”的实践来源。

4.3 为什么索引、切片的魔法方法也能“重载”

除开算术运算符,方括号也是运算符。obj[key]对应__getitem__,obj[key] = value对应__setitem__,del obj[key]对应__delitem__。如果你想实现一个支持负索引、切片、in判断的自定义序列,靠这几个方法就够了:

class Playlist: def __init__(self, tracks): self.tracks = list(tracks) def __getitem__(self, index): return self.tracks[index] def __setitem__(self, index, track): self.tracks[index] = track def __len__(self): return len(self.tracks) def __contains__(self, track): return track in self.tracks pl = Playlist(["A", "B", "C"]) print(pl[-1]) # C,负索引自动支持,因为列表本身支持 print(pl[1:]) # ['B', 'C'],切片也直接透传 print("B" in pl) # True,依赖__contains__ print(len(pl)) # 3

实现__getitem__之后,这个对象自动获得可迭代能力:Python兼容旧式的迭代协议,如果发现对象有__getitem__但没有__iter__,它会从索引0开始依次调用__getitem__(0)、__getitem__(1),直到遇到IndexError。这种“隐式迭代”对写惯了for item in obj的人来说非常方便,但最好还是显式实现__iter__,语义更清晰,性能也更好。

5. 迭代协议:让对象用上for循环

5.1 __iter__和__next__配合

迭代器协议由两个方法组成:__iter__返回迭代器自身,__next__返回下一个值,没有更多值时抛出StopIteration。自定义类时最常见的模式是让对象既是可迭代的,又自身是迭代器:

class Countdown: def __init__(self, start): self.start = start self.current = start def __iter__(self): # 每次for循环都重置,保证可以重复迭代 self.current = self.start return self def __next__(self): if self.current <= 0: raise StopIteration value = self.current self.current -= 1 return value for num in Countdown(3): print(num) # 3 2 1

这里有一个我实际踩过的坑:如果__iter__只是return self而不重置状态,同一个对象遍历完第一次后再遍历第二次,直接就是空的。对于一次性迭代器(比如打开的文件对象)没问题,但如果你希望对象能反复被遍历,必须在__iter__里把状态拽回初始位置。

如果想偷懒,直接让__iter__返回一个生成器是最省事、最不容易出错的写法:

class Week: def __init__(self): self.days = ["一", "二", "三", "四", "五", "六", "日"] def __iter__(self): yield from self.days print(list(Week())) # ['一', '二', '三', '四', '五', '六', '日']

因为生成器本身就符合迭代器协议,yield from把内部逻辑交给生成器,状态管理也一并托管了。

5.2 让索引和迭代统一

有些场景下,你可能希望一个自定义类像列表一样支持“按索引访问”,又想直接for循环遍历。最稳妥的做法是同时实现__getitem__、__len__、__iter__。前面提到,只有__getitem__也能迭代,但那属于“老式协议”(legacy iteration protocol),在部分场景下有额外开销,也不够直观。显式实现__iter__后,迭代逻辑完全由你控制,比如按某种过滤条件遍历:

class FilteredList: def __init__(self, data, predicate): self.data = data self.predicate = predicate def __getitem__(self, index): return self.data[index] def __len__(self): return len(self.data) def __iter__(self): return (item for item in self.data if self.predicate(item)) fl = FilteredList(range(10), lambda x: x % 2 == 0) print(list(fl)) # [0, 2, 4, 6, 8] print(fl[2]) # 2,依然支持下标访问

6. 上下文管理器:把资源开关放进with语句

6.1 __enter__和__exit__的基本契约

with语句背后是上下文管理器协议:进入with块时调用__enter__,退出时无论是否抛异常都会调用__exit__。写文件用with open(...),加锁用with lock:,本质都是这套机制。

class ManagedFile: def __init__(self, filename): self.filename = filename self.file = None def __enter__(self): self.file = open(self.filename, "w", encoding="utf-8") return self.file def __exit__(self, exc_type, exc_val, exc_tb): if self.file: self.file.close() # 返回None(或False)表示不吞掉异常;返回True表示异常已被处理 return False

一段常规约定:__exit__的三个参数分别接收异常类型、异常实例、异常回溯对象(traceback)。如果with块里没抛异常,这三个参数都是None。如果返回True,异常会被当作“已处理”而不再向上传播。

什么时候需要返回True?很少。绝大部分场景都不该吞异常。一个典型例外是实现“数据库事务”这类需求:事务失败时回滚,但业务异常需要继续上抛;事务成功则提交。这种情况下__exit__里做回滚,返回False不吞异常。

6.2 用上下文管理器做计时器

上下文管理器很适合做耗时统计。把“开始时间和结束时间”的样板逻辑包起来,业务代码里只需要一个with:

import time class Timer: def __enter__(self): self.start = time.perf_counter() return self def __exit__(self, exc_type, exc_val, exc_tb): self.elapsed = time.perf_counter() - self.start print(f"耗时: {self.elapsed:.4f}秒") with Timer(): sum(i * i for i in range(1000000))

如果你还想在with块里拿到计时对象本身,把__enter__返回self就行。这个模式可以无限扩展:网络请求限时、日志上下文绑定、环境变量临时设置、目录临时切换等等,都可以封装成上下文管理器。

6.3 让__exit__精确处理异常

继续展开__exit__的异常处理。假设你设计一个“只在特定异常下回滚”的场景:

class DBTransaction: def __enter__(self): print("开启事务") return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: print("提交事务") else: print(f"回滚事务,原因: {exc_val}") return False # 仍然抛出原始异常 try: with DBTransaction(): raise ValueError("业务失败") except ValueError: print("捕获到业务异常")

这里两个信息值得记住:exc_val里面就是raise后面那个异常实例,直接描述文本就是它的字符串形式;而返回False是让异常继续往上走,外部try/except还能接住。

另一个容易被忽视的“隐藏特性”:with也可以同时管理多个上下文管理器,with A() as a, B() as b:里A和B的__enter__会按顺序进入;如果中间的__enter__抛异常,前面已经进入的上下文管理器也会依次执行__exit__。这一点写多资源管理代码时非常有用,比如同时打开两个文件。

7. 属性访问与可调用对象:更“高阶”的魔法

7.1 区分__getattr__和__getattribute__

这两个方法的名字很像,行为天差地别:

  • __getattr__只在常规属性查找失败时触发。对象本身没这个属性、类里也没有、父类也没有,才会调它。
  • __getattribute__则不同,所有属性访问都会先经过这里,比__getattr__更底层,也更危险。

实际业务里,__getattr__用途更广。比如你想做一个“属性名映射到字典键”的配置类:

class AttrDict: def __init__(self, mapping): self._mapping = mapping def __getattr__(self, name): if name in self._mapping: return self._mapping[name] raise AttributeError(f"{name} not found") config = AttrDict({"host": "127.0.0.1", "port": 8080}) print(config.host) # 127.0.0.1 print(config.not_exist) # AttributeError

注意__getattr__内部触发小坑:self._mapping本身也是属性访问,它在常规查找中能找到_mapping,不会无限递归;但如果我在__getattr__里写成self.__dict__["_mapping"]就不会有问题,写成self._mapping则要小心递归。原因在于:self._mapping一旦_mapping不存在,又会转而去调__getattr__,死循环就开始了。

7.2 __setattr__的重写禁忌

__setattr__允许你在所有属性赋值的入口做拦截。最常见的用途是校验或自动触发某种行为。但它也有一个著名的坑:在__setattr__里写self.xxx = value,会再一次触发__setattr__,导致无限递归。正确写法是用object.__setattr__(self, name, value)绕过拦截:

class PositiveNumber: def __init__(self, value): self.value = value def __setattr__(self, name, value): if name == "value" and value < 0: raise ValueError("不能设置负数") object.__setattr__(self, name, value) p = PositiveNumber(10) print(p.value) # 10 try: p.value = -1 except ValueError: print("捕获到负数拦截")

还要注意一点:__setattr__拦截的是所有属性赋值,包括__init__里的self.value = value。所以上面例子里__init__第一次初始化就经过了__setattr__的校验,负数当场就被拦下了。

7.3 __call__让实例变成函数

实现__call__的方法后,实例就像函数一样可以直接被调用:obj(...)。这有什么用?可以让一个对象同时保存状态和逻辑。比如做一个“带默认提成的价格计算器”:

class PriceCalculator: def __init__(self, discount=0.9): self.discount = discount def __call__(self, price): return round(price * self.discount, 2) calc = PriceCalculator(discount=0.8) print(calc(100)) # 80.0

这类“可调用对象”很适合代替闭包,尤其当你要保存的状态比较多时,类的方式可读性更好。另外,类方法中还有一个__call__的兄弟——__class__,跟这个无关,不要混淆。

7.4 __len__返回布尔值?一个边界细节

前面提过__bool__和__len__的关系,这里补充一个很容易糊弄过去的细节:__len__的返回值必须是整数,不能是None、不能是浮点数,否则调用len()时会抛TypeError。但如果不小心把__len__实现成“返回0或非0”,真值判断也能正常工作,因为bool(obj)会在找不到__bool__时用__len__的结果判断。也就是说,一个对象只要能len(),就能参与条件判断。

这个特性能玩出一些有趣的封装,但我不建议刻意依赖:一个容器类里,__len__的语义是容器尺寸,让它兼职判断真值只是顺带的,可读性要靠命名和文档来保证。

8. 常见问题与排查技巧实录

8.1 高频问题速查表

这些是我在实战和答疑里反复遇到的情况,整理成了表格,方便你遇到问题时直接对照。

现象根本原因解决办法
定义了__eq__后实例塞不进set__eq__出现后,__hash__会被自动设为None对不可变对象重写__hash__;可变对象尽量不要放进set或作为字典键
__getattr__里访问不存在的属性导致递归属性访问失败又触发__getattr__,循环执行用self.__dict__.get(name)或object.__getattribute__绕过
a += b后原有对象没被修改只实现了__add__没实现__iadd__,+=退化为“新建对象再赋值”可变对象实现__iadd__并return self
自定义对象打印出来是一堆内存地址没有实现__str__或__repr__至少实现__repr__,顺便实现__str__
with块里抛异常但程序直接崩溃__exit__返回True,异常被吞掉或没处理明确是否需要吞异常;默认返回False
__init__里调用super().__init__()顺序不对,导致父类状态被覆盖MRO顺序不理解或调用位置过早在需要修改父类字段前先调用super().__init__()
len(obj)抛TypeError__len__返回值不是整数检查__len__中是否把返回值写成None或字符串
运算符重载后出现“奇怪的递归”方法内部又调用了相同运算符,比如__add__里写return self + other在方法内部用格式化的新对象构造结果,而不是再触达运算符

8.2 排查属性相关的魔法问题

遇到AttributeError时,先判断它发生在“读取”还是“写入”。读取侧优先怀疑__getattr__是不是“多管闲事”了——比如内部某个属性还没有初始化,被__getattr__错误接管,导致返回了不该返回的东西。写入侧优先怀疑__setattr__是否在初始化阶段过早抛出异常。

调试时我习惯用dir(obj)快速查看实例的可访问属性与魔法方法列表。如果某个方法名明明写在代码里,dir输出里却没有它,一种可能是类定义后源代码文件有语法错误被IDE缓存了,另一种可能是属性赋值把同名实例属性覆盖了方法查找——这种情况不多见,但排查起来很费时间。

8.3 一个效率技巧:dataclass能省下大量魔法方法

日常写数据类时,手工实现__init__、__repr__、__eq__真的很烦。标准库dataclasses.dataclass装饰器直接帮你生成这些魔法方法。配合frozen=True还可以生成__hash__:

from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int p1 = Point(1, 2) p2 = Point(1, 2) print(p1) # Point(x=1, y=2) print(p1 == p2) # True print(hash(p1)) # 不等于None,因为frozen=True

不过要注意:dataclass默认生成的__eq__是比较所有字段,如果你只希望比较部分业务键,还是得手写__eq__。此外frozen=True只是不可变约定,内部如果放了列表这类可变对象,依然可以修改其内容,别把它当成深不可变的保证。

最后聊两句我的个人体会。魔法方法不是拿来炫技的,它更像是“Python对象模型给你预留的接口契约”。每次写类之前,先问自己几个实际使用场景:这个对象会被print吗?会被for循环遍历吗?会被放进set或当作字典键吗?会被拿去比大小吗?根据这些真实的用法,你就知道该实现哪些魔法方法,而不用背一个冗长清单。我也建议新手多写几个小型类练手,比如自定义集合类、模拟文件对象、封装配置项,每一类都会强迫你用上对应的特殊方法。等项目写得多了,你会发现“魔法”其实只是Python把约定俗成的行为协议摆到了明面上——理解了它,你写出的代码就能更自然地融入这门语言原有的表达方式,而不是让调用方到处绕路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询