Pythonの内部クラスの目的は何ですか?


98

Pythonの内部/ネストされたクラスは私を混乱させます。それらなしでは成し遂げられないものはありますか?もしそうなら、それは何ですか?

回答:


85

http://www.geekinterview.com/question_details/64739からの引用:

内部クラスの利点:

  • クラスの論理的なグループ化:クラスが他の1つのクラスだけに役立つ場合、そのクラスに埋め込み、2つをまとめることが論理的です。このような「ヘルパークラス」をネストすると、パッケージがより効率的になります。
  • カプセル化の増加:2つのトップレベルのクラスAとBを考えます。Bは、そうでなければプライベートとして宣言されるAのメンバーにアクセスする必要があります。クラスAのクラスBを非表示にすることで、メンバーをプライベートに宣言し、Bがそれらにアクセスできます。さらに、B自体を外部の世界から隠すことができます。
  • より読みやすく、保守可能なコード:最上位クラス内に小さなクラスをネストすると、コードが使用される場所にコードが近くなります。

主な利点は組織です。内部クラスで達成できることは、それらがなくても達成できます。


50
もちろん、カプセル化の引数はPythonには適用されません。
ボビンス2009

30
最初の点はPythonにも当てはまりません。1つのモジュールファイルに好きなだけクラスを定義できるため、クラスをまとめて保持でき、パッケージ編成にも影響しません。最後の点は非常に主観的であり、私はそれが有効であるとは思わない。要するに、私はこの答えでPythonの内部クラスの使用をサポートする引数を見つけません。
Chris Arndt、2012年

17
それにもかかわらず、これらがプログラミングで内部クラスが使用される理由です。あなたはただ競合する答えを撃墜しようとしているだけです。この男がここで与えたこの答えは確かです。
反転14

16
@Inversus:同意しません。これは答えではありません。別の言語(Java)に関する他の誰かの答えからの拡張引用です。反対票を投じたし、他の人も同じことを望んでいる。
ケビン

4
私はこの回答に同意し、異議には同意しません。ネストされたクラス Javaの内部クラスではありませんが、便利です。ネストされたクラスの目的は編成です。事実上、1つのクラスを別のクラスの名前空間の下に置きます。そうすることが論理的に理にかなっている場合、これ Pythonicです。「名前空間は非常に重要なアイデアの1つです。もっと多くのことをしましょう!」。たとえばDataLoaderCacheMiss例外をスローする可能性のあるクラスについて考えます。例外をメインクラスの下にネストすると、例外DataLoader.CacheMissをインポートしDataLoaderながら、例外を使用できます。
cbarrick 2017年

50

それらなしでは成し遂げられないものはありますか?

いいえ。これらは通常、最上位でクラスを定義し、それへの参照を外部クラスにコピーすることと完全に同等です。

ネストされたクラスが「許可」されている特別な理由はないと思いますが、明示的に「許可しない」ことは特に意味がありません。

外部/所有者オブジェクトのライフサイクル内に存在し、常に外部クラスのインスタンスへの参照(Javaと同様に内部クラス)を持っているクラスを探している場合、Pythonのネストされたクラスはそうではありません。しかし、あなたはそのようなものをハックすることができます:

import weakref, new

class innerclass(object):
    """Descriptor for making inner classes.

    Adds a property 'owner' to the inner class, pointing to the outer
    owner instance.
    """

    # Use a weakref dict to memoise previous results so that
    # instance.Inner() always returns the same inner classobj.
    #
    def __init__(self, inner):
        self.inner= inner
        self.instances= weakref.WeakKeyDictionary()

    # Not thread-safe - consider adding a lock.
    #
    def __get__(self, instance, _):
        if instance is None:
            return self.inner
        if instance not in self.instances:
            self.instances[instance]= new.classobj(
                self.inner.__name__, (self.inner,), {'owner': instance}
            )
        return self.instances[instance]


# Using an inner class
#
class Outer(object):
    @innerclass
    class Inner(object):
        def __repr__(self):
            return '<%s.%s inner object of %r>' % (
                self.owner.__class__.__name__,
                self.__class__.__name__,
                self.owner
            )

>>> o1= Outer()
>>> o2= Outer()
>>> i1= o1.Inner()
>>> i1
<Outer.Inner inner object of <__main__.Outer object at 0x7fb2cd62de90>>
>>> isinstance(i1, Outer.Inner)
True
>>> isinstance(i1, o1.Inner)
True
>>> isinstance(i1, o2.Inner)
False

