Pythonクラス属性:過度に徹底的なガイド

公開: 2022-03-11

最近、プログラミングのインタビューを受けました。電話スクリーンで、共同テキストエディタを使用しました。

特定のAPIを実装するように依頼され、Pythonで実装することを選択しました。 問題ステートメントを抽象化して、インスタンスがいくつかのdataといくつかのother_dataを格納するクラスが必要だったとしましょう。

深呼吸してタイピングを始めました。 数行後、私は次のようなものになりました。

 class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...

私のインタビュアーは私を止めました:

  • インタビュアー:「その行: data = [] 。 それは有効なPythonではないと思いますか?」
  • 私:「そうだと確信しています。 インスタンス属性のデフォルト値を設定しているだけです。」
  • インタビュアー:「そのコードはいつ実行されますか?」
  • 私:「よくわかりません。 混乱を避けるために修正します。」

参考までに、そして私が何をしようとしていたかを知るために、コードを修正した方法は次のとおりです。

 class Service(object): def __init__(self, other_data): self.data = [] self.other_data = other_data ...

結局のところ、私たちは両方とも間違っていました。 本当の答えは、Pythonクラス属性とPythonインスタンス属性の違いを理解することにあります。

Pythonクラス属性とPythonインスタンス属性

注:クラス属性のエキスパートハンドルがある場合は、スキップしてユースケースに進むことができます。

Pythonクラス属性

上記のコード構文的に有効であるという点で、私のインタビュアーは間違っていました。

インスタンス属性に「デフォルト値」が設定されていないという点で私も間違っていました。 代わりに、値[]を持つクラス属性としてdataを定義しています。

私の経験では、Pythonクラス属性は多くの人が知っいるトピックですが、完全に理解している人はほとんどいません。

Pythonクラス変数とインスタンス変数:違いは何ですか?

Pythonクラス属性は、クラスのインスタンスの属性ではなく、クラスの属性(循環、私は知っています)です。

違いを説明するためにPythonクラスの例を使用してみましょう。 ここで、 class_varはクラス属性であり、 i_varはインスタンス属性です。

 class MyClass(object): class_var = 1 def __init__(self, i_var): self.i_var = i_var

クラスのすべてのインスタンスはclass_varにアクセスでき、クラス自体のプロパティとしてもアクセスできることに注意してください。

 foo = MyClass(2) bar = MyClass(3) foo.class_var, foo.i_var ## 1, 2 bar.class_var, bar.i_var ## 1, 3 MyClass.class_var ## <— This is key ## 1

JavaまたはC++プログラマーの場合、クラス属性は静的メンバーと似ていますが、同一ではありません。 それらがどのように異なるかは後でわかります。

クラスとインスタンスの名前空間

ここで何が起こっているのかを理解するために、 Python名前空間について簡単に説明しましょう。

名前空間は、名前からオブジェクトへのマッピングであり、異なる名前空間の名前の間には関係がないという特性があります。 これらは通常Python辞書として実装されますが、これは抽象化されています。

コンテキストに応じて、ドット構文(例: object.name_from_objects_namespace )またはローカル変数(例: object_from_namespace )を使用して名前空間にアクセスする必要がある場合があります。 具体的な例として:

 class MyClass(object): ## No need for dot syntax class_var = 1 def __init__(self, i_var): self.i_var = i_var ## Need dot syntax as we've left scope of class namespace MyClass.class_var ## 1

Pythonクラスクラスのインスタンスにはそれぞれ、事前定義された属性MyClass.__dict__instance_of_MyClass.__dict__でそれぞれ表される独自の名前空間があります。

クラスのインスタンスから属性にアクセスしようとすると、最初にそのインスタンスの名前空間が調べられます。 属性が見つかると、関連付けられた値を返します。 そうでない場合は、クラスの名前空間調べて属性を返します(存在する場合は、エラーをスローします)。 例えば:

 foo = MyClass(2) ## Finds i_var in foo's instance namespace foo.i_var ## 2 ## Doesn't find class_var in instance namespace… ## So look's in class namespace (MyClass.__dict__) foo.class_var ## 1

インスタンスの名前空間は、クラスの名前空間よりも優先されます。両方に同じ名前の属性がある場合、インスタンスの名前空間が最初にチェックされ、その値が返されます。 属性ルックアップのコード(ソース)の簡略化されたバージョンは次のとおりです。

 def instlookup(inst, name): ## simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]

そして、視覚的な形で:

視覚的な形式での属性ルックアップ

クラス属性が割り当てを処理する方法

