ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python进阶语法核心:语法糖、装饰器与协议机制实战解析

Python进阶语法核心:语法糖、装饰器与协议机制实战解析 这篇是《Python语法进阶笔记》的第八篇我打算把镜头拉高一点专门聊聊那些写法上很漂亮、背后又很有讲究的进阶语法特性。如果你已经学完了基础语法变量、循环、函数、类正在从能写走向会想那么这篇应该正对你的胃口。我会从装饰器、迭代协议、上下文管理器、类型注解到运行时反射逐个拆开讲清楚并且每个大主题都配了可以直接复制去实验的代码片段。整篇笔记会延续前七篇的风格不堆术语、不灌概念就是站在一个写了很多年代码的老朋友视角把语法背后的设计逻辑和真实使用场景揉碎了讲给你听。1. 为什么需要进阶语法从语法糖说起的思考路径很多人刚上手Python时会觉得这是一门写起来挺快、读起来也顺的语言。但等你碰过几个真实项目尤其是读过几份开源代码之后就会意识到那些看起来有点绕的写法往往才是让代码变得灵活、可复用、高内聚的关键。网上有个搜索热词叫语法糖我特别想借这个词展开。语法糖syntactic sugar指的是那些不改变语言功能、但能让代码更好读更易写的语法设计。比如列表推导式就是典型代表[i * 2 for i in range(10)]和等价的for循环功能完全一样但前者一眼就能看出生成一个每个元素翻倍的列表。Python能这么流行很大一部分功劳要归给语法糖——它降低了认知负担让写代码的人可以把脑力省下来去思考业务逻辑。但进阶语法的真正价值不在于再多记几个糖而在于理解糖下面包着的那层机制。比如装饰器decorator本质上不过是一个接收函数、返回函数的函数但如果你只看它的语法很难理解它凭什么能让日志、鉴权、缓存这些横切逻辑变得如此优雅。一旦你拆开了它的壳去看它背后那句普通函数调用怎么写所有的疑惑都会自然消解。我自己学进阶语法有个很笨但很有效的方法遇到一段看不懂的语法先把它改写成不用该语法的等价形式再对照着读一遍。这样能同时看清语法糖长什么样和解释器实际做了什么双份理解之后再遇到变形用法就很容易举一反三。这篇笔记里讲到的每个语法特性我都会补上这种展开等价形态的对照方便你用自己的编辑器亲手实验。2. 函数进阶装饰器、闭包与参数处理的实战拆解2.1 装饰器从函数里包函数到用 语法装饰器是不是Python进阶语法里绕不开的第一课我觉得是。因为它横跨了两个层面一方面解决了很多真实痛点比如给多个函数统一加日志、计时、缓存另一方面它背后的闭包、函数作为一等对象这些概念恰好连接了会写函数和理解Python运行方式之间的落差。先看一个最简的装饰器长什么样import time def timer(func): def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) print(f函数 {func.__name__} 执行耗时: {time.perf_counter() - start:.6f} 秒) return result return wrapper timer def slow_add(a, b): time.sleep(0.2) return a b print(slow_add(1, 2))如果不看timer这行timer本身就是一个普通函数接收一个函数func在内部定义wrapper最后把wrapper返回出去。timer的作用不过是让解释器执行slow_add timer(slow_add)这一步赋值操作。换句话说之后你调用的slow_add已经不是原来那个函数而是被wrapper包裹后的新函数。这个替换逻辑是理解装饰器的关键点之一。正因为它本质上是把原函数作为参数传进去、再把新函数赋回去所以你可以在不修改原函数任何代码的前提下给函数添加额外行为。这是所谓开闭原则在Python里的一个非常自然的实践对扩展开放加装饰器就能加功能对修改关闭原函数代码不用动。实战中装饰器最常见的落点包括日志与审计统一在函数入口、出口打印关键信息或者记录调用参数。性能分析就像上面的timer一样统计每个函数的耗时。权限校验在Web框架比如Flask、Django的视图函数上加login_required。缓存functools.lru_cache就是一个官方提供的、用装饰器形式使用的缓存工具。重试机制当函数抛出特定异常时自动重试若干次。不过装饰器也有一个很容易踩的坑如果装饰器本身也需要参数怎么办比如你想让timer能指定打印几行日志或按秒还是毫秒计时直接套一层还是不够的。这时候需要写装饰器工厂——一个返回装饰器的函数def timer_with_unit(unit秒): def decorator(func): def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) elapsed time.perf_counter() - start print(f{func.__name__} 耗时 {elapsed:.6f} {unit}) return result return wrapper return decorator timer_with_unit(秒) def compute(): time.sleep(0.1) return 42 print(compute())注意这里timer_with_unit(秒)的执行顺序先调用timer_with_unit(秒)得到一个真正的装饰器decorator再用这个装饰器去装饰compute。想清楚这一层带参装饰器就不再神秘了。但如果你真的打算在正式项目里大量使用装饰器我强烈建议同时引入functools.wraps。为什么因为经过装饰器包装后函数的__name__、__doc__等元信息会被wrapper覆盖这在做调试、生成API文档时会造成很多困扰。functools.wraps能把原函数的元信息复制到包装函数上from functools import wraps def timer(func): wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) print(f耗时 {time.perf_counter() - start:.6f} 秒) return result return wrapper注意装饰器的执行时机是模块被导入时而不是函数被调用时。换句话说只要装饰器写在模块里一导入就会执行func decorator(func)这一步。如果你的装饰器内部有比较重的初始化逻辑请确保它不会拖慢模块导入速度。2.2 参数传递的进阶玩法*args、**kwargs 与仅限关键字参数装饰器代码里反复出现的*args, **kwargs也是个大话题。它在Python语法里被称为可变参数收集作用是把任意数量的位置参数和关键字参数分别收集进元组和字典。理解了它你不仅能写出更通用的封装代码还能读懂很多框架源码里那一大串星号参数。举个例子你写一个日志记录函数希望调用方式极其灵活def log(message, *values, **options): level options.get(level, INFO) prefix options.get(prefix, ) print(f[{level}] {prefix}{message}, end) if values: print(:, , .join(str(v) for v in values)) else: print() log(启动服务, 端口8080, 环境prod, levelWARN, prefix)这个函数能同时接收任意多个位置参数被塞进values元组、任意多个关键字参数被塞进options字典。这种设计在配置文件读取、事件回调、命令行工具解析中特别常用。进阶语法里还有一个容易被忽略的小知识点仅限关键字参数keyword-only arguments。它用*在参数列表中切断位置参数和关键字参数写在*之后的参数只能用paramvalue的形式传入def connect(host, port, *, timeout10, retries3): print(f连接 {host}:{port}超时 {timeout} 秒重试 {retries} 次) connect(localhost, 8080) connect(localhost, 8080, timeout20) # connect(localhost, 8080, 20) # TypeError: connect() takes 2 positional arguments but 3 were given为什么要这么设计因为在一些参数表中某些参数语义较强、容易引起误解限定它们只能以关键字方式传入能大幅提高调用代码的可读性。真实案例是Python的print函数它的sep、end、file参数都是仅限关键字参数。如果你去翻标准库的源码会发现这种设计在需要可选行为开关的场景里出现频率极高。这里想分享一个我在处理函数封装时的习惯如果一个函数有3个以上可选参数我一定会把它们定义成仅限关键字参数。这样调用方每次都写paramvalue既不容易传错顺序也方便代码审查时快速判断哪个选项被修改过。2.3 闭包与 nonlocal为什么 Python 的函数能记住外层变量闭包closure这个词听起来像个数学概念但其实它描述的现象非常朴实内层函数可以引用外层函数的变量并且在外层函数返回之后这个变量仍然活着。装饰器正是靠这个特性工作的——wrapper内部引用了func即使外层timer已经返回func还是被wrapper牢牢记在手里。下面这段代码展示了闭包最基本的样子def make_counter(): count 0 def counter(): nonlocal count count 1 return count return counter c make_counter() print(c()) # 1 print(c()) # 2如果不写nonlocal count直接在counter里写count 1会怎样会报UnboundLocalError。原因是Python有变量作用域推断规则只要函数体内出现对某个变量的赋值解释器就认为它是一个局部变量。count 1已经算赋值了所以counter里会把count当作局部变量可你还没给它初始值于是报错。nonlocal的作用就是告诉解释器这个count不是局部变量而是往上一级函数的作用域去找。这个概念理解之后闭包就不再是一个只能背定义的概念而是你随手就能写出来的工具。但在实际项目里闭包有个并发场景下的小坑如果多个闭包共享同一个外层变量并且这个变量是可变的你就要小心共享状态带来的意外。比如循环创建闭包时不注意所有闭包捕获的可能是同一个变量的最终值funcs [] for i in range(3): funcs.append(lambda: i) print([f() for f in funcs]) # [2, 2, 2]这就是经典的闭包陷阱。原因是lambda: i里并没有立即把i值拷贝进去它引用的是变量i本身当循环结束后i已经是2所有函数调用的结果自然都是2。解决办法是给外层函数额外包一层或者用默认参数绑定值funcs [] for i in range(3): funcs.append(lambda ii: i) print([f() for f in funcs]) # [0, 1, 2]这个lambda ii: i的写法利用了默认参数在函数定义时求值这个机制把每个i当时的取值快照下来。我见过不少初级中级工程师在GUI按钮绑定、批量注册回调时踩到这个坑所以特意放在这里提示一下。3. 迭代协议、生成器与上下文管理器理解 Python 的背后协议3.1 for 循环背后的秘密迭代协议 iter() 与 next()如果只看表面for x in items:不过是一个循环语法。但Python里的for循环能作用于列表、元组、集合、字典、文件对象、数据库游标……它靠的不是给每种容器都实现一个专用循环指令而是一套统一的迭代协议。简单说任何实现了__iter__方法的对象都可以被for遍历。协议可以拆成两步理解iter(obj)会调用obj.__iter__()返回一个迭代器对象。循环内部反复调用next(iterator)这个调用会触发迭代器的__next__()方法返回下一个元素当没有更多元素时抛出一个StopIteration异常循环捕获到这个异常就结束。你可以自己写一个非常简陋的迭代器class CountDown: def __init__(self, n): self.n n def __iter__(self): return self def __next__(self): if self.n 0: raise StopIteration self.n - 1 return self.n 1 for num in CountDown(3): print(num)把这个类跑一下你就等于亲手实现了一遍for 循环的底层机制。理解这个协议有什么实际价值最直接的一点是如果你想让自己写的容器类支持for、len()、in等操作只需要实现对应的一组魔法方法。比如实现了__len__和__getitem__你的对象就能支持len()和下标访问。这里还有一个容易被忽略的小知识点对字典做for循环时遍历的是它的键而不是值对元组、字符串来说遍历的是元素和字符。这些看起来约定俗成的行为其实正是容器类各自实现了相应迭代语义的结果。你自己定义类的时候也可以自由决定__iter__到底产出什么。3.2 生成器与 yield惰性求值如何节省海量内存生成器是迭代协议的轻量级实现。它不需要写一个完整的类只需要在函数里用yield关键字def fibonacci(): a, b 0, 1 while True: yield a a, b b, a b fib fibonacci() for _ in range(10): print(next(fib))这段代码里的fibonacci()不是一个普通函数调用——执行它不会立刻运行函数体而是返回一个生成器对象。每次next(fib)时代码才运行到下一个yield处并把a的值递出下次再调用next()时从上次停下的地方继续执行。这个函数里有个while True但它不会因为无限而崩溃因为你只在需要的时候取元素取完就暂停。生成器最大的优势是惰性求值。设想你要处理一个巨大的日志文件几GB那种如果一次性把全部内容读进内存机器很可能会卡死。但生成器可以做到一次只读一行def read_large_file(file_path): with open(file_path, r, encodingutf-8) as f: for line in f: # 文件对象本身就是惰性迭代器 yield line.strip() for line in read_large_file(huge.log): if ERROR in line: print(line)注意for line in f之所以能逐行处理也正是因为文件对象实现了迭代协议而它的__next__是按行读取的。这是Python把语法协议和真实场景结合得非常漂亮的例子。还有一个进阶技巧叫生成器表达式(x * 2 for x in range(10))。它长得像列表推导式但外面是圆括号记住它不是立刻生成所有元素的列表而是一个惰性的生成器。在sum(x * 2 for x in range(10))、max((len(x) for x in names))这种需要临时计算一批值、又不想建中间列表的场景里生成器表达式几乎总是更好的选择。实操心得调试生成器代码时建议先用list(generator)把它转换成列表观察输出是否符合预期你还可以用next()逐步推进确认每个yield的执行顺序。我见过不少人在生成器里写了print却看不到输出原因就是生成器根本还没被执行。3.3 上下文管理器与 with 语句资源管理的优雅姿势文件操作里最常见的写法是with open(data.txt, r, encodingutf-8) as f: data f.read()with语句到底做了什么它和try/finally其实是等价但更优雅的写法。with语法背后要求对象实现两个魔法方法__enter__和__exit__。进入with块时调用__enter__它的返回值会赋给as后面的变量离开with块时无论正常退出还是抛出异常都会调用__exit__你可以在里面做清理工作比如关闭文件、释放锁、提交或回滚数据库事务。自己写一个上下文管理器并不难。最简单的方式是用标准库的contextlib.contextmanagerfrom contextlib import contextmanager contextmanager def timed_block(label): import time start time.perf_counter() try: yield finally: print(f{label} 耗时 {time.perf_counter() - start:.4f} 秒) with timed_block(解析数据): time.sleep(0.1)这里yield之前的代码相当于__enter__yield之后的代码相当于__exit__。finally块保证哪怕with块内部抛了异常计时结束的打印语句也会执行。我建议在日常代码里养成凡是获取了资源就立刻考虑用with包裹的习惯。因为资源忘记释放造成的连接泄漏、文件句柄过多等问题通常在低负载测试时完全看不出来一到生产环境并发一上来就暴露。如果你在自定义类里维护了连接、锁、临时目录这类资源实现一套__enter__/__exit__让使用者只需要with一下就是对整个项目负责。4. 类型注解与面向对象进阶让代码自我说明4.1 类型注解基础给函数加一份说明书Python是动态类型语言运行时不强制变量类型但为了工程上的可维护性从Python 3.5起引入了类型注解type hints语法。你可以在函数定义时标注参数类型和返回值类型def calculate_price(unit_price: float, quantity: int, discount: float 0.0) - float: return unit_price * quantity * (1 - discount)变量也可以直接标注name: str Python version: float 3.12 count: int | None None注意类型注解只是标注解释器并不会因此报类型错误。它的作用在于给人看如果说得难听点是给未来的自己看的文档给静态类型检查工具看比如 mypy、pyright给IDE的代码提示和补全提供依据。这里有一个很常见的新手困惑既然不强制写了有什么用我的回答是——真正的收益发生在代码规模上来之后。当你打开一个三个月没碰过的项目看到一个def handle_event(user, payload)的函数你完全不知道user是字符串还是对象、payload是字典还是Pydantic模型。但如果你写成def handle_event(user: User, payload: dict[str, Any]) - None很多上下文瞬间就清晰了。4.2 typing 模块进阶注解的常用工具只用str、int、float、bool这几种基础注解还不够真实场景里的数据结构往往更复杂。typing模块提供了一批泛型容器注解最常用的是这几个注解写法含义示例场景list[int]元素为整数的列表成绩列表[85, 92, 78]dict[str, Any]键为字符串、值为任意类型的字典解析JSON返回的通用字典tuple[int, str]固定长度的元组坐标和名称(3, point_a)Optional[int]可能是整数也可能是None查询结果可能为空Union[int, float]可能是整数或浮点数数值参数接受int和floatCallable[[int], str]接收一个整数参数、返回字符串的函数回调函数类型Any任意类型用于绕开类型检查或兼容动态数据在Python 3.10之后Union[int, float]有更简洁的写法int | floatOptional[int]也可以写成int | None。我自己在3.10的代码里基本都倾向于新版简化写法它读起来和自然语言几乎一样。使用类型注解还有助于发现隐藏的Bug。比如你声明def process(items: list[int]) - int:那么当你不小心把[a, b]传进去时IDE和mypy会直接划线提示。这种“在运行前就发现问题”的收益在几十个模块互相调用的项目里尤其明显。补充说明# type: ignore 注释可以在少数无法让类型检查通过的真实现场中临时绕过检查TYPE_CHECKING变量则可以在运行时为False的前提下只在类型检查阶段导入需要标注的类从而避免循环导入问题。4.3 面向对象进阶slots、property 与类方法/静态方法基础语法里我们已经学会了class定义和实例化但面向对象进阶语法的部分多的是既有默认行为再定制的魔法方法。__repr__和__str__是最典型的例子它们控制打印实例对象时显示的字符串一个好的__repr__能让调试日志变得非常直观。再比如property装饰器它允许你在外部用法像普通属性的同时内部执行方法逻辑class Circle: def __init__(self, radius: float): self._radius radius property def radius(self) - float: return self._radius radius.setter def radius(self, value: float) - None: if value 0: raise ValueError(半径必须为正数) self._radius value property def area(self) - float: return 3.14159 * self._radius ** 2有了property你既能对赋值做校验又能把计算属性包装成一个普通属性使用。circle.area看起来像一个存储属性但它每次都会实时计算。这种语法让类的对外接口非常稳定哪怕内部存储方式改成了别的字段外部代码依然不需要变化。__slots__是一个容易被忽视但对性能有影响的进阶语法。在类里声明__slots__ (name, age)之后实例不再拥有__dict__属性字典而是被限制为只能拥有指定属性。这样做的效果是省内存——如果你需要一次性创建成千上万个实例对象比如批量拉取数据记录的DTO内存节省会很可观。代价是不能再随意给实例添加新的属性。另外staticmethod和classmethod也值得提。staticmethod定义一个和实例、类都没有绑定关系的普通函数但它仍然放在类内部起到组织代码的作用classmethod的第一个参数是类本身常用于提供类级别的工厂方法class Config: def __init__(self, path: str): self.path path classmethod def from_env(cls) - Config: import os return cls(os.getenv(CONFIG_PATH, config.yaml)) config Config.from_env()5.1 用 dir 与 help 做运行时侦查dir(obj)配合getattr(obj, name)是探索任意对象最简单的入门方式。dir()返回一个对象的所有可用属性与方法名列表如果你想知道某个具体属性的值就用getattr(obj, attr_name)去拿。这种组合不需要打开源码直接在交互式环境里就能把对象底细翻个底朝天。help(obj)则是查看官方文档的快速通道。如果你在命令行里敲help(str)会直接看到str类的完整文档help(function_name)会显示该函数的签名和docstring。我平时在写代码卡壳时第一反应通常不是搜索引擎而是本地help()和dir()——它们永远离线可用而且信息来自你当前实际安装的这个版本准确性更高。5.2 getattr/setattr动态属性读写的反射基础反射reflection指的是程序在运行过程中可以检查自身状态、获取或修改属性、甚至调用任意方法。Python中getattr、setattr、hasattr、delattr这四个函数构成了对实例属性的动态读写能力。一个典型的应用场景是配置驱动的调用。比如你有上百个数据清洗函数清洗规则由一个外部配置文件指定每行写着函数名和参数。如果不用反射你要写一大串if分支用了反射代码非常简洁import dataclasses def clean_phone(val: str) - str: return val.strip().replace(-, ) def clean_email(val: str) - str: return val.strip().lower() dataclasses.dataclass class Record: phone: str email: str def apply_cleaning(record: Record, rules: dict[str, type]) - None: for field, func_name in rules.items(): if hasattr(record, field) and callable(func : globals().get(func_name)): setattr(record, field, func(getattr(record, field))) r Record(phone 123-456 , email UserExample.COM ) apply_cleaning(r, {phone: clean_phone, email: clean_email}) print(r.phone, r.email) # 123456 userexample.com这里用到了hasattr判断属性是否存在、getattr读取属性值、setattr给属性赋值以及globals().get(...)按名字查找函数。整个过程都是动态的规则表里加一行新条目代码什么都不用改清洗逻辑就自动扩展了。反射当然也很容易被滥用。如果什么地方都用字符串去拼方法名、绕开所有静态分析代码会变得很难调试、很难重构。我的建议是反射适合批量处理、规则驱动的框架层场景不适用于业务逻辑里每行都来一次动态查找。稳定性优先的代码显式的if/else往往更可靠。5.3getattr与setattr拦截属性访问的魔法方法Python中的属性访问也不是只有查找__dict__然后返回这一种路径。当你访问obj.attr时如果常规查找失败解释器就会调用obj.__getattr__(attr)如果你给实例设置属性obj.attr value解释器会调用obj.__setattr__(attr, value)——没定义这两个魔法方法时用的才是默认行为。__getattr__最广为人知的用途是实现懒加载。比如你已经有一个User对象其中permissions需要调用后端接口获取才能拿到但你不想在初始化时就着急请求而是等到代码真去读user.permissions时才触发加载class User: def __init__(self, user_id: int): self.user_id user_id self._permissions None def _load_permissions(self) - list[str]: print(触发权限懒加载...) return [read, write] def __getattr__(self, item: str): if item permissions: self._permissions self._load_permissions() return self._permissions raise AttributeError(f{item} 不存在)需要注意的是__getattr__只在默认查找失败后才触发__getattribute__则会在每次属性访问时无条件触发一般我不建议普通业务代码去碰它拦截层级太深容易把自己绕晕。而__setattr__可以用来统一校验所有字段的赋值但如果写得不小心很容易造成递归调用因为它自己也会走属性设置流程。一个安全的写法是在方法内部用object.__setattr__(self, key, value)绕过魔法方法。每当我需要在__setattr__里干活时都会先停下来再想想用property是不是更简单——这算是一条很实用的经验。6. 进阶语法在实际项目里的组织方式与练习建议6.1 怎么把语法点组合成一个完整的语言特性真正用好Python进阶语法不是背会十个知识点就完事而是要让这些知识点在黑板上组合成立体结构。举个例子一个带装饰器、又接收配置的函数背后可能同时用到闭包、*args/**kwargs、functools.wraps、泛型注解一个自定义上下文管理器可能同时用到生成器、contextlib.contextmanager、类型注解。我建议读者在读代码时主动做语法拆解练习遇到一个带着两三个特性叠加的片段先找到最外层的语法糖再逐层向内剥直到露出最朴素的变量赋值、函数调用为止。6.2 官方文档与源码是最重要的学习资料进阶学习阶段我最推荐的是去读Python官方文档里的tutorial和reference部分以及标准库源码。特别是functools、itertools、contextlib这几个模块它们集合了大量语法和函数式编程交叉的例子。比如functools.partial可以让函数的一部分参数被预填你读它的定义时就会碰到闭包是怎么用的、位置参数和关键字参数是怎么传的这些语法点。与其记结论不如每次动手敲一遍再想想如果是我会怎么写这个实现。6.3 一个小练习用进阶语法重构一段普通代码还是用代码来收尾吧。假设你有一段反复出现的耗时监控代码想让多个函数都能检测执行时间并希望输出包含函数名、时长和单位。最朴素的做法是每个函数写一遍start time.time()和elapsed time.time() - start。但在进阶视角里一个带参装饰器加闭包就能解决from functools import wraps import time def monitor(unit: str 秒): def decorator(func): wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() result func(*args, **kwargs) duration time.perf_counter() - start print(f{func.__name__} 执行耗时 {duration:.6f} {unit}) return result return wrapper return decorator monitor(unit秒) def fetch_data(): time.sleep(0.3) return [1, 2, 3] print(fetch_data())左手是这个装饰器右手是monitor的用法中间是*args/**kwargs和functools.wraps的配合。一个本来会散落在各处的重复逻辑被剥离成了统一的关注点。6.4 遇到运行时报错时善用 traceback 与 pytest 做语法理解验证学习进阶语法的过程中报错是最好的老师之一。遇到TypeError: missing 2 required positional arguments时往往说明你的参数设计出了问题遇到UnboundLocalError时基本就是忘了nonlocal。我的建议是写几个带有 错误预期 的实验脚本故意漏掉参数、故意忘记nonlocal、故意在__setattr__里递归赋值然后对照报错信息追查原因。用pytest把这些错误触发的断言写下来语法体系会进入一个可持续验证的回路。我个人在这个学习阶段最大的体会是三个字多拆解。把每一个看起来高级的语法糖都拆成普通代码再把普通代码重新包回语法糖反复几轮后你就不需要再记任何规则了因为你能直接推出规则本身。这篇笔记写到的装饰器、迭代协议、生成器、上下文管理器、类型注解和反射本质上都是同一种思维的多个侧面理解Python的协议机制然后顺着协议去创作自己的语法。希望你在读完这篇之后也能把这些特性当成自己工具箱里顺手可用的零件而不仅仅是看过的知识点。
返回列表