Was ist der Zweck der inneren Klassen von Python?

97

Pythons innere / verschachtelte Klassen verwirren mich. Gibt es etwas, das ohne sie nicht erreicht werden kann? Wenn ja, was ist das für ein Ding?

Geo
quelle

Antworten:

85

Zitiert von http://www.geekinterview.com/question_details/64739 :

Vorteile der inneren Klasse:

  • Logische Gruppierung von Klassen : Wenn eine Klasse nur für eine andere Klasse nützlich ist, ist es logisch, sie in diese Klasse einzubetten und die beiden zusammenzuhalten. Durch das Verschachteln solcher "Hilfsklassen" wird das Paket rationalisiert.
  • Erhöhte Kapselung : Betrachten Sie zwei Klassen A und B der obersten Ebene, in denen B Zugriff auf Mitglieder von A benötigt, die andernfalls als privat deklariert würden. Durch das Ausblenden von Klasse B innerhalb der Klasse AA können Mitglieder als privat deklariert werden und B kann auf sie zugreifen. Außerdem kann B selbst vor der Außenwelt verborgen werden.
  • Besser lesbarer, wartbarer Code : Durch das Verschachteln kleiner Klassen in Klassen der obersten Ebene wird der Code näher an dem Ort platziert, an dem er verwendet wird.

Der Hauptvorteil ist die Organisation. Alles, was mit inneren Klassen erreicht werden kann , kann ohne sie erreicht werden.

Aziz
quelle
50
Das Kapselungsargument gilt natürlich nicht für Python.
Bobince
30
Der erste Punkt gilt auch nicht für Python. Sie können in einer Moduldatei beliebig viele Klassen definieren, um sie zusammenzuhalten, und die Paketorganisation ist ebenfalls nicht betroffen. Der letzte Punkt ist sehr subjektiv und ich glaube nicht, dass er gültig ist. Kurz gesagt, ich finde in dieser Antwort keine Argumente, die die Verwendung innerer Klassen in Python unterstützen.
Chris Arndt
17
Dies sind jedoch die Gründe, warum innere Klassen bei der Programmierung verwendet werden. Sie versuchen nur, eine konkurrierende Antwort abzuschießen. Diese Antwort, die dieser Typ hier gegeben hat, ist solide.
Inversus
16
@Inversus: Ich bin anderer Meinung. Dies ist keine Antwort, sondern ein erweitertes Zitat aus der Antwort eines anderen über eine andere Sprache (nämlich Java). Downvoted und ich hoffe, dass andere das Gleiche tun.
Kevin
4
Ich stimme dieser Antwort zu und stimme den Einwänden nicht zu. Verschachtelte Klassen sind zwar keine inneren Klassen von Java, sie sind jedoch nützlich. Der Zweck einer verschachtelten Klasse ist die Organisation. Tatsächlich setzen Sie eine Klasse unter den Namespace einer anderen. Wenn es logisch sinnvoll ist , dies zu tun, dies ist Pythonic: „Namespaces sind eine großartige Idee Hupen - lassen Sie uns mehr davon machen!“. Stellen Sie sich beispielsweise eine DataLoaderKlasse vor, die eine CacheMissAusnahme auslösen kann . Wenn Sie die Ausnahme unter der Hauptklasse verschachteln, können Sie die Ausnahme DataLoader.CacheMissimportieren DataLoaderund trotzdem verwenden.
Cbarrick
50

Gibt es etwas, das ohne sie nicht erreicht werden kann?

Nein. Sie sind absolut gleichbedeutend damit, die Klasse normalerweise auf oberster Ebene zu definieren und dann einen Verweis darauf in die äußere Klasse zu kopieren.

Ich glaube nicht, dass es einen besonderen Grund gibt, warum verschachtelte Klassen "erlaubt" sind, außer dass es keinen besonderen Sinn macht, sie explizit "zu verbieten".

Wenn Sie nach einer Klasse suchen, die im Lebenszyklus des äußeren / Eigentümerobjekts vorhanden ist und immer einen Verweis auf eine Instanz der äußeren Klasse enthält - innere Klassen wie Java -, sind die verschachtelten Klassen von Python nicht das Richtige. Aber man kann etwas zerhacken wie das Ding:

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