これを念頭に置いて、Pythonクラス属性が割り当てを処理する方法を理解できます。

  • クラスにアクセスしてクラス属性を設定すると、すべてのインスタンスの値が上書きされます。 例えば:

     foo = MyClass(2) foo.class_var ## 1 MyClass.class_var = 2 foo.class_var ## 2

    名前空間レベルでは… MyClass.__dict__['class_var'] = 2を設定しています。 (注:これは正確なコードではありません( setattr(MyClass, 'class_var', 2) )。__dict__は直接割り当てを防ぐ不変のラッパーである__dict__を返しますが、デモンストレーションに役立ちます)。 次に、 foo.class_varにアクセスすると、 class_varはクラス名前空間に新しい値を持つため、2が返されます。

  • インスタンスにアクセスしてPaythonクラス変数を設定すると、そのインスタンスの値のみがオーバーライドされます。 これは基本的にクラス変数をオーバーライドし、そのインスタンスでのみ直感的に使用できるインスタンス変数に変換します。 例えば:

     foo = MyClass(2) foo.class_var ## 1 foo.class_var = 2 foo.class_var ## 2 MyClass.class_var ## 1

    名前空間レベルでは… class_var属性をfoo.__dict__に追加しているので、 foo.class_varを検索すると、2が返されます。一方、 MyClassの他のインスタンスは、インスタンスの名前空間にclass_varないため、引き続きclass_varを検索します。 MyClass.__dict__で、したがって1を返します。

可変性

クイズの質問:クラス属性に可変型がある場合はどうなりますか? 特定のインスタンスを介してクラス属性にアクセスすることでクラス属性を操作(切断?)でき、その結果、すべてのインスタンスがアクセスしている参照オブジェクトを操作することになります(Timothy Wisemanが指摘)。

これは例によって最もよく示されます。 前に定義したServiceに戻って、クラス変数を使用すると、今後どのように問題が発生する可能性があるかを見てみましょう。

 class Service(object): data = [] def __init__(self, other_data): self.other_data = other_data ...

私の目標は、 dataのデフォルト値として空のリスト( [] )を設定し、 Serviceの各インスタンスに、インスタンスごとに時間の経過とともに変更される独自のデータを設定することでした。 ただし、この場合、次の動作が発生します( Serviceが引数other_dataを取得することを思い出してください。これは、この例では任意です)。

 s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data.append(1) s1.data ## [1] s2.data ## [1] s2.data.append(2) s1.data ## [1, 2] s2.data ## [1, 2]

これは良くありません。1つのインスタンスを介してクラス変数を変更すると、他のすべてのインスタンスも変更されます。

名前空間レベルでは… Serviceのすべてのインスタンスは、インスタンスの名前空間に独自のdata属性を作成せずに、 Service.__dict__の同じリストにアクセスして変更しています。

割り当てを使用してこれを回避できます。 つまり、リストの可変性を利用する代わりに、次のように、 Serviceオブジェクトに独自のリストを割り当てることができます。

 s1 = Service(['a', 'b']) s2 = Service(['c', 'd']) s1.data = [1] s2.data = [2] s1.data ## [1] s2.data ## [2]

この場合、 s1.__dict__['data'] = [1]を追加しているため、元のService.__dict__['data']は変更されません。

残念ながら、これにはServiceユーザーがその変数についての深い知識を持っている必要があり、間違いなく間違いを犯しがちです。 ある意味では、原因ではなく症状に対処することになります。 構造上正しいものが望ましいです。

私の個人的な解決策:クラス変数を使用してデフォルト値をPythonインスタンス変数に割り当てる場合は、可変値を使用しないでください。 この場合、 Serviceのすべてのインスタンスが最終的に独自のインスタンス属性でService.dataをオーバーライドするため、デフォルトとして空のリストを使用すると、見落とされがちな小さなバグが発生しました。 上記の代わりに、次のいずれかを行うことができます。

  1. はじめに示したように、インスタンス属性に完全に固執しました。
  2. 空のリスト(可変値)を「デフォルト」として使用することは避けました。

     class Service(object): data = None def __init__(self, other_data): self.other_data = other_data ...

    もちろん、 Noneの場合は適切に処理する必要がありますが、それは少額の支払いです。

では、いつPythonクラス属性を使用する必要がありますか?

