【问题标题】:How should I declare default values for instance variables in Python?我应该如何在 Python 中声明实例变量的默认值?
【发布时间】:2011-02-10 11:52:27
【问题描述】:

我应该给我的班级成员这样的默认值吗:

class Foo:
    num = 1

还是这样?

class Foo:
    def __init__(self):
        self.num = 1

this question 中我发现在这两种情况下,

bar = Foo()
bar.num += 1

是一个定义明确的操作。

我知道第一种方法会给我一个类变量,而第二种方法不会。但是,如果我不需要类变量,而只需要为我的实例变量设置一个默认值,那么这两种方法都一样好吗?还是其中一个比另一个更“pythonic”?

我注意到的一件事是,在 Django 教程中,they use the second method to declare Models. 个人认为第二种方法更优雅,但我想知道“标准”方法是什么。

【问题讨论】:

    标签: python class oop


    【解决方案1】:

    这两个 sn-ps 做不同的事情,所以这不是品味问题,而是在您的上下文中正确的行为问题。 Python documentation 解释了区别,但这里有一些例子:

    展品 A

    class Foo:
      def __init__(self):
        self.num = 1
    

    这会将num 绑定到 Foo 实例。对此字段的更改不会传播到其他实例。

    因此:

    >>> foo1 = Foo()
    >>> foo2 = Foo()
    >>> foo1.num = 2
    >>> foo2.num
    1
    

    展品 B

    class Bar:
      num = 1
    

    这会将num 绑定到 Bar 。更改已传播!

    >>> bar1 = Bar()
    >>> bar2 = Bar()
    >>> bar1.num = 2 #this creates an INSTANCE variable that HIDES the propagation
    >>> bar2.num
    1
    >>> Bar.num = 3
    >>> bar2.num
    3
    >>> bar1.num
    2
    >>> bar1.__class__.num
    3
    

    实际答案

    如果我不需要类变量,而只需要为我的实例变量设置一个默认值,这两种方法都一样好吗?或者其中一个比另一个更“pythonic”?

    展览 B 中的代码显然是错误的:为什么要将类属性(实例创建时的默认值)绑定到单个实例?

    展览 A 中的代码没问题。

    如果您想在构造函数中为实例变量提供默认值,我会这样做:

    class Foo:
      def __init__(self, num = None):
        self.num = num if num is not None else 1
    

    ...甚至:

    class Foo:
      DEFAULT_NUM = 1
      def __init__(self, num = None):
        self.num = num if num is not None else DEFAULT_NUM
    

    ...甚至:(更可取,但当且仅当您正在处理不可变类型时!)

    class Foo:
      def __init__(self, num = 1):
        self.num = num
    

    这样你就可以做到:

    foo1 = Foo(4)
    foo2 = Foo() #use default
    

    【讨论】:

    • 是本例中使用的init方法,与下划线___init__underscore不同,用于初始化@badp
    • 第一个例子不应该是def __init__(self, num = None)
    • 你不能只添加以下内容就完成了吗? def __init__(self, num = None): self.num = num if num else DEFAULT_NUM
    【解决方案2】:

    使用类成员作为实例变量的默认值并不是一个好主意,而且我还是第一次看到有人提到这个想法。它适用于您的示例,但在很多情况下可能会失败。例如,如果值是可变的,则在未修改的实例上对其进行变异将改变默认值:

    >>> class c:
    ...     l = []
    ... 
    >>> x = c()
    >>> y = c()
    >>> x.l
    []
    >>> y.l
    []
    >>> x.l.append(10)
    >>> y.l
    [10]
    >>> c.l
    [10]
    

    【讨论】:

    • 除了第一个 [] 在那里被克隆; x.ly.l 都是邪恶的名字和不同的直接值链接到同一个所指对象。 (语言律师条款编造)
    【解决方案3】:

    使用类成员来提供默认值非常有效,只要您小心只使用不可变值即可。如果您尝试使用列表或字典来执行此操作,那将是非常致命的。它也适用于实例属性是对类的引用的情况,只要默认值为 None。

    我已经看到这种技术在 repoze 中得到了非常成功的应用,repoze 是一个运行在 Zope 之上的框架。这里的优点不仅在于当您的类被持久化到数据库时,只需要保存非默认属性,而且当您需要将新字段添加到架构中时,所有现有对象都会看到具有默认值的新字段值而无需实际更改存储的数据。

    我发现它在更一般的编码中也能很好地工作,但它是一种风格。使用你最喜欢的任何东西。

    【讨论】:

      【解决方案4】:

      扩展 bp 的答案,我想向您展示他所说的不可变类型的含义。

      首先,这没关系:

      >>> class TestB():
      ...     def __init__(self, attr=1):
      ...         self.attr = attr
      ...     
      >>> a = TestB()
      >>> b = TestB()
      >>> a.attr = 2
      >>> a.attr
      2
      >>> b.attr
      1
      

      但是,这仅适用于不可变(不可更改)类型。如果默认值是可变的(意味着它可以被替换),则会发生这种情况:

      >>> class Test():
      ...     def __init__(self, attr=[]):
      ...         self.attr = attr
      ...     
      >>> a = Test()
      >>> b = Test()
      >>> a.attr.append(1)
      >>> a.attr
      [1]
      >>> b.attr
      [1]
      >>> 
      

      注意ab 都有一个共享属性。这通常是不需要的。

      当类型可变时,这是为实例变量定义默认值的 Pythonic 方式:

      >>> class TestC():
      ...     def __init__(self, attr=None):
      ...         if attr is None:
      ...             attr = []
      ...         self.attr = attr
      ...     
      >>> a = TestC()
      >>> b = TestC()
      >>> a.attr.append(1)
      >>> a.attr
      [1]
      >>> b.attr
      []
      

      我的第一个 sn-p 代码起作用的原因是,对于不可变类型,Python 会在您需要时创建它的新实例。如果您需要将 1 加到 1,Python 会为您创建一个新的 2,因为旧的 1 无法更改。我相信,原因主要是因为散列。

      【讨论】:

      • 我最近遇到了这种为可变属性实现默认值的简单得多的方法。只需在__init__ 方法中写入self.attr = attr or []。它达到了相同的结果(我认为)并且仍然清晰易读。
      • 对不起各位。我对上述评论中的建议进行了更多研究,显然它并不完全相同,是not recommended
      • 为什么TestC类有空括号?这是 Python 2 的遗产吗?
      【解决方案5】:

      您还可以将类变量声明为 None 以防止传播。当您需要一个定义良好的类并希望防止 AttributeErrors 时,这很有用。 例如:

      >>> class TestClass(object):
      ...     t = None
      ... 
      >>> test = TestClass()
      >>> test.t
      >>> test2 = TestClass()
      >>> test.t = 'test'
      >>> test.t
      'test'
      >>> test2.t
      >>>
      

      如果你需要默认值:

      >>> class TestClassDefaults(object):
      ...    t = None
      ...    def __init__(self, t=None):
      ...       self.t = t
      ... 
      >>> test = TestClassDefaults()
      >>> test.t
      >>> test2 = TestClassDefaults([])
      >>> test2.t
      []
      >>> test.t
      >>>
      

      当然,仍然遵循其他答案中有关使用可变类型与不可变类型作为__init__ 中的默认值的信息。

      【讨论】:

        【解决方案6】:

        dataclasses 是 Python 3.7 中添加的一项功能,现在还有另一种(非常方便的)方法可以实现在类实例上设置默认值。装饰器dataclass 会自动在你的类上生成一些方法,比如构造函数。正如上面链接的文档所述,“在这些生成的方法中使用的成员变量是使用 PEP 526 type annotations 定义的。

        考虑到 OP 的例子,我们可以这样实现它:

        from dataclasses import dataclass
        
        @dataclass
        class Foo:
            num: int = 0
        
        

        在构造此类类型的对象时,我们可以选择覆盖该值。

        print('Default val: {}'.format(Foo()))
        # Default val: Foo(num=0)
        print('Custom val: {}'.format(Foo(num=5)))
        # Custom val: Foo(num=5)
        

        【讨论】:

        • 应将num 属性标记为dataclasses.field,否则@badp 中描述的传播行为(抱歉,无法正确提及!) 图表B 仍然存在。 python from dataclasses import dataclass, field @dataclass class Foo: num: int = field(default=0)
        • 嗨@kva1966 文档说“dataclass() 装饰器检查类以查找字段。字段被定义为具有类型注释的类变量”。因此我的理解是int类型注解就足够了。
        • 我很抱歉,你是对的。我可能在之前的测试中遗漏了一些东西。类型注释是与众不同的。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2010-12-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-17
        相关资源
        最近更新 更多