要了解在 中初始化属性的重要性(或不重要)__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
会引发 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__
以初始化属性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'
当PythonAttributeError
在类的命名空间中找不到属性时,它会引发一个。如果您不初始化 中的属性__init__
,则当您访问一个未定义的属性时可能会遇到此错误,该属性可能仅对类的方法是本地的。在这个例子中,定义subject
inside__init__
可以避免混淆并且这样做是完全正常的,因为它也不需要任何计算。
现在,让我们打电话results
看看我们得到了什么:
>>> x.results()
'B'
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': 'B'}
这会打印分数的等级,并在我们查看属性时通知,grade
也已更新。从一开始,我们就清楚地了解了初始属性以及它们的值是如何变化的。
但是呢subject
?如果我想知道蒂姆在数学上的得分是多少以及成绩是多少,我可以很容易地访问我们之前看到的 thescore
和 the grade
,但是我怎么知道这个主题呢?因为,subject
变量是results
方法范围的局部变量,我们可以只return
取subject
. 更改方法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>
,但这只是一个示例,您可以尝试使用subject
initialized 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
,没有办法告诉他们需要一个集合数据类型。这些变量也被命名为单数,这使得它们可能只是一些可能只需要一个值的随机变量更加明显。程序员的目的应该是通过描述性变量命名、类型声明、代码注释等方式使意图尽可能清晰。考虑到这一点,让我们更改. 在我们满足于行为良好、定义良好的声明之前,我们必须注意如何声明默认参数。score
grade
__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
。如果我们将一些有效名称(当然作为列表)传递给对象的names
arg ,则将简单地附加到此列表中。同样,对象值不会受到影响:y
Random_name
x
>>> 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
[]
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
函数应该将所有内容放在一个包含所有分数、等级等的新字典中。我们还应该正确注释代码并尽可能在方法中显式定义 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__
. 在我们的示例中names
,scores
和 可能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
最后,目标应该是通过评论尽可能清楚地传达意图。类、它的方法和属性应该被很好地注释掉。对于所有属性,一个简短的描述和一个例子,对于第一次遇到你的类及其属性的新程序员来说非常有用。