クラス属性には注意が必要ですが、便利な場合をいくつか見てみましょう。

  1. 定数の保存。 クラス属性はクラス自体の属性としてアクセスできるため、クラス全体のクラス固有の定数を格納するためにそれらを使用すると便利なことがよくあります。 例えば:

     class Circle(object): pi = 3.14159 def __init__(self, radius): self.radius = radius def area(self): return Circle.pi * self.radius * self.radius Circle.pi ## 3.14159 c = Circle(10) c.pi ## 3.14159 c.area() ## 314.159
  2. デフォルト値の定義。 簡単な例として、制限付きリスト(つまり、特定の数以下の要素しか保持できないリスト)を作成し、デフォルトの上限を10アイテムにすることを選択できます。

     class MyClass(object): limit = 10 def __init__(self): self.data = [] def item(self, i): return self.data[i] def add(self, e): if len(self.data) >= self.limit: raise Exception("Too many elements") self.data.append(e) MyClass.limit ## 10

    次に、インスタンスのlimit属性に割り当てることで、独自の特定の制限を持つインスタンスを作成することもできます。

     foo = MyClass() foo.limit = 50 ## foo can now hold 50 elements—other instances can hold 10

    これは、 MyClassの一般的なインスタンスが保持する要素を10個以下にする場合にのみ意味があります。すべてのインスタンスに異なる制限を与える場合、 limitはインスタンス変数である必要があります。 (ただし、デフォルトとして可変値を使用する場合は注意してください。)

  3. 特定のクラスのすべてのインスタンスにわたるすべてのデータを追跡します。 これは一種の特定のものですが、特定のクラスの既存のすべてのインスタンスに関連するデータにアクセスしたいというシナリオを見ることができます。

    シナリオをより具体的にするために、 Personクラスがあり、すべての人にnameがあるとします。 使用されたすべての名前を追跡したいと思います。 1つのアプローチは、ガベージコレクターのオブジェクトのリストを反復処理することですが、クラス変数を使用する方が簡単です。

    この場合、 namesはクラス変数としてのみアクセスされるため、変更可能なデフォルトが受け入れられることに注意してください。

     class Person(object): all_names = [] def __init__(self, name): self.name = name Person.all_names.append(name) joe = Person('Joe') bob = Person('Bob') print Person.all_names ## ['Joe', 'Bob']

    このデザインパターンを使用して、関連するデータだけでなく、特定のクラスの既存のすべてのインスタンスを追跡することもできます。

     class Person(object): all_people = [] def __init__(self, name): self.name = name Person.all_people.append(self) joe = Person('Joe') bob = Person('Bob') print Person.all_people ## [<__main__.Person object at 0x10e428c50>, <__main__.Person object at 0x10e428c90>]
  4. パフォーマンス(一種の…以下を参照)。

関連: Toptal開発者によるPythonのベストプラクティスとヒント

フードの下

注:このレベルでのパフォーマンスが心配な場合は、最初からPythonを使用したくない場合があります。違いは、10分の1ミリ秒程度になるためです。ただし、少し調べてみるのは楽しいことですが、とイラストのために役立ちます。

クラスの名前空間は、クラスの定義時に作成および入力されることを思い出してください。 つまり、特定のクラス変数に対して1つの割り当てのみを実行しますが、新しいインスタンスが作成されるたびにインスタンス変数を割り当てる必要があります。 例を見てみましょう。

 def called_class(): print "Class assignment" return 2 class Bar(object): y = called_class() def __init__(self, x): self.x = x ## "Class assignment" def called_instance(): print "Instance assignment" return 2 class Foo(object): def __init__(self, x): self.y = called_instance() self.x = x Bar(1) Bar(2) Foo(1) ## "Instance assignment" Foo(2) ## "Instance assignment"

Bar.yに割り当てるのは1回だけですが、__ __init__を呼び出すたびにinstance_of_Foo.yを割り当てます。

さらなる証拠として、Python逆アセンブラを使用してみましょう。

 import dis class Bar(object): y = 2 def __init__(self, x): self.x = x class Foo(object): def __init__(self, x): self.y = 2 self.x = x dis.dis(Bar) ## Disassembly of __init__: ## 7 0 LOAD_FAST 1 (x) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (x) ## 9 LOAD_CONST 0 (None) ## 12 RETURN_VALUE dis.dis(Foo) ## Disassembly of __init__: ## 11 0 LOAD_CONST 1 (2) ## 3 LOAD_FAST 0 (self) ## 6 STORE_ATTR 0 (y) ## 12 9 LOAD_FAST 1 (x) ## 12 LOAD_FAST 0 (self) ## 15 STORE_ATTR 1 (x) ## 18 LOAD_CONST 0 (None) ## 21 RETURN_VALUE

バイトコードを見ると、 Foo.__init__が2つの割り当てを行う必要があるのに対し、 Bar.__init__は1つだけを行う必要があることは明らかです。

