拓冰建站拓冰建站
首页 / 资讯中心 / 正文

mypy 类注解实战指南:属性声明、方法重写、抽象基类与 __slots__ 完整解析

mypy 类注解实战指南属性声明、方法重写、抽象基类与slots完整解析【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy本指南基于 mypy 官方文档 docs/source/class_basics.rst 整理并深化系统讲解在 Python 类中编写类型注解的核心规则实例属性与类属性的声明与类型推断、__init__的注解约定、ClassVar的语义与陷阱、方法重写的签名兼容性检查、override装饰器、抽象基类ABC与多重继承以及__slots__的静态校验。读完本文你将掌握用 mypy 为类体系写出可被静态检查的、安全且易于维护的类型标注并能理解这些规则在 mypy 源码中的实际检查逻辑。从类属性注解开始让 mypy 替你抓属性不存在mypy 类型检查器会检测你是否访问了不存在的属性——这是编程中最常见的错误之一。要让这项检查生效实例属性和类属性必须在类内部被定义或初始化。mypy 会自动推断属性的类型class A: def __init__(self, x: int) - None: self.x x # Aha, attribute x of type int a A(1) a.x 2 # OK! a.y 3 # Error: A has no attribute y这有点类似于每个类都隐式定义了__slots__属性——但请注意这种约束只在类型检查阶段生效程序运行时并不会真正阻止你动态添加属性。从 mypy 的实现角度看成员访问会经过 checkmember.py 中analyze_member_access与analyze_var的逐项校验未声明的属性会直接触发has no attribute错误。在类体中显式声明属性类型你可以用类型注解在类体中显式声明变量的类型class A: x: list[int] # Declare attribute x of type list[int] a A() a.x [1] # OK与 Python 的通用规则一致定义在类体中的变量既可以作为类变量使用也可以作为实例变量使用下一节会说明如何用ClassVar注解覆盖这一默认行为。在方法中显式声明实例属性类型同样地你也可以在方法内为实例变量给出显式类型class A: def __init__(self) - None: self.x: list[int] [] def f(self) - None: self.y: Any 0一个关键限制你只能在方法内通过显式使用self赋值的方式来定义实例变量。通过其他别名赋值不会被识别为属性定义class A: def __init__(self) - None: self.y 1 # Define y a self a.x 1 # Error: x not defined这是 mypy 有意为之的设计只有self.xxx形式的赋值才会被追踪为类成员的声明别名引用无法建立属性与类的关联。注解init方法何时必须写- None__init__方法有些特殊——它不返回值最合适的表达方式是- None。不过由于很多人认为这有些冗余mypy 允许在至少有一个参数被注解的情况下省略__init__的返回类型声明。例如下面两个类的__init__都被视为完全注解class C1: def __init__(self) - None: self.var 42 class C2: def __init__(self, arg: int): self.var arg然而如果__init__既没有任何注解参数也没有返回类型注解它就会被当作未类型化的方法处理class C3: def __init__(self): # This body is not type checked self.var 42 abc注意上例中42 abc这种运行时必然抛TypeError的代码因为方法整体被视为未类型化mypy 不会检查其函数体。这是渐变类型gradual typing设计的一部分逐步引入类型标注时未注解的方法默认不参与检查。ClassVar显式声明仅类变量使用ClassVar[t]注解可以显式声明某个属性不应在实例上被赋值from typing import ClassVar class A: x: ClassVar[int] 0 # Class variable only A.x 1 # OK a A() a.x 1 # Error: Cannot assign to class variable x via instance print(a.x) # OK -- can be read through an instance在 mypy 源码 semanal.py 的check_classvar中语义分析阶段会把带ClassVar注解的赋值标记为is_classvar True并在后续检查中据此拒绝通过实例给类变量赋值的写法。同时需要说明并非所有类变量都必须标注ClassVar。没有ClassVar注解的属性仍然可以作为类变量使用只是 mypy 不会阻止它同时被当作实例变量class A: x 0 # Can be used as a class or instance variable A.x 1 # OK a A() a.x 1 # Also OKClassVar不是类不能用于isinstance或issubclass它不会改变 Python 运行时行为——只对 mypy 这类类型检查器有意义对人类读者也是很好的文档。省略方括号和变量类型可能出乎意料class A: y: ClassVar 0 # Type implicitly Any!此时属性类型会被隐式推断为Any。这一行为有些反直觉未来版本可能会改变。从源码看当ClassVar缺少类型参数时semanal.py 会在启用--disallow-any-generics时给出ClassVar without type argument becomes Any错误码type-arg提示。用 ClassVar 区分可调用类型属性的语义ClassVar在区分类变量与实例变量的可调用类型时尤其有用。实例方法本质上是在实例上调用自动传入self而类变量中的函数对象则按普通函数调用from collections.abc import Callable from typing import ClassVar class A: foo: Callable[[int], None] bar: ClassVar[Callable[[A, int], None]] bad: Callable[[A], None] A().foo(42) # OK A().bar(42) # OK A().bad() # Error: Too few arguments这里foo是实例方法类型隐式携带selfbar是存于类上的普通函数必须显式传入实例而bad被错误地标注成了只接受一个A实例参数的函数实际通过实例调用时会因缺少参数而报错。注意ClassVar的类型参数不能包含类型变量——若T是类型变量则ClassVar[T]和ClassVar[list[T]]都是非法的关于类型变量详见 generics.rst。重写静态类型方法签名兼容性检查当子类重写一个静态类型化的方法时mypy 会检查重写签名是否兼容class Base: def f(self, x: int) - None: ... class Derived1(Base): def f(self, x: str) - None: # Error: type of x incompatible ... class Derived2(Base): def f(self, x: int, y: int) - None: # Error: too many arguments ... class Derived3(Base): def f(self, x: int) - None: # OK ... class Derived4(Base): def f(self, x: float) - None: # OK: mypy treats int as a subtype of float ... class Derived5(Base): def f(self, x: int, y: int 0) - None: # OK: accepts more than the base ... # class method这里体现了面向对象类型系统的两条核心规则mypy 在 checker.py 中对 override 逐一比对参数与返回类型返回类型可以协变重写时可以把返回类型Iterable[int]替换为其子类型如list[int]参数类型可以逆变子类可以使用比基类更宽泛更一般的参数类型例如把int换成其父类型float。另外子类方法可以比基类接受更多参数只要它们有默认值如上例Derived5。用 override 显式标记重写为了保证重命名方法时代码仍然正确可以用override装饰器显式标记某个方法是在重写基类方法。override从 Python 3.12 起可从typing导入更早的 Python 版本则从typing_extensions导入。如果基类方法被重命名而重写方法未同步mypy 会报错from typing import override class Base: def f(self, x: int) - None: ... def g_renamed(self, y: str) - None: ... class Derived1(Base): override def f(self, x: int) - None: # OK ... override def g(self, y: str) - None: # Error: no corresponding base method found ...在 mypy 内部override会通过 semanal.py 将函数节点标记为is_explicit_override True随后 checker.py 的check_explicit_override_decorator会在基类中找不到对应方法时报错。你还可以通过--enable-error-code explicit-override该错误码定义于 errorcodes.py强制要求所有方法重写都必须使用override装饰器缺失即报错。这对于大型团队协作、防止基类方法改名后子类悄悄失去重写关系非常有用。用动态类型方法重写静态类型方法你也可以用动态类型无注解的方法重写静态类型的方法这样动态代码可以重写库类中的方法而无需关心其精确类型签名class Base: def inc(self, x: int) - int: return x 1 class Derived(Base): def inc(self, x): # Override, dynamically typed return hello # Incompatible with Base, but no mypy error如你所见这样做可能不安全由于注解在运行时没有任何效果mypy不会在运行时强制执行重写方法返回与原始返回类型兼容的值。依赖动态类型代码时需自行权衡。抽象基类ABC与多重继承mypy 完整支持 Python 的抽象基类。抽象类至少包含一个必须由具体非抽象子类实现的抽象方法或抽象属性。可以用abc.ABCMeta元类与abc.abstractmethod装饰器来定义抽象基类from abc import ABCMeta, abstractmethod class Animal(metaclassABCMeta): abstractmethod def eat(self, food: str) - None: pass property abstractmethod def can_walk(self) - bool: pass class Cat(Animal): def eat(self, food: str) - None: ... # Body omitted property def can_walk(self) - bool: return True x Animal() # Error: Animal is abstract due to eat and can_walk y Cat() # OK注意mypy即使省略ABCMeta元类也会检查未实现的抽象方法。这在元类可能引发运行时冲突的场景下非常有用——你可以只依赖abstractmethod让 mypy 做静态检查而不必真正引入元类。由于无法实例化 ABC它们最常见的用途是出现在类型注解中。例如下面这个方法接受任意包含任意动物具体Animal子类的实例的可迭代对象def feed_all(animals: Iterable[Animal], food: str) - None: for animal in animals: animal.eat(food)隐式抽象忘记实现抽象方法的常见陷阱Python 中 ABC 有一个重要特性某个类是否为抽象类在某种程度上是隐式的。在下面的例子中Derived因为从Base继承了抽象的f方法且没有显式实现它因此被当作抽象基类。Derived的定义不会产生任何 mypy 错误因为它是合法的 ABCfrom abc import ABCMeta, abstractmethod class Base(metaclassABCMeta): abstractmethod def f(self, x: int) - None: pass class Derived(Base): # No error -- Derived is implicitly abstract def g(self) - None: ... d Derived() # Error: Derived is abstract常见错误忘记实现抽象方法。如上所示类定义阶段不会报错但任何尝试构造实例的操作都会被标记为错误。这是 mypy 设计上的有意取舍——抽象类定义本身是合法的实例化才非法。抽象方法体与 super() 调用的安全性mypy 允许你省略抽象方法的方法体但若省略trivial body通过super()调用这类方法是不安全的from abc import abstractmethod class Base: abstractmethod def foo(self) - int: pass abstractmethod def bar(self) - int: return 0 class Sub(Base): def foo(self) - int: return super().foo() 1 # error: Call to abstract method foo of Base # with trivial body via super() is unsafe abstractmethod def bar(self) - int: return super().bar() 1 # This is OK however.该错误消息Call to abstract method ... with trivial body via super() is unsafe定义于 messages.py其依据是 nodes.py 中记录的trivial body概念...省略号、pass或raise NotImplementedError()都被视为 trivial body详见 semanal.py。有实际实现哪怕是返回0的抽象方法通过super()调用是安全的。多重继承与抽象属性一个类可以继承任意数量的类抽象的或具体的。与普通重写一致动态类型的方法可以重写或实现任意基类中定义的静态类型方法——包括抽象基类中定义的抽象方法。抽象属性既可以用普通property实现也可以用实例变量实现。slots的静态校验当类显式定义了__slots__时mypy 会检查所有被赋值的属性是否都属于__slots__的成员class Album: __slots__ (name, year) def __init__(self, name: str, year: int) - None: self.name name self.year year # Error: Trying to assign name released that is not in __slots__ of type Album self.released True my_album Album(Songs about Python, 2021)mypy 仅在同时满足以下三个条件时才检查对__slots__的属性赋值所有基类内建类除外都必须显式定义__slots__这与 Python 的运行时语义一致__slots__中不包含__dict__——如果包含__dict__任意属性都可以被设置与未定义__slots__时类似同样与 Python 语义一致__slots__中的所有值必须是字符串字面量。从实现层面看checker.py 的check_slots_definition会校验__slots__本身的类型必须是str或Iterable[str]而成员访问/赋值检查则在 checkmember.py 处通过if info.slots and name in info.slots判断属性是否落在__slots__白名单内超出即报错。小结围绕 mypy 的类注解本文覆盖了从属性声明到继承体系的完整规则链属性类体或self.xxx赋值定义属性mypy 自动推断类型动态添加未声明属性会被拒绝__init__推荐写- None只要有一个参数被注解即可省略返回注解否则方法体不参与类型检查ClassVar显式区分类变量与实例变量注意省略类型参数会退化为Any且不能与类型变量组合方法重写返回类型可协变、参数类型可逆变override装饰器配合explicit-override错误码可在重命名基类方法时及时暴露问题抽象基类mypy 静态检查抽象方法实现情况忽略ABCMeta也能检查省略方法体的抽象方法经super()调用不安全__slots__满足三个前提条件时mypy 会像运行时一样限制属性赋值范围。这些规则是 mypy 类型系统中的基础构件与 builtin_types.rst、generics.rst、protocols.rst 等文档共同构成了完整的类类型体系参考。将以上实践应用到你的类设计中即可让 mypy 在开发阶段发现大量潜在的属性访问与继承错误。【免费下载链接】mypyOptional static typing for Python项目地址: https://gitcode.com/GitHub_Trending/my/mypy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门