python enum
## Python 中的Any一个被低估的类型注解工具在 Python 的类型注解体系里Any是一个看似简单却常常引发误解的特殊类型。很多开发者第一次见到它时可能会觉得这不过是个“万金油”式的占位符用来应付那些暂时不想仔细标注类型的场景。但实际上Any的设计背后有着相当深刻的考量理解它的真正含义和适用场景能显著提升代码的类型安全性和可维护性。他是什么Any并不是一个运行时存在的具体数据类型它只存在于静态类型检查的语境中是typing模块提供的一个特殊注解。你可以把它理解为一个“类型黑洞”。当我们说一个变量是Any类型时我们是在向类型检查器比如 mypy宣告放弃对这个变量的一切类型约束和推断。这意味着对这个变量你可以进行任何操作把它当成整数来加当成字符串来切片当成列表来迭代或者调用它假设它是可调用的。类型检查器会对此全部开绿灯不会发出任何类型不匹配的警告。这听起来很危险也确实如此因为它完全绕过了类型检查的核心防护机制。一个常见的误解是认为Any等同于object。object是 Python 中所有类的基类一个被标注为object的变量你只能对它进行所有对象共有的操作比如调用__str__方法。但你不能随意把它当整数加除非你显式地做类型转换。而Any则允许你“为所欲为”这正是它最特殊也最需要谨慎使用的地方。他能做什么既然这么危险为什么还需要Any呢它的存在主要是为了解决几个现实而棘手的问题。首要场景是处理动态性极强的代码。Python 社区有大量历史遗留的库或者为了极致灵活性而设计的框架比如某些 Web 框架的 ORM 或模板系统它们的返回值在静态层面几乎无法确定。强行用Union列出几十种可能的类型既不现实也失去了可读性。这时Any就是一个诚实的声明“这里的类型我确实说不清检查器你别管了。”其次它充当了渐进式类型化的“粘合剂”。在一个大型项目从无类型向强类型迁移的过程中总会有一些模块暂时无法标注或者需要与完全无类型的第三方库交互。在这些边界上使用Any可以暂时隔离类型化的部分和非类型化的部分让迁移工作能够分块进行而不至于被一个难点卡住整个进程。另外在某些设计模式中比如实现一个通用的装饰器或中间件它需要处理任意类型的函数和参数此时使用Any来描述这种“任意性”在语义上反而是准确的表明设计意图就是如此。怎么使用使用Any在语法上极其简单从typing模块导入后像其他类型注解一样使用即可。fromtypingimportAnydefread_data_from_source(source:Any)-Any:# 这个函数可能从网络、数据库、文件读取数据返回任何格式datasome_magic_operation(source)returndata上面的例子展示了一种典型用法函数的参数和返回值都极其不确定。但更常见也更推荐的做法是尽可能缩小Any的传染范围。比如一个函数内部不得不处理一个Any类型的变量但应尽快通过类型判断将其“降级”为具体类型。defprocess_item(item:Any)-None:# 尽快进行类型守卫缩小不确定性ifisinstance(item,str):print(f处理字符串:{item.upper()})# 这里 item 被推断为 strelifisinstance(item,int):print(f处理整数:{item*2})# 这里 item 被推断为 intelse:print(未知类型)这种模式非常重要。它接收了外部的“混沌”Any但在函数内部建立了秩序具体类型。这阻止了Any像病毒一样在代码库中传播将不确定性控制在最小的、必要的范围内。最佳实践关于Any的最佳实践核心思想可以概括为将其视为最后的手段而非首选的工具。首先在添加新代码时应极力避免主动使用Any。多花几分钟思考是否能用Union、TypeVar泛型、Protocol协议或overload重载来更精确地描述类型。这些工具虽然复杂一些但能提供真正的类型安全。例如一个可以处理整数和浮点数的函数应该用Union[int, float]而不是Any。其次要警惕Any的“无声扩散”。一个函数返回了Any那么调用它的所有地方接收返回值的变量都可能被“污染”为Any类型。这会使得类型检查在很大一片代码区域失效。因此如果某个核心函数不得不返回Any应该在其文档中清晰说明原因并规划在未来将其替换为更精确的类型。对于第三方库如果它们没有类型注解可以尝试寻找或创建存根文件.pyi文件。如果确实没有再在导入时使用Any来注解。现代 IDE 和类型检查器通常能很好地处理存根文件这比在整个项目中使用Any要好得多。最后可以将mypy的配置项disallow_any_expr或warn_return_any设置为True让类型检查器对Any的出现提出警告这有助于在代码审查中及时发现不必要的Any使用。和同类技术对比在类型注解的工具箱里Any有几个近亲区分它们有助于做出更合适的选择。最常被混淆的是object。如前所述object是具体的、约束的。当你标注object时你是在说“这是一个任何 Python 对象”但类型检查器会强制你只能进行通用的对象操作。而Any是在说“类型检查在此失效”。如果你需要一个通用的容器来存放“任何东西”但后续会通过isinstance来明确类型那么object往往是更安全、更表达意图的选择。Union[Type1, Type2, ...]是Any的“结构化”替代品。当可能类型是有限、可知的集合时Union是绝对优于Any的选择。它提供了真实的类型安全检查器能基于不同的分支进行推理。Any则是一种“无限的、未知的 Union”放弃了所有安全性。TypeVar泛型用于表达“多个位置是同一个不确定类型”。例如一个函数返回其参数的同类型值。用Any会丢失这种关联关系而用TypeVar可以保持它检查器能确保类型的一致性这是Any完全无法做到的。typing.cast是另一个相关工具。它用于在开发者比检查器更了解类型时进行强制类型断言。cast是在某个点上“欺骗”检查器而Any是在一个作用# ## Python中的namedtuple不只是个花哨的元组在Python的标准库collections模块里藏着不少实用但容易被忽略的工具namedtuple就是其中一个。第一次见到它的时候很多人会想这不就是个能命名的元组吗但用久了会发现它解决的问题比看起来要多得多。它到底是什么简单来说namedtuple创建的是一个元组的子类。元组我们都知道不可变、有序、能通过索引访问。但有个问题当元组里元素多了代码里全是data[0]、data[1]这样的数字索引过段时间再看根本记不清每个位置代表什么。namedtuple给每个位置起了个名字。比如你处理一个二维坐标普通元组可能是(3, 4)你得记住第一个是x第二个是y。而用namedtuple创建的Point你可以写成Point(x3, y4)通过point.x和point.y来访问。它本质上还是元组所以依然不可变、可哈希但多了可读性。它能解决哪些实际问题想象一下你在处理从数据库查询出来的用户数据。每条记录可能包含id、姓名、邮箱、注册时间等字段。如果用普通元组代码里会充斥着user[0]、user[1]这样的魔法数字。同事review代码时得不断翻看数据库字段定义效率很低。换成namedtuple你可以创建一个User类型然后通过user.name、user.email来访问。代码突然就变得自解释了。更重要的是因为它是元组的子类所有元组能用的特性它都能用——拆包、迭代、作为字典的键都没问题。另一个常见场景是函数返回多个值。Python函数虽然能返回元组但调用方得记住返回值的顺序。用namedtuple作为返回类型调用方可以通过属性名来访问不用依赖顺序。这在API设计里特别有用尤其是当返回字段可能随着版本增加时向后兼容会更容易处理。怎么用才顺手使用namedtuple很简单但有些细节值得注意。基本用法是从collections导入然后定义类型fromcollectionsimportnamedtuple# 定义类型Pointnamedtuple(Point,[x,y])# 创建实例pPoint(3,4)# 也可以 Point(x3, y4)# 访问print(p.x)# 3print(p[0])# 3依然支持索引这里有个小技巧字段名可以用字符串列表也可以用空格或逗号分隔的字符串。个人更喜欢用字符串列表因为更明确尤其是字段名比较多的时候。创建之后namedtuple实例的行为和普通类实例很像但它没有__dict__内存占用更小。这也是为什么它适合处理大量数据——在性能和可读性之间取得了不错的平衡。一些实际使用中的经验虽然namedtuple很好用但也不是万能的。有些最佳实践值得注意。首先考虑是否真的需要不可变性。namedtuple创建后不能修改字段值这是优点也是限制。如果你的数据结构需要频繁修改可能用字典或自定义类更合适。但不可变性带来了其他好处——线程安全、可哈希、可以作为字典的键。其次当字段很多时考虑是否应该用真正的类。namedtuple适合轻量级的数据载体但如果需要添加方法、属性或者更复杂的行为普通的类可能更合适。不过Python 3.7之后dataclass可能是更好的选择这个后面会提到。还有一个细节namedtuple有_asdict()方法可以转换成有序字典。这在需要JSON序列化时特别有用因为字段顺序会保留。_replace()方法可以创建新实例并修改部分字段这符合函数式编程的风格。和类似技术的比较说到数据类Python生态里有几个选择普通字典、自定义类、namedtuple、dataclass还有第三方库如attrs。字典最灵活但缺少结构定义。字段名是字符串拼写错误要到运行时才能发现。自定义类功能最全但样板代码多。namedtuple在两者之间——有结构定义但足够轻量。dataclass是Python 3.7加入的可以看作是namedtuple的增强版。默认情况下dataclass是可变的但可以通过frozenTrue参数变成不可变。它支持默认值、类型提示还能轻松添加方法。如果项目已经用Python 3.7大多数情况下dataclass比namedtuple更合适。但namedtuple有个优势它存在于标准库不需要额外依赖。在维护旧项目或者写库代码时这个考虑很重要。而且因为它是元组的子类在一些需要元组兼容性的场景下namedtuple是更自然的选择。attrs是第三方库功能比dataclass更丰富但需要额外安装。如果项目已经用了attrs继续用就好如果是新项目dataclass通常足够了。选择哪个取决于具体需求。如果只是需要一个轻量级的、不可变的数据容器并且希望保持与元组的兼容性namedtuple是很不错的选择。如果需要更多功能或者项目已经用新版本Pythondataclass可能更合适。最后一点想法技术选型很少有绝对的对错# # Python中的枚举不只是给数字起个名字写代码时间长了总会在某些地方遇到这样的场景需要定义一组相关的常量。比如星期几、订单状态、用户类型这些。最早的时候可能就是用一堆数字或者字符串硬编码在代码里。MONDAY1TUESDAY2WEDNESDAY3# ... 后面还有一堆或者更糟一点直接写魔法数字iforder_status2:# 处理已发货的订单这种写法的问题很明显。数字2代表什么过了三个月再看代码自己都记不清了。字符串稍微好一点但拼写错误、大小写不一致的问题又冒出来了。Python从3.4版本开始在标准库里加入了enum模块算是给了这个问题一个官方的解决方案。不过很多人对枚举的理解还停留在“给数字起个别名”的层面这就有点小看它了。枚举到底是什么简单说枚举就是一组有名字的常量集合。但Python里的枚举有点特别它其实是一个类。当你定义一个枚举时实际上是在创建一个特殊的类这个类的每个实例都代表枚举中的一个成员。这些成员是唯一的、不可变的而且在枚举定义完成后就不能再动态添加新的成员了。这听起来有点抽象可以想象成你有一个装满了特定颜色小球的盒子。盒子里只有红、黄、蓝三种颜色的小球你不能往里面放绿色的小球也不能把红色小球改成紫色。每个小球除了颜色不同还有自己的名字标签。枚举就是这样一个“盒子”它限定了你能使用的“小球”的种类和数量。枚举能解决什么问题最直接的用途当然是替代魔法数字和魔法字符串。代码的可读性会好很多。# 以前ifstatusshipped:pass# 使用枚举后ifstatusOrderStatus.SHIPPED:pass但枚举的价值不止于此。它还能提供类型安全——虽然Python是动态类型语言但使用枚举至少能保证你用的值是预定义集合里的一个。枚举成员也是对象这意味着它们可以有自己的方法。这个特性很多人没用过但其实很有用。比如你可以给订单状态的枚举加上判断方法classOrderStatus(Enum):PENDING1PROCESSING2SHIPPED3DELIVERED4defis_completed(self):returnselfin[self.SHIPPED,self.DELIVERED]另一个不太为人知的特性是枚举的迭代能力。你可以很方便地遍历所有枚举成员这在生成选项、验证输入时很有用。怎么用好枚举定义枚举很简单继承Enum类就行fromenumimportEnumclassColor(Enum):RED1GREEN2BLUE3但这里有个细节枚举成员的值不一定非得是整数可以是任何不可变类型。字符串、元组都可以。不过整数是最常用的因为很多场景下需要和数据库、API交互整数更通用。访问枚举成员有几种方式Color.RED、Color[RED]、Color(1)。第一种最直观第二种在动态场景下有用第三种通过值获取成员。实际使用中经常会遇到需要序列化枚举的情况。比如把枚举存到数据库或者通过API传输。这时候要注意直接序列化枚举对象可能不是你想要的结果。通常的做法是序列化枚举的值Color.RED.value或者名字Color.RED.name。还有一个实用的技巧是使用auto()来自动生成值。当你不关心具体值是多少只关心成员之间的区别时这很方便classColor(Enum):REDauto()GREENauto()BLUEauto()一些实践中的经验首先枚举成员的命名最好用全大写。这是Python社区的约定一眼就能看出是常量。其次考虑使用IntEnum或StrEnumPython 3.11如果枚举需要和整数或字符串比较。普通的Enum成员不能直接和整数比较但IntEnum可以。不过要小心这可能破坏了枚举的隔离性。fromenumimportIntEnumclassPriority(IntEnum):LOW1MEDIUM2HIGH3# 现在可以这样比较ifpriorityPriority.MEDIUM:# 处理高优先级对于有逻辑关系的枚举可以考虑使用Flag或IntFlag。比如用户的权限一个用户可能有多个权限fromenumimportFlag,autoclassPermission(Flag):READauto()WRITEauto()EXECUTEauto()# 组合权限user_permissionsPermission.READ|Permission.WRITE# 检查权限ifPermission.READinuser_permissions:print(可以读取)枚举应该定义在靠近使用它的地方。如果是多个模块共享的枚举可以放在单独的模块里。但不要为了“复用”而把不相关的枚举硬塞到一个文件里。和其他方式的对比在枚举出现之前常用的替代方案有几种模块级常量、字典、命名元组。模块级常量就是文章开头提到的那种方式。它的主要问题是缺乏组织性常量之间没有明确的关联关系。而且常量可以被随意修改虽然Python没有真正的常量但至少枚举成员在定义后是只读的。字典方式稍微好一点COLORS{RED:1,GREEN:2,BLUE:3}这样至少把相关的常量组织在一起了。但字典可以被修改键拼写错误要到运行时才能发现而且没有类型提示的支持。命名元组也能达到类似的效果fromcollectionsimportnamedtuple Colornamedtuple(Color,[RED,GREEN,BLUE])colorsColor(1,2,3)这种方式比字典好一点至少是不可变的。但命名元组的“成员”实际上是属性不是独立的常量使用起来不够直观。和这些方式相比枚举提供了更好的封装性、安全性和功能。它有自己的类型IDE可以做更好的智能提示和代码补全。枚举成员是单例的相同的枚举成员是同一个对象可以用is来比较这比用比较值更安全。不过枚举也不是万能的。如果只是两三个简单的常量用枚举可能有点重。如果常量需要频繁地从配置文件或数据库加载枚举可能也不太合适因为枚举定义是写在代码里的。总的来说枚举是Python中一个被低估的特性。它不仅仅是给数字起名字的工具而是一种组织相关常量的完整方案。在合适的场景下使用枚举能让代码更清晰、更安全。下次定义常量时不妨考虑一下是否可以用枚举来组织它们。更多的是权衡。namedtuple在Python工具链里可能不是最闪亮的那个但它解决了一类特定问题而且解决得很好。它的存在提醒我们有时候简单的解决方案恰恰是最持久的。在代码里看到namedtuple时通常意味着作者在可读性和性能之间做了考虑选择了这个折中方案。这种考虑本身比用哪个具体工具更重要。工具终究是工具知道什么时候用什么工具才是真正经验所在。域内“关闭”检查器。通常局部、有限的“欺骗”比大范围的“关闭”更可取。总的来说Any是类型系统中的一个紧急出口而不是日常通道。它的强大在于其彻底的“无为”而危险也恰恰源于此。在精心划定边界、控制影响范围的前提下使用它能让它在处理动态代码、进行渐进式迁移时发挥不可替代的作用。但时刻记住精确的类型注解才是让代码更健壮、更易理解的根本。