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

Posted

技术标签:

【中文标题】在 __init__ 中为用户类设置默认/空属性【英文标题】:Setting default/empty attributes for user classes in __init__ 【发布时间】:2019-09-11 23:45:20 【问题描述】:

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

我找不到更好的地方来回答这个问题,尽管有可能讨厌这类问题的煽动者,我还是发布了这个。很抱歉,如果这让你不高兴。我只是想学习,而不是惹你生气。

问题:

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

MyClass 的属性results 参见下面的示例:

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 并且已经在上面搜索了我的问题几次,但找不到任何人接触到这个。

谢谢

安迪

【问题讨论】:

我会初始化 __init__ 中的所有内容,即使最初是 None。它明确了实例数据属性是什么,并在使用实例时防止AttributeErrorsself 进行处理(当然其他例外情况仍然可能存在)。 首先,永远不要为提问而道歉。 基于第一条评论:如果您这样做,您稍后可能会想,这是否已初始化?您的属性,并想知道您是否可以在不通过某种方法进行存在检查的情况下从中读取。如果所有内容都在 __init__ 中,那么您知道 (a.) 它都在那里,并且 (b.) 它已在最合理的位置进行初始化,即您首先要查看的位置。 作为另一种观点:听起来您正在定义一类对象,其中某些属性在该类中的某些对象上无效。从面向对象的角度来看,这是有问题的。如果您有一个计算结果的方法,也许它应该返回一个表示结果的对象,而不是改变当前对象?如果您避免需要在__init__ 之外定义属性,那么这个问题就会消失。 感谢丹尼尔的评论,但我没有这样做。所有属性在所有实例上都有效,只是有些属性稍后通过方法而不是 init 分配值 【参考方案1】:

在与经验丰富的程序员进行大量研究和讨论后,请参阅下面我认为对这个问题最符合 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

描述我的学习、改变和原因:

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

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

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

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

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

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

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

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

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

【讨论】:

您好,请您帮我解决这个问题***.com/questions/68792725/… 这是一个很好的问题解决方案(对我目前正在努力解决的问题非常有帮助,所以感谢您发布它)。改进解决方案的一个建议:提出一个比Exception 更具体的异常。如果您提出一个通用的Exception,那么当您在其他地方检索该属性时,您必须在try/except 块中捕获所有 种错误。如果你提出一个更具体的异常,例如AttributeError,它会更容易处理。【参考方案2】:

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

看看你的类的两个稍微修改过的版本,一个 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

MyClass1MyClass2 之间的唯一区别是第一个在构造函数中初始化results,而第二个在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. 你想避免扩展操作(计算、文件访问、网络……),也就是“惰性初始化”。这两种情况在现实世界中都会遇到,并且需要使用完全初始化的对象。

幸运的是,这个问题有一个有据可查的解决方案:设计模式,更准确地说是Creational patterns。在您的情况下,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
         