(Hierbei werden Klassendekoratoren verwendet, die in Python 2.6 und 3.0 neu sind. Andernfalls müssten Sie nach der Klassendefinition "Inner = innerclass (Inner)" sagen.)

Bobince
quelle
5
Die Anwendungsfälle diesen Aufruf für die (dh Java-artige innere Klassen, deren Instanzen haben eine Beziehung mit Instanzen der äußeren Klasse) kann in der Regel durch die Definition der innere Klasse innerhalb in Python angesprochen werden Methoden der äußeren Klasse - sie das sehen Outer's selfohne zusätzliche Arbeit (verwenden Sie einfach eine andere Kennung, an der Sie normalerweise die Inner's platzieren würden self; like innerself) und können über diese auf die äußere Instanz zugreifen.
Evgeni Sergeev
Die Verwendung von a WeakKeyDictionaryin diesem Beispiel ermöglicht nicht, dass die Schlüssel durch Müll gesammelt werden, da die Werte über ihr ownerAttribut stark auf ihre jeweiligen Schlüssel verweisen .
Kritzefitz
36

Es gibt etwas, das Sie um den Kopf wickeln müssen, um dies verstehen zu können. In den meisten Sprachen sind Klassendefinitionen Anweisungen an den Compiler. Das heißt, die Klasse wird erstellt, bevor das Programm jemals ausgeführt wird. In Python sind alle Anweisungen ausführbar. Das bedeutet, dass diese Aussage:

class foo(object):
    pass

ist eine Anweisung, die zur Laufzeit genau wie diese ausgeführt wird:

x = y + z

Dies bedeutet, dass Sie nicht nur Klassen in anderen Klassen erstellen können, sondern auch Klassen erstellen können, wo immer Sie möchten. Betrachten Sie diesen Code:

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

Die Idee einer "inneren Klasse" ist also nicht wirklich ein Sprachkonstrukt; Es ist ein Programmierkonstrukt. Guido hat eine sehr gute Zusammenfassung, wie dies zustande kam hier . Die Grundidee ist jedoch, dass dies die Grammatik der Sprache vereinfacht.

Jason Baker
quelle
16

Verschachtelungsklassen innerhalb von Klassen:

  • Verschachtelte Klassen blähen die Klassendefinition auf und machen es schwieriger zu sehen, was los ist.

  • Verschachtelte Klassen können eine Kopplung erzeugen, die das Testen erschweren würde.

  • In Python können Sie im Gegensatz zu Java mehr als eine Klasse in eine Datei / ein Modul einfügen, sodass die Klasse weiterhin in der Nähe der Klasse der obersten Ebene bleibt und dem Klassennamen sogar ein "_" vorangestellt werden kann, um anzuzeigen, dass dies nicht der Fall sein sollte es benutzen.

Der Ort, an dem sich verschachtelte Klassen als nützlich erweisen können, liegt innerhalb von Funktionen

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

Die Klasse erfasst die Werte aus der Funktion, sodass Sie dynamisch eine Klasse wie die Metaprogrammierung von Vorlagen in C ++ erstellen können

Ed.
quelle
7

Ich verstehe die Argumente gegen verschachtelte Klassen, aber es gibt Gründe, sie gelegentlich zu verwenden. Stellen Sie sich vor, ich erstelle eine doppelt verknüpfte Listenklasse und muss eine Knotenklasse für die Wartung der Knoten erstellen. Ich habe zwei Möglichkeiten: Erstellen Sie eine Node-Klasse innerhalb der DoublyLinkedList-Klasse oder erstellen Sie die Node-Klasse außerhalb der DoublyLinkedList-Klasse. In diesem Fall bevorzuge ich die erste Wahl, da die Node-Klasse nur innerhalb der DoublyLinkedList-Klasse von Bedeutung ist. Es gibt zwar keinen Vorteil beim Ausblenden / Einkapseln, aber einen Gruppierungsvorteil, wenn Sie sagen können, dass die Node-Klasse Teil der DoublyLinkedList-Klasse ist.

user411279
quelle
5
Dies gilt unter der Annahme, dass dieselbe NodeKlasse für andere Arten von verknüpften Listenklassen, die Sie möglicherweise auch erstellen, nicht nützlich ist. In diesem Fall sollte sie wahrscheinlich nur außerhalb liegen.
Acumenus
Ein anderer Weg, um es Nodeauszudrücken : steht unter dem Namespace von DoublyLinkedList, und es ist logisch sinnvoll, dies zu tun. Dies ist Pythonic: "Namespaces sind eine großartige Idee - lasst uns mehr davon machen!"
Cbarrick
@cbarrick: "mehr von denen" zu tun, sagt nichts darüber aus, sie zu verschachteln.
Ethan Furman
3

