Qual è lo scopo delle classi interne di Python?


98

Le classi interne / annidate di Python mi confondono. C'è qualcosa che non può essere realizzato senza di loro? Se è così, cos'è quella cosa?

Risposte:


85

Citato da http://www.geekinterview.com/question_details/64739 :

Vantaggi della classe interna:

  • Raggruppamento logico delle classi : se una classe è utile solo per un'altra classe, è logico incorporarla in quella classe e tenerle insieme. La nidificazione di queste "classi di supporto" rende il loro pacchetto più snello.
  • Incapsulamento aumentato : si consideri due classi di primo livello A e B in cui B necessita dell'accesso ai membri di A che altrimenti sarebbero dichiarati privati. Nascondendo la classe B all'interno della classe AA i membri possono essere dichiarati privati ​​e B può accedervi. Inoltre B stesso può essere nascosto al mondo esterno.
  • Codice più leggibile e gestibile: annidare piccole classi all'interno di classi di primo livello colloca il codice più vicino a dove viene utilizzato.

Il vantaggio principale è l'organizzazione. Tutto ciò che può essere realizzato con le classi interne può essere realizzato senza di loro.


50
L'argomento dell'incapsulamento ovviamente non si applica a Python.
bobince

30
Il primo punto non si applica neanche a Python. Puoi definire tutte le classi che desideri in un file modulo, mantenendole così insieme e anche l'organizzazione del pacchetto non ne viene influenzata. L'ultimo punto è molto soggettivo e non credo sia valido. In breve, in questa risposta non trovo alcun argomento che supporti l'uso di classi interne in Python.
Chris Arndt

17
Tuttavia, queste SONO le ragioni per cui le classi interne vengono utilizzate nella programmazione. Stai solo cercando di abbattere una risposta in competizione. Questa risposta che questo tizio ha dato qui è solida.
Inversus

16
@ Inversus: non sono d'accordo. Questa non è una risposta, è una citazione estesa dalla risposta di qualcun altro su un linguaggio diverso (vale a dire Java). Downvoted e spero che altri facciano lo stesso.
Kevin

4
Sono d'accordo con questa risposta e non sono d'accordo con le obiezioni. Sebbene le classi annidate non siano classi interne di Java, sono utili. Lo scopo di una classe annidata è l'organizzazione. In effetti, stai mettendo una classe sotto lo spazio dei nomi di un'altra. Quando ha senso logico farlo, questo è Pythonic: "Gli spazi dei nomi sono una grande idea clamorosa - facciamone di più!". Ad esempio, considera una DataLoaderclasse che può generare CacheMissun'eccezione. Annidare l'eccezione sotto la classe principale come DataLoader.CacheMisssignifica che puoi importare solo DataLoaderma usare comunque l'eccezione.
cbarrick

50

C'è qualcosa che non può essere realizzato senza di loro?

No. Sono assolutamente equivalenti a definire la classe normalmente al livello più alto e quindi copiare un riferimento ad essa nella classe esterna.

Non penso che ci sia alcun motivo speciale per cui le classi annidate sono "consentite", a parte il fatto che non ha alcun senso particolare anche "non consentirle" esplicitamente.

Se stai cercando una classe che esiste all'interno del ciclo di vita dell'oggetto esterno / proprietario e ha sempre un riferimento a un'istanza della classe esterna - classi interne come fa Java - allora le classi nidificate di Python non sono quella cosa. Ma si può incidere qualcosa del genere che cosa:

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

(Questo utilizza decoratori di classi, che sono nuovi in ​​Python 2.6 e 3.0. Altrimenti dovresti dire "Inner = innerclass (Inner)" dopo la definizione della classe.)


5
I casi d'uso che richiedono che (cioè Java campionature classi interne, le cui istanze avere un rapporto con istanze della classe esterna) in genere può essere affrontata in Python definendo la classe interna all'interno metodi della classe esterna - vedranno la outer selfsenza alcun lavoro aggiuntivo richiesto (basta usare un identificatore diverso dove normalmente si inseriresti quello interno self; mi piace innerself) e sarà in grado di accedere all'istanza esterna tramite quello.
Evgeni Sergeev

L'uso di un WeakKeyDictionaryin questo esempio non consente in realtà la raccolta di dati inutili delle chiavi, perché i valori fanno fortemente riferimento alle rispettive chiavi attraverso il loro ownerattributo.
Kritzefitz

36

C'è qualcosa di cui hai bisogno per capire questo. Nella maggior parte dei linguaggi, le definizioni di classe sono direttive per il compilatore. Cioè, la classe viene creata prima che il programma venga mai eseguito. In Python, tutte le istruzioni sono eseguibili. Ciò significa che questa affermazione:

class foo(object):
    pass

è un'istruzione che viene eseguita in fase di esecuzione proprio come questa:

x = y + z

Ciò significa che non solo puoi creare classi all'interno di altre classi, ma puoi creare classi ovunque tu voglia. Considera questo codice:

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

Quindi, l'idea di una "classe interna" non è realmente un costrutto del linguaggio; è un costrutto del programmatore. Guido ha un ottimo riassunto di come è successo qui . Ma essenzialmente, l'idea di base è che questo semplifica la grammatica della lingua.


16

Annidamento delle classi all'interno delle classi:

  • Le classi annidate gonfiano la definizione della classe rendendo più difficile vedere cosa sta succedendo.

  • Le classi annidate possono creare accoppiamenti che renderebbero i test più difficili.

  • In Python puoi mettere più di una classe in un file / modulo, a differenza di Java, quindi la classe rimane ancora vicina alla classe di primo livello e potrebbe anche avere il nome della classe preceduto da un "_" per aiutare a significare che gli altri non dovrebbero essere usandolo.

Il luogo in cui le classi annidate possono rivelarsi utili è all'interno delle funzioni

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

La classe acquisisce i valori dalla funzione consentendo di creare dinamicamente una classe come la metaprogrammazione del modello in C ++


7

Capisco gli argomenti contro le classi annidate, ma in alcune occasioni è opportuno utilizzarle. Immagina di creare una classe di elenco a doppio collegamento e di dover creare una classe di nodo per mantenere i nodi. Ho due scelte, creare la classe Node all'interno della classe DoublyLinkedList o creare la classe Node all'esterno della classe DoublyLinkedList. Preferisco la prima scelta in questo caso, perché la classe Node è significativa solo all'interno della classe DoublyLinkedList. Sebbene non vi sia alcun vantaggio di nascondere / incapsulare, esiste un vantaggio di raggruppamento di poter dire che la classe Node fa parte della classe DoublyLinkedList.


5
Ciò è vero supponendo che la stessa Nodeclasse non sia utile per altri tipi di classi di elenchi collegati che potresti creare, nel qual caso dovrebbe probabilmente essere solo all'esterno.
Acumenus

Un altro modo per dirlo: Nodeè sotto lo spazio dei nomi di DoublyLinkedList, e ha senso logico che sia così. Questo è Pythonic: "Gli spazi dei nomi sono una grande idea clamorosa - facciamone di più!"
cbarrick

@cbarrick: Fare "più di quelli" non dice nulla sull'annidamento di loro.
Ethan Furman,

3

C'è qualcosa che non può essere realizzato senza di loro? Se è così, cos'è quella cosa?

C'è qualcosa di cui non si può fare facilmente a meno : l' ereditarietà delle classi correlate .

Ecco un esempio minimalista con le classi correlate Ae 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

Questo codice porta a un comportamento abbastanza ragionevole e prevedibile:

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

Se Bfosse una classe di primo livello, non si potrebbe scrivere self.B()nel metodo make_Bma si scriverebbe semplicemente B(), perdendo così l' associazione dinamica alle classi adeguate.

Nota che in questa costruzione non dovresti mai fare riferimento alla classe Anel corpo della classe B. Questa è la motivazione per introdurre l' parentattributo in classe B.

Naturalmente, questo collegamento dinamico può essere ricreato senza classe interna al costo di una strumentazione noiosa e soggetta a errori delle classi.


1

Il caso d'uso principale per cui lo uso è prevenire la proliferazione di piccoli moduli e prevenire l'inquinamento dello spazio dei nomi quando non sono necessari moduli separati. Se sto estendendo una classe esistente, ma quella classe esistente deve fare riferimento a un'altra sottoclasse che dovrebbe sempre essere accoppiata ad essa. Ad esempio, potrei avere un utils.pymodulo che contiene molte classi helper, che non sono necessariamente accoppiate insieme, ma voglio rafforzare l'accoppiamento per alcune di quelle classi helper. Ad esempio, quando implemento 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) 

Allora possiamo:

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

Ovviamente, avremmo potuto evitare del json.JSONEnocdertutto l' ereditarietà e sovrascrivere semplicemente 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}'

Ma a volte solo per convenzione, vuoi utilsessere composto da classi per l'estensibilità.

Ecco un altro caso d'uso: voglio una fabbrica per mutabili nella mia OuterClass senza dover invocare 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]

Preferisco questo modello al @staticmethoddecoratore che altrimenti useresti per una funzione di fabbrica.


1

1. Due modi funzionalmente equivalenti

I due modi mostrati prima sono funzionalmente identici. Tuttavia, ci sono alcune sottili differenze e ci sono situazioni in cui vorresti sceglierne una piuttosto che un'altra.