(您也可以使用 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'

优势很明显:

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

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

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

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

【讨论】:

感谢@jferard,一个非常有帮助的贯穿始终。最后一点,你为什么不喜欢一个有 getter 和 setter 的类呢?我认为这就是大多数人应用它们的方式。你如何区分它们? @Andy 我想这是因为这句话,这个答案被否决了,因此我会尽量说清楚。这个想法是,当大多数对象是不可变的时,更容易理解(和测试)程序。如果你有 getter 和 setter,对象基本上是可变的,它们的当前状态通常是不确定的(如果你的程序是并发的,情况会更糟)。 有时,您确实需要可变对象,但大多数时候,您需要setter 来初始化对象然后 getter 来使用对象。在这种情况下,创建模式会将 setter(例如在构建器中)与 getter 隔离开,并且创建的对象将是不可变的,如给定示例中所示。这消除了对象延迟初始化或不需要的突变的风险,并使测试变得容易。 感谢@jferard 的跟进。我需要再考虑一下。我认为 OOP 的核心功能之一是修改实例化对象的属性以实现程序的目标,即它们是可变的。我知道如果您的对象是不可变的,那么调试会更容易,但是您的编码风格肯定会变得更类似于函数式语言吗?如果我在这里的评论离题很远,请原谅我的无知! @BubbleMaster PyCharm 是对的。请注意,我写的是评论# give a default value to other fields if possible。我应该使用更强有力的措辞。如果您没有为所有字段提供默认值,则会收到此警告,因为该对象可能未初始化。 MyClassBuyilder().build() 应该返回一个有效的对象(就像默认构造函数那样)。查看我的编辑。【参考方案3】:

要了解在__init__ 中初始化属性的重要性(或不重要),让我们以您的类MyClass 的修改版本为例。课程的目的是在给定学生姓名和分数的情况下计算一门学科的成绩。您可以在 Python 解释器中跟随。

>>> class MyClass:
...     def __init__(self,name,score):
...         self.name = name
...         self.score = score
...         self.grade = None
...
...     def results(self, subject=None):
...         if self.score >= 70:
...             self.grade = 'A'
...         elif 50 <= self.score < 70:
...             self.grade = 'B'
...         else:
...             self.grade = 'C'
...         return self.grade

这个类需要两个位置参数namescore必须提供这些参数来初始化类实例。没有这些,类对象x 无法实例化,TypeError 将被引发:

>>> x = MyClass()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() missing 2 required positional arguments: 'name' and 'score'

此时,我们知道我们必须提供学生的name 和一个科目的score,但grade 现在并不重要,因为这将在稍后计算,在results 方法中。所以,我们只使用self.grade = None 并且不将其定义为位置参数。让我们初始化一个类实例(对象):

>>> x = MyClass(name='John', score=70)
>>> x
<__main__.MyClass object at 0x000002491F0AE898>

&lt;__main__.MyClass object at 0x000002491F0AE898&gt; 确认类对象x 已在给定内存位置成功创建。现在,Python 提供了一些有用的内置方法来查看创建的类对象的属性。其中一种方法是__dict__。你可以阅读更多关于它here:

>>> x.__dict__
'name': 'John', 'score': 70, 'grade': None

这清楚地给出了所有初始属性及其值的dict 视图。请注意,grade 具有在 __init__ 中分配的 None 值。

让我们花点时间了解__init__ 的作用。有很多 answers 和在线资源可以解释这种方法的作用,但我会总结一下:

__init__ 一样,Python 有另一个名为__new__() 的内置方法。当你创建像x = MyClass(name='John', score=70) 这样的类对象时,Python 内部首先调用__new__() 来创建类MyClass 的新实例,然后调用__init__ 来初始化属性namescore。当然,在这些内部调用中,当 Python 找不到所需位置参数的值时,它会引发错误,正如我们在上面看到的那样。换句话说,__init__ 初始化属性。您可以像这样为namescore 分配新的初始值:

>>> x.__init__(name='Tim', score=50)
>>> x.__dict__
'name': 'Tim', 'score': 50, 'grade': None

也可以访问如下的单个属性。 grade 不提供任何信息,因为它是 None

>>> x.name
'Tim'
>>> x.score
50
>>> x.grade
>>>

results 方法中,您会注意到subject“变量”被定义为None,一个位置参数。此变量的范围仅在此方法内。出于演示的目的,我在此方法中明确定义了subject,但这也可以在__init__ 中初始化。但是如果我尝试使用我的对象访问它会怎样:

>>> x.subject
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'MyClass' object has no attribute 'subject'

当 Python 在类的命名空间中找不到属性时,它会引发 AttributeError。如果您不初始化 __init__ 中的属性,则当您访问可能仅对类的方法本地的未定义属性时,可能会遇到此错误。在此示例中,在 __init__ 中定义 subject 可以避免混淆,并且这样做是完全正常的,因为它也不需要任何计算。

现在,让我们拨打results 看看我们得到了什么:

>>> x.results()
'B'
>>> x.__dict__
'name': 'Tim', 'score': 50, 'grade': 'B'

这会打印分数的等级,并在我们查看属性时注意,grade 也已更新。从一开始,我们就清楚地了解了初始属性以及它们的值是如何变化的。

但是subject 呢?如果我想知道蒂姆在数学上的得分和成绩,我可以很容易地访问scoregrade,就像我们之前看到的那样,但是我怎么知道这个主题呢?因为,subject 变量在 results 方法的范围内是局部的,我们可以只使用 return 的值 subject。更改results方法中的return语句:

def results(self, subject=None):
    #<---code--->
    return self.grade, subject

让我们再次致电results()。我们得到了一个包含成绩和学科的元组。

>>> x.results(subject='Math')
('B', 'Math')

要访问元组中的值,让我们将它们分配给变量。在 Python 中,可以将集合中的值分配给同一表达式中的多个变量,前提是变量的数量等于集合的长度。在这里,长度只有两个,所以我们可以在表达式的左边有两个变量:

>>> grade, subject = x.results(subject='Math')
>>> subject
'Math'

所以,我们有了它,尽管它需要几行额外的代码才能获得subject。使用x.&lt;attribute&gt; 访问属性时,使用点运算符一次访问所有这些属性会更直观,但这只是一个示例,您可以尝试使用在__init__ 中初始化的subject

接下来,考虑有很多学生(比如 3 个),我们需要数学的姓名、分数和成绩。除了主题之外,所有其他都必须是某种集合数据类型,例如可以存储所有姓名、分数和等级的list。我们可以像这样初始化:

>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40])
>>> x.name
['John', 'Tom', 'Sean']
>>> x.score
[70, 55, 40]

