で属性を初期化することの重要性 (または重要でないこと) を理解するため__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
このクラスには、2 つの位置引数name
とが必要score
です。これらの引数は、クラス インスタンスを初期化するために提供する必要があります。これらがないと、クラス オブジェクトx
をインスタンス化できず、 aTypeError
が発生します。
>>> 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
サブジェクトの a を最低限提供する必要があることを理解していますが、メソッドgrade
で後で計算されるため、現時点では重要ではありません。results
したがって、self.grade = None
それを位置引数として使用し、定義しません。クラスインスタンス(オブジェクト)を初期化しましょう:
>>> x = MyClass(name='John', score=70)
>>> x
<__main__.MyClass object at 0x000002491F0AE898>
は<__main__.MyClass object at 0x000002491F0AE898>
、クラス オブジェクトx
が指定されたメモリ位置に正常に作成されたことを確認します。現在、Python には、作成されたクラス オブジェクトの属性を表示するための便利な組み込みメソッドがいくつか用意されています。方法の 1 つが__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'
Python はAttributeError
、クラスの名前空間内で属性を見つけられない場合に を発生させます。で属性を初期化しない場合__init__
、クラスのメソッドのみに対してローカルである可能性のある未定義の属性にアクセスすると、このエラーが発生する可能性があります。この例では、subject
内側__init__
を定義すると混乱が回避され、計算にも必要ないため、完全に正常でした。
それでは、呼び出しresults
て、何が得られるか見てみましょう。
>>> x.results()
'B'
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': 'B'}
これにより、スコアのグレードが出力され、属性を表示すると通知grade
も更新されます。最初から、初期属性とその値がどのように変化したかを明確に把握できました。
しかし、どうsubject
ですか?ティムの数学の点数と成績を知りたい場合は、前に見たようにscore
とに簡単にアクセスできます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 では、変数の数がコレクションの長さと等しい場合、コレクションの値を同じ式の複数の変数に割り当てることができます。ここでは、長さが 2 つなので、式の左側に 2 つの変数を含めることができます。
>>> grade, subject = x.results(subject='Math')
>>> subject
'Math'
これで、subject
. で属性にアクセスするためにドット演算子だけを使用して一度にすべてにアクセスする方がより直感的ですx.<attribute>
が、これは単なる例であり、subject
initialized in で試すことができます__init__
。
次に、多くの生徒 (たとえば 3 人) がいて、数学の名前、点数、成績が必要であるとします。list
件名を除いて、他のすべては、すべての名前、スコア、成績を格納できるのような何らかのコレクション データ型である必要があります。次のように初期化できます。
>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40])
>>> x.name
['John', 'Tom', 'Sean']
>>> x.score
[70, 55, 40]
これは一見問題ないように見えますがname
、score
とgrade
の初期化をもう一度 (または他のプログラマーに) 見__init__
てもらうと、コレクション データ型が必要であることを伝える方法がありません。変数には単数形の名前も付けられているため、値が 1 つだけ必要な確率変数である可能性がより明確になっています。プログラマーの目的は、説明的な変数名、型宣言、コード コメントなどを使用して、意図をできるだけ明確にすることです。これを念頭に置いて、 の属性宣言を変更しましょう__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
ます。いくつかの有効な名前を (もちろんリストとして)オブジェクトのnames
argに渡すと、単にこのリストに追加されます。繰り返しますが、オブジェクトの値は影響を受けません:y
Random_name
x
>>> y = MyClass(names=['Viky','Sam'])
>>> y.names
['Viky', 'Sam', 'Random_name']
>>> x.names
['Random_name']
おそらく、この概念に関する最も簡単な説明は、Effbot の Web サイトでも見つけることができます。いくつかの優れた回答を読みたい場合:「最小の驚き」と可変デフォルト引数。
デフォルト引数に関する簡単な説明に基づいて、クラス宣言は次のように変更されます。
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
関数はすべてのスコア、グレードなどを含む新しいディクショナリにすべてをまとめる必要があります。また、コードを適切にコメントし、可能な限りメソッドで引数を明示的に定義する必要があります。最後に、成績がリストに追加されるのではなく、明示的に割り当てられることがわかるように、self.grades
もう inは必要ないかもしれません。__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__
が属性の変更を引き起こしている場合、属性の値を変更できます。デフォルトの引数を として宣言しNone
、デフォルト値が であるときはいつでも、後で空の変更可能なコレクションに再初期化するのが最も安全ですNone
。
- 属性名は明確である必要があり、PEP8 ガイドラインに従ってください。
- 一部の変数は、クラス メソッドのスコープ内でのみ初期化する必要があります。たとえば、計算に必要な内部変数や、他のメソッドと共有する必要のない変数などです。
- で変数を定義するもう 1 つの説得力のある理由は、名前のない/スコープ外の属性へのアクセスが原因で発生する
__init__
可能性のある を回避することです。組み込みメソッドは、ここで初期化された属性のビューを提供しますAttributeError
。__dict__
クラスのインスタンス化で属性 (位置引数) に値を割り当てる際、属性名を明示的に定義する必要があります。例えば:
x = MyClass('John', 70) #not explicit
x = MyClass(name='John', score=70) #explicit
最後に、目的は、コメントを使用してできるだけ明確に意図を伝えることです。クラス、そのメソッド、および属性は、十分にコメントする必要があります。すべての属性について、簡単な説明と例は、クラスとその属性に初めて遭遇する新しいプログラマーにとって非常に役立ちます。