web-dev-qa-db-ja.com

__init__でユーザークラスのデフォルト/空の属性を設定する

私はまともなレベルのプログラミングをしていて、ここのコミュニティから多くの価値を得ています。しかし、私はプログラミングで多くの学術的な教育を受けたことはなく、実際に経験豊富なプログラマの隣で働いたこともありません。その結果、私は時々「ベストプラクティス」と格闘します。

私はこの質問のためのよりよい場所を見つけることができません、そしてこの種の質問を嫌う可能性のある炎上者にもかかわらずこれを投稿しています。これがあなたを混乱させるなら、すみません。私はあなたを怒らせるのではなく、ただ学ぼうとしています。

質問:

新しいクラスを作成するとき、それらが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について知っていて、すでに上記の質問を数回検索しましたが、これに触れている人を見つけることができません。

ありがとう

アンディ

3
Andy

___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つの位置引数namescoreが必要です。これらの引数は、クラスインスタンスを初期化するために提供する必要があります。これらがないと、クラスオブジェクト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___は属性を初期化します。次のように、namescoreに新しい初期値を割り当てることができます。

_>>> x.__init__(name='Tim', score=50)
>>> x.__dict__
{'name': 'Tim', 'score': 50, 'grade': None}
_

以下のように個々の属性にアクセスすることもできます。 gradeNoneであるため、何も与えられません。

_>>> 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でどれだけ採点したか、そして何点を取ったか知りたい場合は、以前に見たようにscoregradeに簡単にアクセスできますが、件名はどのようにしてわかりますか? 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___のnamescoregradeの初期化をもう一度見ると、コレクションデータ型が必要であることを伝える方法がありません。変数は単数形とも呼ばれ、1つの値だけを必要とする可能性のあるランダム変数の可能性があることをより明確にします。プログラマーの目的は、説明的な変数の名前付け、型の宣言、コードのコメントなどによって、意図をできるだけ明確にすることです。これを念頭に置いて、___init___の属性宣言を変更してみましょう。 well-behavedwell-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___で定義する必要があります。この例では、namesscores、場合によってはsubjectresults()に必要でした。これらの属性は、スコアの平均を計算するsay averageなどの別のメソッドで共有できます。
  • 属性は適切なデータ型で初期化する必要があります。これは、問題のクラスベースの設計に取り組む前に、事前に決定する必要があります。
  • デフォルトの引数で属性を宣言するときは注意が必要です。囲んでいる___init___がすべての呼び出しで属性の変更を引き起こしている場合、変更可能なデフォルト引数は属性の値を変更できます。デフォルトの引数をNoneとして宣言し、後でデフォルト値がNoneであるときはいつでも空の可変コレクションに再初期化するのが最も安全です。
  • 属性名は明確でなければなりません。PEP8ガイドラインに従ってください。
  • 一部の変数は、クラスメソッドのスコープ内でのみ初期化する必要があります。これらは、たとえば、計算に必要な内部変数や、他のメソッドと共有する必要のない変数である可能性があります。
  • ___init___で変数を定義するもう1つの説得力のある理由は、名前のない/スコープ外の属性へのアクセスが原因で発生する可能性があるAttributeErrorsを回避するためです。 ___dict___組み込みメソッドは、ここで初期化された属性のビューを提供します。
  • クラスのインスタンス化で属性(位置引数)に値を割り当てる際、属性名を明示的に定義する必要があります。例えば:

    _x = MyClass('John', 70)  #not explicit
    x = MyClass(name='John', score=70) #explicit
    _
  • 最後に、目的はコメントで意図をできるだけ明確に伝えることです。クラス、そのメソッド、および属性は十分にコメントされている必要があります。すべての属性について、簡単な説明と例は、クラスとその属性に初めて遭遇する新しいプログラマにとって非常に役立ちます。

0
amanb