実際には、このゲインは実際にはどのように見えますか? 私は、タイミングテストがしばしば制御できない要因に大きく依存しており、それらの違いを正確に説明するのが難しいことを最初に認めます。

ただし、これらの小さなスニペット(Python timeitモジュールで実行)は、クラス変数とインスタンス変数の違いを説明するのに役立つと思うので、とにかくそれらを含めました。

注:私はOSX10.8.5とPython2.7.2を搭載したMacBookProを使用しています。

初期化

10000000 calls to `Bar(2)`: 4.940s 10000000 calls to `Foo(2)`: 6.043s

Barの初期化は1秒以上高速であるため、ここでの違いは統計的に有意であるように見えます。

では、なぜこれが当てはまるのでしょうか。 1つの推測的な説明: Foo.__init__で2つの割り当てを行いますが、 Bar.__init__ 1つだけ割り当てます。

割り当て

10000000 calls to `Bar(2).y = 15`: 6.232s 10000000 calls to `Foo(2).y = 15`: 6.855s 10000000 `Bar` assignments: 6.232s - 4.940s = 1.292s 10000000 `Foo` assignments: 6.855s - 6.043s = 0.812s

注:timeitを使用して各試行でセットアップコードを再実行する方法はないため、試行で変数を再初期化する必要があります。 時間の2行目は、以前に計算された初期化時間を差し引いた上記の時間を表します。

上記から、 Fooが割り当てを処理するのにBarの約60%しかかからないように見えます。

なぜそうなのですか? 1つの推測的な説明: Bar(2).yに割り当てるとき、最初にインスタンスの名前空間( Bar(2).__dict__[y] )を調べ、 yを見つけられず、次にクラスの名前空間( Bar.__dict__[y] )を調べます。 Bar.__dict__[y] )、次に適切な割り当てを行います。 Foo(2).yに割り当てると、インスタンスの名前空間( Foo(2).__dict__[y] )にすぐに割り当てるので、半分の数のルックアップを実行します。

要約すると、これらのパフォーマンスの向上は実際には重要ではありませんが、これらのテストは概念レベルで興味深いものです。 どちらかといえば、これらの違いがクラス変数とインスタンス変数の機械的な違いを説明するのに役立つことを願っています。

結論は

クラス属性はPythonでは十分に活用されていないようです。 多くのプログラマーは、彼らがどのように機能し、なぜ彼らが役立つのかについて異なる印象を持っています。

私の見解:Pythonクラス変数は、優れたコードの学校内でその位置を占めています。 注意して使用すると、物事を簡素化し、読みやすさを向上させることができます。 しかし、不注意に特定のクラスに投げ込まれると、彼らは必ずあなたをつまずかせます。

付録:プライベートインスタンス変数

含めたかったのですが、自然な入り口がありませんでした…

Pythonには、いわばプライベート変数はありませんが、クラスとインスタンスの名前付けの間のもう1つの興味深い関係には、名前のマングリングがあります。

Pythonスタイルガイドでは、疑似プライベート変数の前に二重アンダースコア「__」を付ける必要があると言われています。 これは、変数が個人的に扱われることを意図していることを他の人に示すだけでなく、ある種の変数へのアクセスを防ぐ方法でもあります。 これが私が意味することです:

 class Bar(object): def __init__(self): self.__zap = 1 a = Bar() a.__zap ## Traceback (most recent call last): ## File "<stdin>", line 1, in <module> ## AttributeError: 'Bar' object has no attribute '__baz' ## Hmm. So what's in the namespace? a.__dict__ {'_Bar__zap': 1} a._Bar__zap ## 1

それを見てください。インスタンス属性__zapには、クラス名のプレフィックスが自動的に付けられ、 _Bar__zapが生成されます。

a._Bar__zapを使用して設定および取得可能ですが、この名前マングリングは、あなた他の人が偶然または無知によってアクセスするのを防ぐため、「プライベート」変数を作成する手段です。

編集:Pedro Werneckが親切に指摘したように、この動作は主にサブクラス化を支援することを目的としています。 PEP 8スタイルガイドでは、(1)サブクラスが特定の属性にアクセスするのを防ぐ、および(2)これらのサブクラスでの名前空間の衝突を防ぐという2つの目的を果たしていると彼らは考えています。 変数のマングリングは便利ですが、Javaに存在するような、官民の区別が想定されたコードを作成するための招待と見なされるべきではありません。

関連:より高度になる:Pythonプログラマーが犯す最も一般的な10の間違いを回避する