在 __init__ 中为用户类设置默认/空属性

新手上路,请多包涵

我的编程水平不错,并从这里的社区中获得了很多价值。然而,我从来没有接受过太多编程方面的学术教学,也没有在真正有经验的程序员旁边工作过。因此,我有时会与“最佳实践”作斗争。

问题:

当我创建一个新类时,我是否应该在 __init__ 中设置所有实例属性,即使它们是 None 并且实际上稍后在类方法中赋值?

请参阅下面的示例,了解 --- 的属性 results MyClass

 class MyClass:
    def __init__(self,df):
          self.df = df
          self.results = None

    def results(df_results):
         #Imagine some calculations here or something
         self.results = df_results

我在其他项目中发现,当类属性仅出现在类方法中时,类属性可能会被掩埋,并且有很多事情要做。

那么对于经验丰富的专业程序员来说,标准做法是什么?为了便于阅读,您会在 __init__ 中定义所有实例属性吗?

如果有人有关于我在哪里可以找到这些原则的材料的任何链接,那么请把它们放在一个答案中,我们将不胜感激。我知道 PEP-8 并且已经在上面多次搜索了我的问题,但找不到任何人涉及这个。

谢谢

安迪

原文由 Andy 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 623
2 个回答

在与经验丰富的程序员进行大量研究和讨论之后,请在下面查看我认为对这个问题最 Pythonic 的解决方案。我首先包括了更新的代码,然后是叙述:

 class MyClass:
    def __init__(self,df):
          self.df = df
          self._results = None

    @property
    def results(self):
        if self._results is None:
            raise Exception('df_client is None')
        return self._results

    def generate_results(self, df_results):
         #Imagine some calculations here or something
         self._results = df_results

描述我所学到的、改变的以及原因:

  1. 所有类属性都应包含在 __init__ (初始化程序)方法中。这是为了确保可读性和帮助调试。

  2. 第一个问题是您不能在 Python 中创建私有属性。一切都是公开的,因此可以访问任何部分初始化的属性(例如设置为 None 的结果)。指示私有属性的约定是在前面放置一个前导下划线,因此在这种情况下,我将其更改为 self.resultsself._results

请记住,这只是惯例,仍然可以直接访问 self._results 。然而,这是处理伪私有属性的 Pythonic 方式。

  1. 第二个问题是有一个部分初始化的属性被设置为无。因为它被设置为 None ,正如下面的@jferard 解释的那样,我们现在已经失去了快速失败提示并添加了一层混淆来调试代码。

为了解决这个问题,我们添加了一个 getter 方法。这可以看作上面的函数 results() 上面有 @property 装饰器。

这是一个在调用时检查 self._results 是否为 None 的函数。如果是这样,它将引发异常(故障安全提示),否则它将返回该对象。 @property 装饰器将调用样式从函数更改为属性,因此用户必须在 MyClass 的实例上使用 .results 就像任何其他属性一样。

(我将设置结果的方法的名称更改为 `generate_results()` 以避免混淆并释放 `.results` 用于 getter 方法)
  1. 如果您在类中有其他方法需要使用 self._results ,但只有在正确分配时,您才可以使用 self.results ,这样故障安全提示就会被嵌入多于。

我还建议阅读@jferard 对这个问题的回答。他深入探讨了问题和一些解决方案。我添加我的答案的原因是,我认为在很多情况下,以上就是你所需要的(以及 Pythonic 的方式)。

原文由 Andy 发布,翻译遵循 CC BY-SA 4.0 许可协议

我认为你应该避免这两种解决方案。仅仅是因为您应该避免创建未初始化或部分初始化的对象,除了我稍后将概述的一种情况。

看一下你的类的两个稍微修改的版本,一个 setter 和一个 getter:

 class MyClass1:
    def __init__(self, df):
          self.df = df
          self.results = None

    def set_results(self, df_results):
         self.results = df_results

    def get_results(self):
         return self.results

class MyClass2:
    def __init__(self, df):
          self.df = df

    def set_results(self, df_results):
         self.results = df_results

    def get_results(self):
         return self.results

The only difference between MyClass1 and MyClass2 is that the first one initializes results in the constructor while the second does it in set_results .你班级的用户来了(通常是你,但不总是)。每个人都知道你不能信任用户(即使是你):

 MyClass1("df").get_results()
# returns None

或者

MyClass2("df").get_results()
# Traceback (most recent call last):
# ...
# AttributeError: 'MyClass2' object has no attribute 'results'

你可能认为第一种情况更好,因为它不会失败,但我不同意。在这种情况下,我希望程序能够快速失败,而不是进行长时间的调试以找出发生了什么。因此, 第一个答案的第一部分是: _不要将未初始化的字段设置为 None ,因为您丢失了快速失败提示_。

但这不是全部答案。无论您选择哪个版本,都会遇到一个问题:该对象未被使用,也不应该被使用,因为它没有完全初始化。您可以将文档字符串添加到 get_results"""Always use set_results **BEFORE** this method""" 。不幸的是,用户也不阅读文档字符串。

您的对象中未初始化的字段有两个主要原因:1.您(暂时)不知道该字段的值; 2. 您想避免扩展操作(计算、文件访问、网络……),又名“惰性初始化”。这两种情况在现实世界中都会遇到,并且与仅使用完全初始化对象的需求相冲突。

令人高兴的是,这个问题有一个有据可查的解决方案:设计模式,更准确地说是 创建模式。在您的情况下,工厂模式或建造者模式可能就是答案。例如:

 class MyClassBuilder:
    def __init__(self, df):
          self._df = df # df is known immediately
          # GIVE A DEFAULT VALUE TO OTHER FIELDS to avoid the possibility of a partially uninitialized object.
          # The default value should be either:
          # * a value passed as a parameter of the constructor ;
          # * a sensible value (eg. an empty list, 0, etc.)

    def results(self, df_results):
         self._results = df_results
         return self # for fluent style

    ... other field initializers

    def build(self):
        return MyClass(self._df, self._results, ...)

class MyClass:
    def __init__(self, df, results, ...):
          self.df = df
          self.results = results
          ...

    def get_results(self):
         return self.results

    ... other getters


(您也可以使用 Factory,但我发现 Builder 更灵活)。让我们给用户第二次机会:

 >>> b = MyClassBuilder("df").build()
Traceback (most recent call last):
...
AttributeError: 'MyClassBuilder' object has no attribute '_results'
>>> b = MyClassBuilder("df")
>>> b.results("r")
... other fields iniialization
>>> x = b.build()
>>> x
<__main__.MyClass object at ...>
>>> x.get_results()
'r'

优点很明显:

  1. 检测和修复创建失败比后期使用失败更容易;
  2. 您不会在野外发布对象的未初始化(因此可能具有破坏性)版本。

Builder 中存在未初始化的字段并不矛盾:这些字段在设计上是未初始化的,因为 Builder 的作用是初始化它们。 (实际上,这些字段是 Builder 的某种 forein 字段。)这就是我在介绍中谈到的情况。在我看来,它们应该设置为默认值(如果存在)或保持未初始化状态,以便在您尝试创建不完整的对象时引发异常。

我的回答的第二部分: _使用创建模式来确保对象被正确初始化_。

旁注:当我看到一个带有 getter setter 的类时,我非常怀疑。我的经验法则是:始终尝试将它们分开,因为当它们相遇时,物体会变得不稳定。

原文由 jferard 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题