25

私はまともなレベルのプログラミングをしており、ここのコミュニティから多くの価値を得ています。しかし、私はプログラミングのアカデミックな指導を受けたことも、本当に経験豊富なプログラマーの隣で働いたこともありません。その結果、私は「ベストプラクティス」に苦労することがあります。

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

質問:

新しいクラスを作成するとき、すべてのインスタンス属性を に設定する必要__init__Noneありますか?実際には後でクラス メソッドに値が割り当てられますか?

resultsの属性については、以下の例を参照してください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について知っており、上記の質問をすでに数回検索しましたが、これに触れている人を見つけることができません。

ありがとう

アンディ

4

3 に答える 3

1

で属性を初期化することの重要性 (または重要でないこと) を理解するため__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ていることに気付くでしょう。この変数のスコープは、このメソッド内のみです。デモンストレーションのために、このメソッド内で明示的に定義しますが、これも初期化できます。しかし、オブジェクトでアクセスしようとするとどうなりますか:subjectNonesubject__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>が、これは単なる例であり、subjectinitialized in で試すことができます__init__

次に、多くの生徒 (たとえば 3 人) がいて、数学の名前、点数、成績が必要であるとします。list件名を除いて、他のすべては、すべての名前、スコア、成績を格納できるのような何らかのコレクション データ型である必要があります。次のように初期化できます。

>>> x = MyClass(name=['John', 'Tom', 'Sean'], score=[70, 55, 40])
>>> x.name
['John', 'Tom', 'Sean']
>>> x.score
[70, 55, 40]

これは一見問題ないように見えますがnamescoregradeの初期化をもう一度 (または他のプログラマーに) 見__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ます。いくつかの有効な名前を (もちろんリストとして)オブジェクトのnamesargに渡すと、単にこのリストに追加されます。繰り返しますが、オブジェクトの値は影響を受けません:yRandom_namex

>>> 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
[]

gradesresults()が呼び出されたときに複数の学生の成績が計算されることを明確にする空のリストです。したがって、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__。この例ではnamesscoresと が によってsubject必要とされた可能性がありresults()ます。averageこれらの属性は、スコアの平均を計算するなどの別の方法で共有できます。
  • 属性は、適切なデータ型で初期化する必要があります。これは、問題のクラスベースの設計に取り組む前に、事前に決定する必要があります。
  • デフォルトの引数で属性を宣言するときは注意が必要です。変更可能なデフォルト引数は、すべての呼び出しで囲み__init__が属性の変更を引き起こしている場合、属性の値を変更できます。デフォルトの引数を として宣言しNone、デフォルト値が であるときはいつでも、後で空の変更可能なコレクションに再初期化するのが最も安全ですNone
  • 属性名は明確である必要があり、PEP8 ガイドラインに従ってください。
  • 一部の変数は、クラス メソッドのスコープ内でのみ初期化する必要があります。たとえば、計算に必要な内部変数や、他のメソッドと共有する必要のない変数などです。
  • で変数を定義するもう 1 つの説得力のある理由は、名前のない/スコープ外の属性へのアクセスが原因で発生する__init__可能性のある を回避することです。組み込みメソッドは、ここで初期化された属性のビューを提供しますAttributeError__dict__
  • クラスのインスタンス化で属性 (位置引数) に値を割り当てる際、属性名を明示的に定義する必要があります。例えば:

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

于 2019-04-23T02:31:21.463 に答える