乍一看这似乎很好,但是当您再看一下(或其他一些程序员)在 __init__ 中的 namescoregrade 的初始化时,无法判断他们需要一个集合数据类型。这些变量也被命名为单数,这使得它们可能只是一些可能只需要一个值的随机变量更加明显。程序员的目的应该是通过描述性变量命名、类型声明、代码 cmets 等方式,使意图尽可能清晰。考虑到这一点,让我们更改__init__ 中的属性声明。在我们满足于良好的良好定义的声明之前,我们必须注意我们如何声明默认参数。


编辑:可变默认参数的问题:

现在,在声明默认参数时,我们必须注意一些“陷阱”。考虑以下声明,它初始化names 并在对象创建时附加一个随机名称。回想一下,列表是 Python 中的可变对象。

#Not recommended
class MyClass:
    def __init__(self,names=[]):
        self.names = names
        self.names.append('Random_name')

让我们看看当我们从这个类创建对象时会发生什么:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name', 'Random_name']

随着每个新对象的创建,该列表会继续增长。这背后的原因是,无论何时调用__init__,都会始终评估默认值。多次调用__init__,会继续使用相同的函数对象,从而附加到前一组默认值。您可以自己验证这一点,因为 id 在每次创建对象时都保持不变。

>>> id(x.names)
2513077313800
>>> id(y.names)
2513077313800

那么,在明确定义属性支持的数据类型的同时,定义默认参数的正确方法是什么?最安全的选择是将默认参数设置为None,并在参数值为None 时初始化为一个空列表。以下是声明默认参数的推荐方式:

#Recommended
>>> class MyClass:
...     def __init__(self,names=None):
...         self.names = names if names else []
...         self.names.append('Random_name')

让我们检查一下行为:

>>> x = MyClass()
>>> x.names
['Random_name']
>>> y = MyClass()
>>> y.names
['Random_name']

现在,我们正在寻找这种行为。只要没有值传递给names,该对象就不会“携带”旧行李并重新初始化为一个空列表。如果我们将一些有效名称(当然是列表)传递给 y 对象的 names 参数,Random_name 将简单地附加到此列表中。同样,x 对象值不会受到影响:

>>> y = MyClass(names=['Viky','Sam'])
>>> y.names
['Viky', 'Sam', 'Random_name']
>>> x.names
['Random_name']

也许,关于这个概念的最简单的解释也可以在Effbot website 上找到。如果您想阅读一些优秀的答案:“Least Astonishment” and the Mutable Default Argument。


基于对默认参数的简要讨论,我们的类声明将修改为:

class MyClass:
    def __init__(self,names=None, scores=None):
        self.names = names if names else []
        self.scores = scores if scores else []
        self.grades = []
#<---code------>

这更有意义,所有变量都有复数名称,并在对象创建时初始化为空列表。我们得到与以前相似的结果:

>>> x.names
['John', 'Tom', 'Sean']
>>> x.grades
[]

grades 是一个空列表,清楚地表明当调用results() 时将为多个学生计算成绩。因此,我们的results 方法也应该修改。我们现在应该在分数数字(70、50 等)和self.scores 列表中的项目之间进行比较,同时self.grades 列表也应该使用个人成绩进行更新。将results 方法更改为:

def results(self, subject=None):
    #Grade calculator 
    for i in self.scores:
        if i >= 70:
            self.grades.append('A')
        elif 50 <= i < 70:
            self.grades.append('B')
        else:
            self.grades.append('C')
    return self.grades, subject

当我们调用results() 时,我们现在应该以列表的形式获取成绩:

>>> x.results(subject='Math')
>>> x.grades
['A', 'B', 'C']
>>> x.names
['John', 'Tom', 'Sean']
>>> x.scores
[70, 55, 40]

这看起来不错,但想象一下,如果列表很大,并且要弄清楚谁的分数/等级属于谁,那绝对是一场噩梦。在这里,使用正确的数据类型初始化属性很重要,这些数据类型可以以一种易于访问并清楚地显示它们的关系的方式存储所有这些项目。这里最好的选择是字典。