(これは、Python 2.6および3.0の新機能であるクラスデコレーターを使用します。それ以外の場合は、クラス定義の後に「Inner = innerclass(Inner)」と言う必要があります。)


5
以下のためのコールというユースケースその(インスタンス外部クラスのインスタンスと関係を持っているすなわちジャワ風の内部クラスは、)は、一般的に内部の内部クラスを定義することによって、Pythonで対処できる方法を彼らが表示されます-外部クラスのself余分な作業を必要としないアウター(通常はインナーのselfように別の識別子を使用するなどinnerself)を使用し、それを介してアウターインスタンスにアクセスできます。
Evgeni Sergeev

WeakKeyDictionaryこの例でa を使用しても、実際にはキーがガベージコレクションされることはありません。これは、値がowner属性を通じてそれぞれのキーを強く参照しているためです。
Kritzefitz

36

これを理解するために頭を回さなければならないことがあります。ほとんどの言語では、クラス定義はコンパイラーへのディレクティブです。つまり、プログラムが実行される前にクラスが作成されます。Pythonでは、すべてのステートメントが実行可能です。つまり、このステートメントは:

class foo(object):
    pass

次のように、実行時に実行されるステートメントです。

x = y + z

つまり、他のクラス内にクラスを作成できるだけでなく、どこにでもクラスを作成できます。このコードを考えてみましょう:

def foo():
    class bar(object):
        ...
    z = bar()

したがって、「内部クラス」の概念は、実際には言語構造ではありません。それはプログラマーの構造です。グイドは、これがここでどのようにして生じたのかについて非常に良い要約を持っています。しかし、基本的に、基本的な考え方は、これにより言語の文法が簡素化されるということです。


16

クラス内のクラスのネスト:

  • ネストされたクラスはクラス定義を膨らませるので、何が起こっているのかわかりにくくなります。

  • ネストされたクラスは、テストをより困難にするカップリングを作成する可能性があります。

  • Pythonでは、Javaとは異なり、ファイル/モジュールに複数のクラスを配置できます。そのため、クラスは依然としてトップレベルのクラスに近いままであり、クラス名の前に「_」を付けて、他のクラスがそれを使用して。

ネストされたクラスが役立つことがわかる場所は関数内です

def some_func(a, b, c):
   class SomeClass(a):
      def some_method(self):
         return b
   SomeClass.__doc__ = c
   return SomeClass

クラスは関数から値をキャプチャし、C ++でテンプレートメタプログラミングのようなクラスを動的に作成できるようにします


7

ネストされたクラスに対する引数は理解していますが、場合によってはそれらを使用する場合があります。二重リンクリストクラスを作成していて、ノードを維持するためのノードクラスを作成する必要があるとします。DoublyLinkedListクラス内にNodeクラスを作成するか、DoublyLinkedListクラス外にNodeクラスを作成するかの2つの選択肢があります。NodeクラスはDoublyLinkedListクラス内でのみ意味があるので、この場合は最初の選択肢を選びます。非表示/カプセル化の利点はありませんが、NodeクラスがDoublyLinkedListクラスの一部であると言えるというグループ化の利点があります。


5
これは、同じNodeクラスが、作成する可能性のある他のタイプのリンクリストクラスには役に立たないと想定している場合です。
Acumenus 14

別の言い方をするとNode、の名前空間の下にありDoublyLinkedList、そうすることは論理的に理にかなっています。これ Pythonicです。「名前空間は非常に重要なアイデアの1つです。もっと多くのことをしましょう!」
cbarrick 2017年

@cbarrick:「それらの詳細」を実行しても、ネストについては何も言えません。
イーサンファーマン2018

3

それらなしでは成し遂げられないものはありますか?もしそうなら、それは何ですか?

何かがあります簡単にせずに実行することができません関連クラスの継承は

以下は、関連するクラスAとミニマリストの例Bです。

class A(object):
    class B(object):
        def __init__(self, parent):
            self.parent = parent

    def make_B(self):
        return self.B(self)


class AA(A):  # Inheritance
    class B(A.B):  # Inheritance, same class name
        pass

このコードは、非常に合理的で予測可能な動作をもたらします。

>>> type(A().make_B())
<class '__main__.A.B'>
>>> type(A().make_B().parent)
<class '__main__.A'>
>>> type(AA().make_B())
<class '__main__.AA.B'>
>>> type(AA().make_B().parent)
<class '__main__.AA'>

場合はB、トップレベルのクラスだった、あなたは書くことができなかったself.B()方法ではmake_Bなく、単に書きますB()ので、失う動的結合に適切なクラスにします。

