25

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

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

问题:

当我创建一个新类时,我是否应该在 中设置所有实例属性__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 并且已经多次搜索了我的问题,但找不到任何涉及此问题的人。

谢谢

安迪

4

3 回答 3

23

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

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

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

令人高兴的是,这个问题有一个有据可查的解决方案:设计模式,更准确地说是创建模式。在您的情况下,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'

优点很明显:

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

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

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

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

于 2019-04-23T14:56:40.533 回答
11

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

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

    为了解决这个问题,我们添加了一个 getter 方法。这可以在上面被视为results()具有上述@property装饰器的功能。

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

    (我更改了设置结果的方法的名称以避免混淆并为 getter 方法generate_results()腾出空间).results

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

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

于 2020-09-18T10:57:00.683 回答
1

要了解在 中初始化属性的重要性(或不重要)__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会引发 a:

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

在这一点上,我们知道我们必须至少提供name学生的 a 和score一个学科的 a,但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__你可以在这里阅读更多关于它的信息:

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

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

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

就像__init__,Python 有另一个名为__new__(). 当你像这样创建一个类对象时x = MyClass(name='John', score=70),Python 内部__new__()首先调用以创建该类的新实例,MyClass然后调用__init__以初始化属性namescore. 当然,在这些内部调用中,当 Python 找不到所需位置参数的值时,它会引发错误,正如我们在上面看到的那样。换句话说,__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“变量”被定义为None一个位置参数。此变量的范围仅在此方法内。出于演示的目的,我subject在此方法中明确定义,但这也可以在其中进行初始化__init__。但是如果我尝试用我的对象访问它怎么办:

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

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

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

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

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

但是呢subject?如果我想知道蒂姆在数学上的得分是多少以及成绩是多少,我可以很容易地访问我们之前看到的 thescore和 the grade,但是我怎么知道这个主题呢?因为,subject变量是results方法范围的局部变量,我们可以只returnsubject. 更改方法return中的语句results

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.<attribute>,但这只是一个示例,您可以尝试使用subjectinitialized in __init__

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

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

乍一看这似乎很好,但是当您再看一下(或其他一些程序员)在和in的初始化时name,没有办法告诉他们需要一个集合数据类型。这些变量也被命名为单数,这使得它们可能只是一些可能只需要一个值的随机变量更加明显。程序员的目的应该是通过描述性变量命名、类型声明、代码注释等方式使意图尽可能清晰。考虑到这一点,让我们更改. 在我们满足于行为良好定义良好的声明之前,我们必须注意如何声明默认参数。scoregrade__init____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

那么,在明确定义属性支持的数据类型的同时,定义默认参数的正确方法是什么?最安全的选择是将默认 args 设置为None并在 arg 值为 时初始化为一个空列表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。如果我们将一些有效名称(当然作为列表)传递给对象的namesarg ,则将简单地附加到此列表中。同样,对象值不会受到影响:yRandom_namex

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

也许,关于这个概念的最简单的解释也可以在Effbot 网站上找到。如果您想阅读一些出色的答案:“Least Astonishment”和 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
[]

gradesresults()是一个空列表,清楚地表明在调用时将为多个学生计算成绩。因此,我们的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函数应该将所有内容放在一个包含所有分数、等级等的新字典中。我们还应该正确注释代码并尽可能在方法中显式定义 args。最后,我们可能不再需要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,这只是一个内部参数,用于存储更新的 dict 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__. 在我们的示例中namesscores和 可能subject需要results()。这些属性可以由另一种方法共享,例如average计算分数的平均值。
  • 属性应该使用适当的数据类型进行初始化。这应该在冒险进入基于类的问题设计之前事先决定。
  • 使用默认参数声明属性时必须小心。如果封闭__init__导致每次调用时属性的突变,则可变默认参数可以改变属性的值。将默认 args 声明为None并稍后在默认值为 时重新初始化为空的可变集合是最安全的None
  • 属性名称应该是明确的,遵循 PEP8 指南。
  • 一些变量应该只在类方法的范围内初始化。例如,这些可能是计算所需的内部变量或不需要与其他方法共享的变量。
  • 定义变量的另一个令人信服的原因__init__是避免AttributeError由于访问未命名/超出范围的属性而可能发生的 s。__dict__内置方法提供了此处初始化的属性的视图。
  • 在类实例化时为属性(位置参数)分配值时,应显式定义属性名称。例如:

    x = MyClass('John', 70)  #not explicit
    x = MyClass(name='John', score=70) #explicit
    
  • 最后,目标应该是通过评论尽可能清楚地传达意图。类、它的方法和属性应该被很好地注释掉。对于所有属性,一个简短的描述和一个例子,对于第一次遇到你的类及其属性的新程序员来说非常有用。

于 2019-04-23T02:31:21.463 回答