要了解在__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
这个类需要两个位置参数name 和score。 必须提供这些参数来初始化类实例。没有这些,类对象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>
<__main__.MyClass object at 0x000002491F0AE898> 确认类对象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__ 来初始化属性name 和score。当然,在这些内部调用中,当 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'
当 Python 在类的命名空间中找不到属性时,它会引发 AttributeError。如果您不初始化 __init__ 中的属性,则当您访问可能仅对类的方法本地的未定义属性时,可能会遇到此错误。在此示例中,在 __init__ 中定义 subject 可以避免混淆,并且这样做是完全正常的,因为它也不需要任何计算。
现在,让我们拨打results 看看我们得到了什么:
>>> x.results()
'B'
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': 'B'}
这会打印分数的等级,并在我们查看属性时注意,grade 也已更新。从一开始,我们就清楚地了解了初始属性以及它们的值是如何变化的。
但是subject 呢?如果我想知道蒂姆在数学上的得分和成绩,我可以很容易地访问score 和grade,就像我们之前看到的那样,但是我怎么知道这个主题呢?因为,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.<attribute> 访问属性时,使用点运算符一次访问所有这些属性会更直观,但这只是一个示例,您可以尝试使用在__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__ 中的 name、score 和 grade 的初始化时,无法判断他们需要一个集合数据类型。这些变量也被命名为单数,这使得它们可能只是一些可能只需要一个值的随机变量更加明显。程序员的目的应该是通过描述性变量命名、类型声明、代码 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() 需要 names、scores 和可能的 subject。这些属性可以由另一种方法共享,例如 average,它计算得分的平均值。
- 应使用适当的数据类型初始化属性。这应该在冒险进入基于类的问题设计之前事先决定。
- 使用默认参数声明属性时必须小心。如果封闭的
__init__ 导致每次调用时属性发生突变,则可变默认参数可以改变属性的值。将默认 args 声明为 None 并稍后在默认值为 None 时重新初始化为空的可变集合是最安全的。
- 属性名称应明确,遵循 PEP8 指南。
- 一些变量只能在类方法的范围内初始化。例如,这些可能是计算所需的内部变量或不需要与其他方法共享的变量。
- 在
__init__ 中定义变量的另一个令人信服的原因是避免由于访问未命名/超出范围的属性而可能发生的AttributeErrors。 __dict__ 内置方法提供了此处初始化的属性的视图。
-
在类实例化时为属性(位置参数)分配值时,应显式定义属性名称。例如:
x = MyClass('John', 70) #not explicit
x = MyClass(name='John', score=70) #explicit
最后,目标应该是与 cmets 尽可能清楚地传达意图。类、它的方法和属性应该被很好地注释掉。对于所有属性,一个简短的描述和一个例子,对于第一次遇到你的类及其属性的新程序员来说非常有用。