この構造では、クラスAの本体でクラスを参照してはならないことに注意してくださいB。これが、parentクラスに属性を導入する動機ですB

もちろん、この動的バインディングは、クラスの面倒でエラーが発生しやすいインストルメンテーションを犠牲にして、内部クラスなしで再作成できます。


1

私がこれを使用する主なユースケースは、小さなモジュールの急増の防止、個別のモジュールが不要な場合の名前空間の汚染の防止です。既存のクラスを拡張しているが、その既存のクラスは、常にそれに結合されている別のサブクラスを参照する必要がある場合。たとえば、utils.pyモジュールに多くのヘルパークラスが含まれていて、必ずしも一緒に結合されていない場合がありますが、一部のヘルパークラスの結合を強化したいと考えています。たとえば、https://stackoverflow.com/a/8274307/2718295を実装すると

utils.py

import json, decimal

class Helper1(object):
    pass

class Helper2(object):
    pass

# Here is the notorious JSONEncoder extension to serialize Decimals to JSON floats
class DecimalJSONEncoder(json.JSONEncoder):

    class _repr_decimal(float): # Because float.__repr__ cannot be monkey patched
        def __init__(self, obj):
            self._obj = obj
        def __repr__(self):
            return '{:f}'.format(self._obj)

    def default(self, obj): # override JSONEncoder.default
        if isinstance(obj, decimal.Decimal):
            return self._repr_decimal(obj)
        # else
        super(self.__class__, self).default(obj)
        # could also have inherited from object and used return json.JSONEncoder.default(self, obj) 

その後、次のことができます。

>>> from utils import DecimalJSONEncoder
>>> import json, decimal
>>> json.dumps({'key1': decimal.Decimal('1.12345678901234'), 
... 'key2':'strKey2Value'}, cls=DecimalJSONEncoder)
{"key2": "key2_value", "key_1": 1.12345678901234}

もちろん、json.JSONEnocder完全に継承を避け、default()をオーバーライドすることもできます。

import decimal, json

class Helper1(object):
    pass

def json_encoder_decimal(obj):
    class _repr_decimal(float):
        ...

    if isinstance(obj, decimal.Decimal):
        return _repr_decimal(obj)

    return json.JSONEncoder(obj)


>>> json.dumps({'key1': decimal.Decimal('1.12345678901234')}, default=json_decimal_encoder)
'{"key1": 1.12345678901234}'

しかし、場合utilsによっては、単なる慣例として、拡張性のためにクラスで構成する必要があります。

次に、別のユースケースを示します。OuterClass内のミュータブルのファクトリーを呼び出さずに必要copyです。