Gibt es etwas, das ohne sie nicht erreicht werden kann? Wenn ja, was ist das für ein Ding?

Es gibt etwas, ohne das es nicht einfach geht : die Vererbung verwandter Klassen .

Hier ist ein minimalistisches Beispiel mit den zugehörigen Klassen Aund 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

Dieser Code führt zu einem vernünftigen und vorhersehbaren Verhalten:

>>> 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'>

Wenn Sie Beine Klasse der obersten Ebene wären , könnten Sie nicht self.B()in die Methode make_Bschreiben B(), sondern würden einfach schreiben und somit die dynamische Bindung an die entsprechenden Klassen verlieren .

Beachten Sie, dass Sie sich in dieser Konstruktion niemals auf eine Klasse Aim Hauptteil der Klasse beziehen sollten B. Dies ist die Motivation, das parentAttribut in der Klasse einzuführen B.

Natürlich kann diese dynamische Bindung ohne innere Klasse auf Kosten einer mühsamen und fehleranfälligen Instrumentierung der Klassen wiederhergestellt werden .

Pascal Sotin
quelle
1

Der Hauptanwendungsfall, für den ich dies verwende, ist die Verhinderung der Verbreitung kleiner Module und die Verhinderung der Verschmutzung von Namespaces, wenn keine separaten Module benötigt werden. Wenn ich eine vorhandene Klasse erweitere, diese vorhandene Klasse jedoch auf eine andere Unterklasse verweisen muss, die immer mit dieser gekoppelt sein sollte. Zum Beispiel kann ich ein utils.pyModul haben, das viele Hilfsklassen enthält, die nicht unbedingt miteinander gekoppelt sind, aber ich möchte die Kopplung für einige dieser Hilfsklassen verstärken. Zum Beispiel, wenn ich https://stackoverflow.com/a/8274307/2718295 implementiere

: 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) 

Dann können wir:

>>> 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}

Natürlich hätten wir das Erben vermeiden können json.JSONEnocder ganz und einfach default () überschreiben können:

::

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}'

Aber manchmal nur für Konventionen, wollen Sie utils aus Klassen bestehen, um die Erweiterbarkeit zu gewährleisten.

Hier ist ein weiterer Anwendungsfall: Ich möchte eine Factory für veränderbare Elemente in meiner OuterClass, ohne sie aufrufen zu müssen 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]

Ich bevorzuge dieses Muster gegenüber dem @staticmethodDekorateur, den Sie sonst für eine Fabrikfunktion verwenden würden.

Cowbert
quelle
0

Ich habe Pythons innere Klassen verwendet, um absichtlich fehlerhafte Unterklassen innerhalb unittest Funktionen (dh innerhalb def test_something():) zu erstellen , um einer 100% igen Testabdeckung näher zu kommen (z. B. das Testen sehr selten ausgelöster Protokollierungsanweisungen durch Überschreiben einiger Methoden).

Rückblickend ähnelt es Eds Antwort https://stackoverflow.com/a/722036/1101109

Solche inneren Klassen sollten außerhalb des Gültigkeitsbereichs liegen und für die Speicherbereinigung bereit sein, sobald alle Verweise auf sie entfernt wurden. Nehmen Sie zum Beispiel die folgende inner.pyDatei:

class A(object):
    pass

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

Unter OSX Python 2.7.6 erhalte ich die folgenden merkwürdigen Ergebnisse:

>>> 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__()
[]

Tipp - Versuchen Sie dies nicht mit Django-Modellen, die andere (zwischengespeicherte?) Verweise auf meine Buggy-Klassen zu behalten schienen.

Daher würde ich im Allgemeinen nicht empfehlen, innere Klassen für diesen Zweck zu verwenden, es sei denn, Sie schätzen diese 100% ige Testabdeckung wirklich und können keine anderen Methoden verwenden. Obwohl ich denke, es ist schön zu wissen, dass wenn Sie das verwenden __subclasses__(), es manchmal durch innere Klassen verschmutzt werden kann. So oder so, wenn Sie so weit gefolgt sind, denke ich, dass wir an diesem Punkt ziemlich tief in Python sind, private Dunderscores und alles.

