私はまともなレベルのプログラミングをしていて、ここのコミュニティから多くの価値を得ています。しかし、私はプログラミングで多くの学術的な教育を受けたことはなく、実際に経験豊富なプログラマの隣で働いたこともありません。その結果、私は時々「ベストプラクティス」と格闘します。
私はこの質問のためのよりよい場所を見つけることができません、そしてこの種の質問を嫌う可能性のある炎上者にもかかわらずこれを投稿しています。これがあなたを混乱させるなら、すみません。私はあなたを怒らせるのではなく、ただ学ぼうとしています。
質問:
新しいクラスを作成するとき、それらがNoneで実際に後でクラスメソッドで値が割り当てられている場合でも、すべてのインスタンス属性をinitに設定する必要がありますか?
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について知っていて、すでに上記の質問を数回検索しましたが、これに触れている人を見つけることができません。
ありがとう
アンディ
___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
をインスタンス化できず、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は、作成されたクラスオブジェクトの属性を表示するための便利な組み込みメソッドをいくつか提供しています。メソッドの1つは___dict__
_です。あなたはそれについてもっと読むことができます ここ :
_>>> x.__dict__
{'name': 'John', 'score': 70, 'grade': None}
_
これにより、すべての初期属性とその値のdict
ビューが明確に示されます。 grade
には、___init__
_に割り当てられているNone
値があることに注意してください。
___init__
_の機能を理解してみましょう。このメソッドの機能を説明するために利用できる answers およびオンラインリソースはたくさんありますが、要約します。
___init__
_と同様に、Pythonには__new__()
と呼ばれるもう1つの組み込みメソッドがあります。この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
はどうでしょうか? TimがMathでどれだけ採点したか、そして何点を取ったか知りたい場合は、以前に見たように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>
_を使用して属性にアクセスするためにドット演算子のみを使用してそれらすべてに一度にアクセスする方が直感的ですが、これは単なる例であり、___init__
_で初期化されたsubject
を使用して試すことができます。
次に、多くの学生(たとえば3人)がいて、Mathの名前、スコア、成績が欲しいと考えます。件名を除いて、他のすべては、すべての名前、スコア、および評点を格納できる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
の初期化をもう一度見ると、コレクションデータ型が必要であることを伝える方法がありません。変数は単数形とも呼ばれ、1つの値だけを必要とする可能性のあるランダム変数の可能性があることをより明確にします。プログラマーの目的は、説明的な変数の名前付け、型の宣言、コードのコメントなどによって、意図をできるだけ明確にすることです。これを念頭に置いて、___init__
_の属性宣言を変更してみましょう。 well-behaved、well-defined宣言を決定する前に、デフォルト引数の宣言方法に注意する必要があります。
編集:変更可能なデフォルト引数の問題:
さて、デフォルトの引数を宣言する際に注意しなければならない「落とし穴」がいくつかあります。 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__
_が呼び出されるたびにデフォルト値がalwaysに評価されるためです。 ___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
オブジェクトの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
_で___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
_を格納する単なる内部引数であることに注意してください。目的は、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__
_で定義する必要があります。この例では、names
、scores
、場合によってはsubject
がresults()
に必要でした。これらの属性は、スコアの平均を計算するsay average
などの別のメソッドで共有できます。__init__
_がすべての呼び出しで属性の変更を引き起こしている場合、変更可能なデフォルト引数は属性の値を変更できます。デフォルトの引数をNone
として宣言し、後でデフォルト値がNone
であるときはいつでも空の可変コレクションに再初期化するのが最も安全です。__init__
_で変数を定義するもう1つの説得力のある理由は、名前のない/スコープ外の属性へのアクセスが原因で発生する可能性があるAttributeError
sを回避するためです。 ___dict__
_組み込みメソッドは、ここで初期化された属性のビューを提供します。クラスのインスタンス化で属性(位置引数)に値を割り当てる際、属性名を明示的に定義する必要があります。例えば:
_x = MyClass('John', 70) #not explicit
x = MyClass(name='John', score=70) #explicit
_
最後に、目的はコメントで意図をできるだけ明確に伝えることです。クラス、そのメソッド、および属性は十分にコメントされている必要があります。すべての属性について、簡単な説明と例は、クラスとその属性に初めて遭遇する新しいプログラマにとって非常に役立ちます。