Metodo 1: definizione della classe annidata
(= "Classe annidata")

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

Metodo 2: con classe interna a livello di modulo collegata alla classe esterna
(= "classe interna referenziata")

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

class MyOuter2:
    Inner = _InnerClass

Il carattere di sottolineatura viene utilizzato per seguire PEP8 "le interfacce interne (pacchetti, moduli, classi, funzioni, attributi o altri nomi) devono essere precedute da un singolo trattino basso iniziale".

2. Somiglianze

Il frammento di codice riportato di seguito mostra le somiglianze funzionali della "Classe nidificata" rispetto alla "Classe interna referenziata"; Si comporterebbero allo stesso modo nel controllo del codice per il tipo di istanza di una classe interna. Inutile dire che m.inner.anymethod()si comporterebbe in modo simile con m1em2

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

Le differenze tra "Classe nidificata" e "Classe interna referenziata" sono elencate di seguito. Non sono grandi, ma a volte vorresti sceglierne uno o l'altro in base a questi.

3.1 Incapsulamento del codice

Con "Classi annidate" è possibile incapsulare il codice meglio che con "Classe interna referenziata". Una classe nello spazio dei nomi del modulo è una variabile globale . Lo scopo delle classi annidate è ridurre il disordine nel modulo e inserire la classe interna nella classe esterna.

Anche se nessuno * lo utilizza from packagename import *, una quantità ridotta di variabili a livello di modulo può essere utile, ad esempio, quando si utilizza un IDE con completamento del codice / intellisense.

* Giusto?

3.2 Leggibilità del codice

La documentazione di Django indica di utilizzare Meta della classe interna per i metadati del modello. È un po 'più chiaro * indicare agli utenti del framework di scrivere un class Foo(models.Model)con inner class Meta;

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

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

invece di "scrivi a class _Meta, quindi scrivi a class Foo(models.Model)con Meta = _Meta";

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

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Con l'approccio "Classe annidata " il codice può essere letto in un elenco puntato annidato , ma con il metodo "Classe interna referenziata" è necessario scorrere indietro per vedere la definizione di _Metaper vedere i suoi "elementi figlio" (attributi).

  • Il metodo "Classe interna referenziata" può essere più leggibile se il livello di nidificazione del codice aumenta o se le righe sono lunghe per qualche altro motivo.

* Naturalmente, una questione di gusti

3.3 Messaggi di errore leggermente diversi

Questo non è un grosso problema, ma solo per completezza: quando si accede ad un attributo inesistente per la classe interna, vediamo eccezioni leggermente diverse. Continuando l'esempio fornito nella sezione 2:

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

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

Questo perché le types delle classi interne lo sono

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

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

0

Ho usato le classi interne di Python per creare sottoclassi deliberatamente buggate all'interno di funzioni unittest (cioè all'interno def test_something():) al fine di avvicinarmi alla copertura del test del 100% (es. Testare dichiarazioni di registrazione molto raramente attivate sovrascrivendo alcuni metodi).

In retrospettiva è simile alla risposta di Ed https://stackoverflow.com/a/722036/1101109

Tali classi interne dovrebbero uscire dall'ambito ed essere pronte per la garbage collection una volta rimossi tutti i riferimenti ad esse. Ad esempio, prendi il seguente inner.pyfile:

class A(object):
    pass

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

Ottengo i seguenti risultati curiosi con 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__()
[]

Suggerimento: non andare avanti e provare a farlo con i modelli Django, che sembravano mantenere altri riferimenti (memorizzati nella cache?) Alle mie classi con errori.

Quindi, in generale, non consiglierei di utilizzare classi interne per questo tipo di scopo a meno che non apprezzi davvero quella copertura del test al 100% e non puoi utilizzare altri metodi. Anche se penso che sia bello essere consapevoli che se usi il __subclasses__(), a volte può essere inquinato da classi interne. Ad ogni modo, se sei arrivato fin qui, penso che siamo abbastanza approfonditi in Python a questo punto, dunderscores privati ​​e tutto il resto.


3
Non si tratta solo di sottoclassi e non di classi interne ?? A
klaas

Nella casse sopra, Buggy eredita da A. Quindi la sottoclasse lo mostra. Vedi anche la funzione incorporata issubclass ()
klaas

Grazie @klaas, penso che potrebbe essere più chiaro che sto usando solo .__subclasses__()per capire come le classi interne interagiscono con il garbage collector quando le cose escono dallo scopo in Python. Questo sembra visivamente dominare il post, quindi i primi 1-3 paragrafi meritano un po 'più di espansione.
pzrq
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.