我们可以有一个最初定义名称和分数的字典,results 函数应该将所有内容放在一个包含所有分数、等级等的新字典中。我们还应该正确注释代码并在尽可能的方法。最后,在__init__ 中我们可能不再需要self.grades,因为您会看到成绩并未附加到列表中,而是明确指定的。这完全取决于问题的要求。

最终代码

class MyClass:
"""A class that computes the final results for students"""

    def __init__(self,names_scores=None):

        """initialize student names and scores
        :param names_scores: accepts key/value pairs of names/scores
                         E.g.: 'John': 70"""

        self.names_scores = names_scores if names_scores else      

    def results(self, _final_results=, subject=None):
        """Assign grades and collect final results into a dictionary.

       :param _final_results: an internal arg that will store the final results as dict. 
                              This is just to give a meaningful variable name for the final results."""

        self._final_results = _final_results
        for key,value in self.names_scores.items():
            if value >= 70:
                self.names_scores[key] = [value,subject,'A']
            elif 50 <= value < 70:
                self.names_scores[key] = [value,subject,'B']
            else:
                self.names_scores[key] = [value,subject,'C']
        self._final_results = self.names_scores #assign the values from the updated names_scores dict to _final_results
        return self._final_results

请注意_final_results 只是一个内部参数,用于存储更新的字典self.names_scores。目的是从函数中返回一个更有意义的变量,清楚地告知 intent。这个变量开头的_ 表示它是一个内部变量,按照惯例。

让我们最后运行一下:

>>> x = MyClass(names_scores='John':70, 'Tom':50, 'Sean':40)
>>> x.results(subject='Math')  

  'John': [70, 'Math', 'A'],
 'Tom': [50, 'Math', 'B'],
 'Sean': [40, 'Math', 'C']

这样可以更清楚地了解每个学生的成绩。现在可以轻松访问任何学生的成绩/分数:

>>> y = x.results(subject='Math')
>>> y['John']
[70, 'Math', 'A']

结论

虽然最终的代码需要一些额外的努力,但这是值得的。输出更精确,并提供有关每个学生结果的清晰信息。代码更具可读性,并清楚地告知读者创建类、方法和变量的意图。以下是本次讨论的主要内容:

预计将在类方法之间共享的变量(属性)应在__init__ 中定义。在我们的示例中,results() 需要 namesscores 和可能的 subject。这些属性可以由另一种方法共享,例如 average,它计算得分的平均值。 应使用适当的数据类型初始化属性。这应该在冒险进入基于类的问题设计之前事先决定。 使用默认参数声明属性时必须小心。如果封闭的__init__ 导致每次调用时属性发生突变,则可变默认参数可以改变属性的值。将默认 args 声明为 None 并稍后在默认值为 None 时重新初始化为空的可变集合是最安全的。 属性名称应明确,遵循 PEP8 指南。 一些变量只能在类方法的范围内初始化。例如,这些可能是计算所需的内部变量或不需要与其他方法共享的变量。 在__init__ 中定义变量的另一个令人信服的原因是避免由于访问未命名/超出范围的属性而可能发生的AttributeErrors。 __dict__ 内置方法提供了此处初始化的属性的视图。

在类实例化时为属性(位置参数)分配值时,应显式定义属性名称。例如:

x = MyClass('John', 70)  #not explicit
x = MyClass(name='John', score=70) #explicit

最后,目标应该是与 cmets 尽可能清楚地传达意图。类、它的方法和属性应该被很好地注释掉。对于所有属性,一个简短的描述和一个例子,对于第一次遇到你的类及其属性的新程序员来说非常有用。

【讨论】:

这是一篇详尽的文章,但我不能赞成,因为您鼓励使用可变的默认参数而没有解释它们有多大的问题。 Daniel 能否详细说明一下“鼓励使用可变默认参数”是什么意思? @DanielPryden,感谢您指出这一点。我会尽快更新答案。这是我现在开始理解的 Python 中的“陷阱”之一。 @DanielPryden,我刚刚用一些关于可变默认参数问题的有用信息更新了答案,并相应地编辑了代码。如果可以随时改进答案,请告诉我。 如果你使用from pystrict import strict \n @strict \n class Firebird: ...,那么在init之外创建attrs会出现运行时错误。

以上是关于在 __init__ 中为用户类设置默认/空属性的主要内容,如果未能解决你的问题,请参考以下文章

如何在 django 中为选项标签添加属性?

如何在 django 中为选项标签添加属性?

面向对象 --类

复合赋值的属性设置器?

__init__ 为啥python类需要初始化

property属性