类变量、成员变量,与注解

last modify

摘要

  • Python 对定义在类体内的变量属于类变量还是成员变量(下称实例变量)的区分并不十分明确,这里记录一下我了解到的最佳实践。

    self.x 形式定义的就是成员变量,没有歧义;

  • 定义在类内的变量,即是类变量,也是成员变量;因为两个同名的变量会分别被定义在了类和实例的 __dict__ 中;它们之间除了同名没有其他关系;

    实例变量在被修改前并不会被真的定义出来,而是访问同名的类变量(相当于默认值);一旦被修改,就会立即在实例中创建一个同名的实例变量,将两者隔离开来;

  • 因此正确的做法是,通过实例对象访问那就是实例变量,通过类对象访问的就是类变量;

    class A:
        x: int = 1
    
    a = A()
    print(A.x)  # 1  # 类变量
    print(a.x)  # 1  # 实例变量(此时并没有被真的创建,而是调用类变量作为默认值)
    print('x' in a.__dict__)  # False  # 实例变量并没有被真的定义出来
    
    a.x += 1  # 实例变量被修改
    print(A.x)  # 1  # 类变量
    print(a.x)  # 2  # 实例变量
    print('x' in a.__dict__)  # True  # 实例变量一旦被修改,就会立即被创建出来

基本使用

  • 下面是一段来自官方的代码:

    class BasicStarship:
        captain: str = 'Picard'               # instance variable with default
        damage: int                           # instance variable without default
        stats: ClassVar[Dict[str, int]] = {}  # class variable

    PEP 526 – Syntax for Variable Annotations | peps.python.org

  • captain 应该是“类变量”(因为保存在 BasicStarship.__dict__ 中),但官方的注释却是“实例变量”(成员变量)。

  • 我理解其中的逻辑可能是这样的:

    • bs = BasicStarship()

    • bs.captain 的方式访问 captain,会按照 bs.__dict__ -> BasicStarship.__dict__ 的路径依次查找;

    • 而任何通过 bs.captaincaptain 的修改,都会在 bs 中添加一个新的成员变量,同时不会影响 BasicStarship.captain

      bs = BasicStarship()
      print(bs.captain)                # Picard
      print(BasicStarship.captain)     # Picard
      print('captain' in bs.__dict__)  # False
      
      bs.captain += '_local'           # setattr(bs, 'captain', BasicStarship.captain + '_local')
      print(bs.captain)                # Picard_local
      print(BasicStarship.captain)     # Picard
      print('captain' in bs.__dict__)  # True
  • 所以我认为定义在类体内的变量(已初始化),即是类变量,也是实例变量;因为它被分别定义在了类和实例的 __dict__ 中(虽然实例变量没有被立即定义),它们之间除了同名之外没有其他关系;

  • 因此正确的做法应该是,通过实例对象访问那就是实例变量(bs.captain),通过类对象访问的就是类变量(BasicStarship.captain);

最佳实践

TODO

参考

Last updated