Python实例变量的类型提示约定

35

我不确定Python中实例变量的类型提示惯例 - 我一直在__init__构造函数参数中进行类型提示,就像这里看到的一样

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value`

但是我也看到了PEP约定将实例变量注释为以下内容(下面是代码片段),然后在__init__参数中进行类型提示:

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

    def __init__(self, damage: int, captain: str = None):
        self.damage = damage
        if captain:
            self.captain = captain  # Else keep the default

最后,在PEP 526文章中,为了方便和惯例,可以执行以下操作:
class Box(Generic[T]):
    def __init__(self, content):
        self.content: T = content

以上两个代码片段都来自这里

那么,这两种惯例中是否有一种更好/更广为接受,我应该尽量遵循(更好的可读性等)?


3
直到现在我才不知道PEP 526,谢谢你。 - chepner
17
为什么第二个例子中captaindamage是实例变量?它们不也是类变量吗?还是因为它们在init方法中被修改所以变成了实例变量?如果我有一个列表,并使用list.append()进行修改,那么这个修改将共享所有实例,因此它仍然是类变量。 - Asara
4个回答

17
我建议在大多数情况下使用第一种版本,其中您将类型分配给__init__方法的参数。
那个特定的方法具有最少的冗余,同时允许类型检查器验证您在代码的其他地方正确调用了该__init__方法。
当您的__init__方法变得足够复杂以至于以下一项或多项适用时,我建议使用第二或第三个版本,在其中明确注释您的字段(在__init__内部或外部):
1.不再那么直观您的字段到底是什么 2.您的参数和字段之间不再有一对一的映射 3.您有复杂的初始化逻辑,使得您的字段如何被赋值变得模糊。
然而,我不清楚第二个或第三个版本哪一个更好-我个人更喜欢第三个版本,因为它在概念上更加清晰,并且似乎不会混淆实例与类属性的概念,但我不能否认第二个版本看起来更简洁。
我在“typing” Gitter 频道上询问了此事,并从 Guido(Python 的创建者,目前正在开发 mypy 和与 typing 相关的东西)那里得到了以下回复:
“人们对此有强烈的意见。我确实更喜欢将属性注释放在类体中,而不是散布在 __init__ 和其他方法中。我也认为,随着 PEP 526 的出现,这将成为未来的趋势(还有基于类的 NamedTuple 声明,可能还有 https://github.com/ericvsmith/dataclasses 等等)。”
引用链接
因此,似乎第二个版本比第三个版本更受推荐,以后定义类的方式将更深入地融入 Python 语言本身!

编辑:PEP 557,数据类已被最近接受,并且似乎正在顺利进行中以包含在Python 3.7中。


13

@Asara

根据Python 3.8.10 / Mypy 0.910(2021年9月)的更新,区分类定义中实例变量的类型注释和类(静态)变量的声明可以通过是否有默认值来判断。如果没有给变量赋默认值(例如,x: int),Python将表达式视为类型注释;如果赋了默认值(例如,x: int = 42),Python将表达式视为类(静态)变量的声明。

可以使用ClassVar语法在类定义中创建类(静态)变量的类型注释。如果不给变量赋默认值(例如,y: ClassVar[int]),将不会创建实际的类(静态)变量;如果赋了默认值(例如,y: ClassVar[int] = 69),则将创建一个实际的类(静态)变量。


3
如果默认值的赋值会产生差异,那么为什么在上面从PEP 526中提取的示例中,具有默认值“Picard”的captain变量被描述为“带默认值的实例变量”,而不是被视为类变量呢? - pawel
@pawel:是的,我认为这很令人困惑。captain 明显是一个类变量(我进行了测试)。我最好的猜测是他们将其称为 instance variable with default 是因为它是如何被使用的。请注意,如果没有提供值,构造函数不会设置 self.captain。但是 self.captainsome_instance_of_BasicStarship.captain 仍然可以工作(并返回 "Picard"),因为它会查找类上的值。 - Mark Doliner
我认为这确实令人困惑。不过,@MarkDoliner提供的这个简单示例可能会有所帮助。 - starriet

2

我建议您继续使用LoggedVar中的方法,它遵循Python的通用规则,因此可以减少混淆。

BasicStarShip类通过将变量移出__init__函数来改变变量的作用域。在那里声明了船长,BasicStarShip.captain将返回'Picard'

PEP 526注释很好读,但这是一个针对特定情况(即__init__函数)的新规则。根据 Python之禅:

"特殊情况并不足以打破规则。"


0

@Gary和其他可能感到困惑的人,

我认为区别在于使用selfClassName

尝试运行这两个示例:(两个示例都使用默认值cnt = 0,因此应该是类变量)

live code

  1. self.cnt += 1 -> 像实例变量一样工作,尽管已经赋值了一个值(cnt = 0)。
class Test:
    cnt = 0 
    def __init__(self):
        # Test.cnt+=1 
        self.cnt+=1 
    def p(self):
        print(f'self.cnt:{self.cnt}, Test.cnt:{Test.cnt}') 

t1 = Test() 
t2 = Test() 
t1.p() 
t2.p() 

# outputs:
# self.cnt:1, Test.cnt:0
# self.cnt:1, Test.cnt:0

  • Test.cnt += 1 -> 如预期地作为类变量工作(因为已分配了一个值)。
  • class Test:
        cnt = 0 
        def __init__(self):
            Test.cnt+=1 
            # self.cnt+=1 
        def p(self):
            print(f'self.cnt:{self.cnt}, Test.cnt:{Test.cnt}') 
    
    t1 = Test() 
    t2 = Test() 
    t1.p() 
    t2.p() 
    
    # outputs:
    # self.cnt:2, Test.cnt:2
    # self.cnt:2, Test.cnt:2
    

    出于简化,没有类型注释 - 使用cnt: int = 0的结果是相同的。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接