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

时间:2019-04-22 19:47:21

标签: python class instance instance-variables attr

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

我找不到适合这个问题的好地方,尽管有可能激怒了这类问题的人,但我仍将其发布。非常抱歉,如果您不满意。我只是想学习,而不是生气。

问题:

当我创建一个新类时,我应该在 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

我在其他项目中发现,当类属性仅出现在类方法中时,它们会被埋没。

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

如果有人在我可以找到这些原则的地方有任何材料的链接,那么请把它们回答,将不胜感激。我了解PEP-8,并且已经在上面多次搜索了我的问题,并且找不到任何涉及此的人。

谢谢

安迪

3 个答案:

答案 0 :(得分:1)

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

看一下您的类的两个稍加修改的版本,分别带有一个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。在您的情况下,工厂模式或构建器模式可能是答案。例如:

class MyClassBuilder:
    def __init__(self, df):
          self._df = df # df is known immediately
          # give a default value to other fields if possible

    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字段。)我在介绍中就是这种情况。在我看来,应该将它们设置为默认值(如果存在),或者在尝试创建不完整的对象时不进行初始化以引发异常。

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

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

答案 1 :(得分:1)

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

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.results为self。** _ ** results

    请记住,这只是约定,并且self._results仍然可以直接访问。但是,这是处理伪私有属性的Python方法。

  3. 第二个问题是具有部分初始化的属性,该属性设置为“无”。由于将其设置为None,如下面的@jferard所述,我们现在已经失去了快速失败提示,并增加了一层混淆来调试代码。

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

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

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

  4. 如果您的类中还有其他需要使用self._results的方法,但只有在正确分配后,您才能使用self.results,这样就可以使用上述故障安全提示。 / p>

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

答案 2 :(得分:0)

要了解在__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而不将其定义为位置arg。让我们初始化一个类实例(对象):

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

<__main__.MyClass object at 0x000002491F0AE898>确认在给定的内存位置成功创建了类对象x。现在,Python提供了一些有用的内置方法来查看创建的类对象的属性。方法之一是__dict__。您可以阅读有关here的更多信息:

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

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

让我们花一点时间来了解__init__的作用。有许多answers和在线资源可用来解释此方法的作用,但我将总结一下:

__init__一样,Python还有另一个内置方法__new__()。当您创建类似x = MyClass(name='John', score=70)的类对象时,Python首先内部调用__new__()来创建类MyClass的新实例,然后调用__init__来初始化属性{{ 1}}和name。当然,在这些内部调用中,当Python找不到所需的位置args的值时,它将引发一个错误,如上所述。换句话说,score初始化属性。您可以像这样为__init__name分配新的初始值:

score

也可以如下访问单个属性。 >>> 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,即位置arg。此变量的范围仅在此方法内部。出于演示目的,我在此方法中明确定义了None,但也可以在subject中对其进行初始化。但是,如果我尝试使用对象访问它,该怎么办?

__init__

Python在类的名称空间内找不到属性时,将引发>>> x.subject Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'MyClass' object has no attribute 'subject' 。如果未在AttributeError中初始化属性,则访问未定义的属性(仅对类的方法来说是本地的)时,可能会遇到此错误。在此示例中,在__init__内定义subject可以避免混淆,并且这样做也是完全正常的,因为也不用进行任何计算。

现在,让我们致电__init__,看看我们得到了什么:

results

这将打印分数的分数并在我们查看属性时注意,>>> x.results() 'B' >>> x.__dict__ {'name': 'Tim', 'score': 50, 'grade': 'B'} 也已更新。从一开始,我们就清楚了解了初始属性及其值的变化方式。

但是grade呢?如果我想知道Tim在数学上的得分是多少,以及年级是多少,我可以像以前一样轻松地访问subjectscore,但是我怎么知道这个主题?由于grade变量是subject方法范围的局部变量,因此我们可以results的值return。在subject方法中更改return语句:

results

让我们再次致电def results(self, subject=None): #<---code---> return self.grade, subject 。我们得到了具有预期成绩和学科的元组。

results()

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

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

因此,我们已经有了它,尽管它需要一些额外的代码行才能获得>>> grade, subject = x.results(subject='Math') >>> subject 'Math' 。使用点运算符一次使用subject来访问所有属性会更直观,但这只是一个示例,您可以使用在{{1中初始化的x.<attribute> }}。

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

__init__

乍看之下似乎不错,但是当您(或其他程序员)对list中的>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40]) >>> x.name ['John', 'Tom', 'Sean'] >>> x.score [70, 55, 40] namescore的初始化有另一番看法时,因此无法判断他们是否需要收集数据类型。变量也被称为单数,这使得它们很明显只是一些可能只需要一个值的随机变量。程序员的目的应该是通过描述性变量命名,类型声明,代码注释等方式使意图尽可能清晰。考虑到这一点,让我们更改grade中的属性声明。在满足行为良好定义明确的声明之前,我们必须注意如何声明默认参数。


编辑:可变默认参数存在问题:

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

__init__

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

names

每次创建新对象时,列表将继续增长。其原因在于,每次调用#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__

那么,定义默认参数的正确方法是什么,同时还要明确说明该属性支持的数据类型?最安全的选项是将默认args设置为id,并在arg值为>>> id(x.names) 2513077313800 >>> id(y.names) 2513077313800 时初始化为空列表。建议使用以下方法来声明默认args:

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 arg传递一些有效的名称(当然是列表),则names将简单地附加到此列表中。同样,y对象的值将不受影响:

Random_name

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


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

x

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

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

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方法也应进行修改。现在,我们进行的比较应该是在得分数字(70、50等)和results()列表中的项目之间进行比较,而同时results列表也应该使用各个等级进行更新。将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()函数应将所有内容放到一个包含所有分数,成绩等的新字典中。我们还应正确注释代码并明确定义args尽可能采用这种方法。最后,我们可能不再需要>>> x.results(subject='Math') >>> x.grades ['A', 'B', 'C'] >>> x.names ['John', 'Tom', 'Sean'] >>> x.scores [70, 55, 40] 中的results,因为正如您所看到的,成绩不会被附加到列表中而是被明确分配。这完全取决于问题的要求。

最终代码

self.grades

请注意,__init__只是一个内部参数,用于存储更新的字典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

这样可以更清晰地查看每个学生的成绩。现在可以轻松地为任何学生访问成绩/分数:

_

结论

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

  • 应在>>> 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__names和可能的scores。这些属性可以由其他类似subject的方法共享,例如计算得分的平均值。
  • 应使用适当的数据类型初始化属性。在进入问题的基于类的设计之前,应事先确定这一点。
  • 声明具有默认参数的属性时,必须小心。如果封闭的results()导致每次调用中的属性发生更改,则可变的默认args可以更改属性的值。将默认args声明为average,然后在默认值为__init__时重新初始化为空的可变集合是最安全的。
  • 属性名称应明确,请遵循PEP8准则。
  • 某些变量应仅在类方法的范围内初始化。例如,这些可能是计算所需的内部变量或不需要与其他方法共享的变量。
  • None中定义变量的另一个令人信服的理由是,避免由于访问未命名/范围外的属性而可能发生的None。内置方法__init__提供了此处初始化的属性的视图。
  • 在类实例化时为属性(位置args)分配值时,应明确定义属性名称。例如:

    AttributeError
  • 最后,目标应该是在评论中尽可能清晰地传达意图。该类,其方法和属性应得到很好的注释。对于所有属性,简短的说明和示例对于第一次接触您的类及其属性的新程序员来说非常有用。