class OuterClass(object):

    class DTemplate(dict):
        def __init__(self):
            self.update({'key1': [1,2,3],
                'key2': {'subkey': [4,5,6]})


    def __init__(self):
        self.outerclass_dict = {
            'outerkey1': self.DTemplate(),
            'outerkey2': self.DTemplate()}



obj = OuterClass()
obj.outerclass_dict['outerkey1']['key2']['subkey'].append(4)
assert obj.outerclass_dict['outerkey2']['key2']['subkey'] == [4,5,6]

このパターン@staticmethodは、ファクトリ関数に使用するデコレータよりも好みます。


1

1.機能的に同等の2つの方法

前に示した2つの方法は機能的に同じです。ただし、微妙な違いがあり、どちらかを選択したい場合があります。

方法1:ネストされたクラスの定義
(= "ネストされたクラス")

class MyOuter1:
    class Inner:
        def show(self, msg):
            print(msg)

方法2:モジュールレベルの内部クラスが外部クラスにアタッチされている場合
(= "参照された内部クラス")

class _InnerClass:
    def show(self, msg):
        print(msg)

class MyOuter2:
    Inner = _InnerClass

アンダースコアは、PEP8に準拠するために使用されます「内部インターフェイス(パッケージ、モジュール、クラス、関数、属性、またはその他の名前)の前に単一のアンダースコアを付ける必要があります。」

2.類似点

以下のコードスニペットは、「ネストされたクラス」と「参照された内部クラス」の機能の類似点を示しています。これらは、内部クラスインスタンスの型をコードチェックする場合と同じように動作します。言うまでもなく、はおよびとm.inner.anymethod()同様に動作しますm1m2

m1 = MyOuter1()
m2 = MyOuter2()

innercls1 = getattr(m1, 'Inner', None)
innercls2 = getattr(m2, 'Inner', None)

isinstance(innercls1(), MyOuter1.Inner)
# True

isinstance(innercls2(), MyOuter2.Inner)
# True

type(innercls1()) == mypackage.outer1.MyOuter1.Inner
# True (when part of mypackage)

type(innercls2()) == mypackage.outer2.MyOuter2.Inner
# True (when part of mypackage)

3.違い

「ネストされたクラス」と「参照された内部クラス」の違いを以下に示します。それらは大きくありませんが、これらに基づいてどちらか一方を選択したい場合があります。

3.1コードのカプセル化

「ネストされたクラス」を使用すると、「参照される内部クラス」を使用するよりもコードをカプセル化できます。モジュール名前空間のクラスはグローバル変数です。ネストされたクラスの目的は、モジュール内の混乱を減らし、内部クラスを外部クラス内に配置することです。

no-one *がを使用from packagename import *している場合、たとえば、コード補完/インテリセンスを備えたIDEを使用する場合は、モジュールレベルの変数の量が少ないと便利です。

* そうですか?

3.2コードの読みやすさ

Djangoのドキュメントでは、モデルのメタデータに内部クラスMetaを使用するように指示しています。フレームワークのユーザーにclass Foo(models.Model)with inner を書くように指示する方が少し明確です* class Meta

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

代わりに、「書き込みのclass _Meta書き込み、その後、class Foo(models.Model)Meta = _Meta」。

class _Meta:
    ordering = ["horn_length"]
    verbose_name_plural = "oxen"

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • 「ネストされたクラス」アプローチを使用すると、コードはネストされた箇条書きリストを読み取ることができますが、「参照された内部クラス」メソッドを使用すると、上にスクロールして、_Metaその「子アイテム」(属性)を表示するための定義を確認する必要があります。

  • 「参照された内部クラス」メソッドは、コードのネストレベルが大きくなるか、行が他の理由で長い場合に、より読みやすくなります。

*もちろん、好みの問題

3.3少し異なるエラーメッセージ

これは大したことではありませんが、完全を期すためです。内部クラスの存在しない属性にアクセスすると、わずかに異なる例外が発生します。セクション2の例を続ける:

innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'

innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'

これはtype、内部クラスのsが

type(innercls1())
#mypackage.outer1.MyOuter1.Inner

type(innercls2())
#mypackage.outer2._InnerClass

0

私はPythonの内部クラスを使用def test_something():して、100%のテストカバレッジに近づけるために、ユニットテスト関数内(つまり内)でバグのあるサブクラスを意図的に作成しました(たとえば、一部のメソッドをオーバーライドすることで非常にまれにトリガーされるロギングステートメントをテストします)。

振り返ってみると、エドの答えに似ていますhttps://stackoverflow.com/a/722036/1101109

このような内部クラススコープから外れ、それらへのすべての参照が削除されると、ガベージコレクションの準備が整います。たとえば、次のinner.pyファイルを見てください。

class A(object):
    pass

def scope():
    class Buggy(A):
        """Do tests or something"""
    assert isinstance(Buggy(), A)

OSX Python 2.7.6で以下の奇妙な結果が得られます。

>>> from inner import A, scope
>>> A.__subclasses__()
[]
>>> scope()
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A, scope
>>> from inner import A
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A
>>> import gc
>>> gc.collect()
0
>>> gc.collect()  # Yes I needed to call the gc twice, seems reproducible
3
>>> from inner import A
>>> A.__subclasses__()
[]

ヒント-Djangoモデルで続行しないでください。これは、バグのあるクラスへの他の(キャッシュされた)参照を保持しているようです。

したがって、一般に、このような目的で内部クラスを使用することはお勧めしません。ただし、実際に100%のテストカバレッジを評価し、他のメソッドを使用できない場合を除きます。を使用する場合、内部クラスによって汚染される場合__subclasses__()あることを認識しておくのは良いことだと思います。どちらにしても、ここまでたどっていれば、現時点では、Python、プライベートダンダースコアなど、かなり深いところにいると思います。


3
これは、すべてのサブクラスと話ではありませんではない内部クラス?A
klaas

上記のキャスでは、バギーはAを継承しています。サブラスはそれを示しています。組み込み関数issubclass()
klaas

@klaasに感謝します。Python .__subclasses__()でスコープの外に出たときに、内部クラスがガベージコレクターとどのように相互作用するかを理解するために単に使用していることが、より明確になると思います。それは視覚的に投稿を支配しているように見えるので、最初の1〜3段落はもう少し拡張するに値します。
pzrq
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.