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

And*_*ndy 19 python class instance-variables instance python-attrs

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

对于这个问题,我找不到更好的地方,尽管可能有讨厌这些问题的喷子,我还是发布了这个。如果这让你感到不安,很抱歉。我只是想学习,而不是惹你生气。

题:

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

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
Run Code Online (Sandbox Code Playgroud)

我在其他项目中发现,当类属性只出现在类方法中时,它们可能会被埋没,而且还有很多事情要做。

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

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

谢谢

安迪

jfe*_*ard 18

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

看看你的类的两个稍微修改过的版本,一个 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

MyClass1和之间的唯一区别MyClass2是第一个results在构造函数中初始化,而第二个在set_results. 你的班级的用户来了(通常是你,但不总是)。每个人都知道你不能信任用户(即使是你):

MyClass1("df").get_results()
# returns None
Run Code Online (Sandbox Code Playgroud)

或者

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

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

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

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

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

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
         
Run Code Online (Sandbox Code Playgroud)

(您也可以使用 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'
Run Code Online (Sandbox Code Playgroud)

优点很明显:

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

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

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

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


And*_*ndy 5

在与有经验的程序员进行大量研究和讨论之后,请参阅下面我认为对这个问题最 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
Run Code Online (Sandbox Code Playgroud)

描述我学到的、改变的以及为什么:

  1. 所有类属性都应包含在init(构造函数)方法中。这是为了确保可读性和帮助调试。

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

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

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

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

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

    (我将设置结果的方法的名称更改为 generate_results() 以避免混淆并为 getter 方法释放 .results)

  4. 如果类中有其他方法需要使用 self._results,但只有在正确分配的情况下,您才能使用 self.results,这样故障安全提示就如上所示。

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

  • 这是解决问题的一个很好的解决方案(对于我目前正在努力解决的问题非常有帮助,所以感谢您发布它)。改进解决方案的一个建议是:引发一个更具体的异常,而不仅仅是“Exception”。如果引发通用“Exception”,那么当您在其他地方检索属性时,必须在“try/ except”块中捕获**所有**类型的错误。如果您提出更具体的异常,例如“AttributeError”,那么处理起来会容易得多。 (7认同)