pzrq
quelle
3
Geht es nicht nur um Unterklassen und nicht um innere Klassen? A
klaas
In der oben genannten Klasse erbt Buggy von A. So zeigt Sublass das. Siehe auch eingebaute Funktion issubclass ()
klaas
Vielen Dank an @klaas, ich denke, es könnte klarer gemacht werden, dass ich nur verwende, um .__subclasses__()zu verstehen, wie innere Klassen mit dem Garbage Collector interagieren, wenn die Dinge in Python außer Reichweite geraten. Das scheint den Beitrag visuell zu dominieren, daher verdienen die ersten 1-3 Absätze etwas mehr Erweiterung.
pzrq
0

1. Zwei funktional äquivalente Wege

Die beiden zuvor gezeigten Möglichkeiten sind funktional identisch. Es gibt jedoch einige subtile Unterschiede, und es gibt Situationen, in denen Sie einander vorziehen möchten.

Weg 1: Verschachtelte Klassendefinition
(= "Verschachtelte Klasse")

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

Weg 2: Mit Modulstufe Innere Klasse an Äußere Klasse angehängt
(= "Referenzierte innere Klasse")

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

class MyOuter2:
    Inner = _InnerClass

Der Unterstrich wird verwendet, um PEP8 zu folgen . "Interne Schnittstellen (Pakete, Module, Klassen, Funktionen, Attribute oder andere Namen) sollten - mit einem einzelnen führenden Unterstrich versehen werden."

2. Ähnlichkeiten

Das folgende Codefragment zeigt die funktionalen Ähnlichkeiten der "verschachtelten Klasse" mit der "referenzierten inneren Klasse". Sie würden sich bei der Codeprüfung für den Typ einer Instanz der inneren Klasse genauso verhalten. Unnötig zu erwähnen, m.inner.anymethod()dass sich die mit m1und ähnlich verhalten würdenm2

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. Unterschiede

Die Unterschiede zwischen "Verschachtelte Klasse" und "Referenzierte innere Klasse" sind unten aufgeführt. Sie sind nicht groß, aber manchmal möchten Sie das eine oder andere basierend auf diesen auswählen.

3.1 Codekapselung

Mit "Verschachtelten Klassen" ist es möglich, Code besser zu kapseln als mit "Referenzierte innere Klasse". Eine Klasse im Modul-Namespace ist eine globale Variable. Der Zweck verschachtelter Klassen besteht darin, die Unordnung im Modul zu verringern und die innere Klasse in die äußere Klasse einzufügen.

Während niemand * verwendet from packagename import *, kann eine geringe Anzahl von Variablen auf Modulebene hilfreich sein, beispielsweise wenn eine IDE mit Code-Vervollständigung / Intellisense verwendet wird.

* Richtig?

3.2 Lesbarkeit des Codes

In der Django-Dokumentation wird angewiesen, Meta der inneren Klasse für Modellmetadaten zu verwenden. Es ist etwas klarer *, die Framework-Benutzer anzuweisen, ein class Foo(models.Model)mit inner zu schreiben class Meta;

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

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

anstelle von "schreibe ein class _Meta, dann schreibe ein class Foo(models.Model)mit Meta = _Meta";

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

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Beim Ansatz "Verschachtelte Klasse" kann der Code als verschachtelte Aufzählungspunktliste gelesen werden. Bei der Methode "Referenzierte innere Klasse" muss jedoch ein Bildlauf nach oben durchgeführt werden, um die Definition von _Metaanzuzeigen und die "untergeordneten Elemente" (Attribute) anzuzeigen.

  • Die Methode "Referenzierte innere Klasse" kann besser lesbar sein, wenn Ihre Verschachtelungsebene wächst oder die Zeilen aus einem anderen Grund lang sind.

* Natürlich Geschmackssache

3.3 Etwas andere Fehlermeldungen

Dies ist keine große Sache, sondern nur der Vollständigkeit halber: Beim Zugriff auf nicht vorhandene Attribute für die innere Klasse sehen wir leicht unterschiedliche Ausnahmen. Fortsetzung des Beispiels in Abschnitt 2:

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

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

Dies liegt daran, dass die types der inneren Klassen sind

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

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