Jaki jest cel klas wewnętrznych Pythona?


100

Mylą mnie wewnętrzne / zagnieżdżone klasy w Pythonie. Czy jest coś, czego nie można osiągnąć bez nich? Jeśli tak, co to jest?

Odpowiedzi:


89

Cytat z http://www.geekinterview.com/question_details/64739 :

Zalety klasy wewnętrznej:

  • Logiczne grupowanie klas : jeśli klasa jest przydatna tylko dla jednej innej klasy, logiczne jest osadzenie jej w tej klasie i trzymanie obu razem. Zagnieżdżanie takich „klas pomocniczych” sprawia, że ​​ich pakiet jest bardziej uproszczony.
  • Zwiększona hermetyzacja : rozważ dwie klasy najwyższego poziomu A i B, w których B potrzebuje dostępu do elementów członkowskich A, które w przeciwnym razie zostałyby zadeklarowane jako prywatne. Ukrywając klasę B w obrębie klasy AA, członkowie klasy AA mogą być uznani za prywatnych i B ma do nich dostęp. Ponadto samo B można ukryć przed światem zewnętrznym.
  • Bardziej czytelny, łatwiejszy w utrzymaniu kod : Zagnieżdżenie małych klas w klasach najwyższego poziomu umieszcza kod bliżej miejsca, w którym jest używany.

Główną zaletą jest organizacja. Wszystko, co można osiągnąć za pomocą klas wewnętrznych, można osiągnąć bez nich.


50
Argument hermetyzacji oczywiście nie dotyczy Pythona.
bobince

31
Pierwszy punkt również nie dotyczy Pythona. Możesz zdefiniować dowolną liczbę klas w jednym pliku modułu, w ten sposób utrzymując je razem i nie ma to wpływu na organizację pakietu. Ostatni punkt jest bardzo subiektywny i nie wierzę, że jest ważny. Krótko mówiąc, nie znajduję w tej odpowiedzi żadnych argumentów wspierających użycie klas wewnętrznych w Pythonie.
Chris Arndt

17
Niemniej jednak są to powody, dla których w programowaniu używane są klasy wewnętrzne. Próbujesz tylko zestrzelić konkurencyjną odpowiedź. Ta odpowiedź, której tu udzielił ten koleś, jest solidna.
Inversus

16
@Inversus: Nie zgadzam się. To nie jest odpowiedź, to rozszerzony cytat z cudzej odpowiedzi na temat innego języka (a mianowicie Java). Negowany i mam nadzieję, że inni zrobią to samo.
Kevin,

5
Zgadzam się z tą odpowiedzią i nie zgadzam się z zastrzeżeniami. Chociaż klasy zagnieżdżone nie są klasami wewnętrznymi Javy, są przydatne. Celem klasy zagnieżdżonej jest organizacja. W efekcie umieszczasz jedną klasę w przestrzeni nazw innej. Kiedy ma to logiczny sens, jest to Pythonic: „Przestrzenie nazw to jeden wspaniały pomysł - zróbmy ich więcej!”. Na przykład rozważmy DataLoaderklasę, która może zgłosić CacheMisswyjątek. Zagnieżdżenie wyjątku w klasie głównej DataLoader.CacheMissoznacza, że ​​możesz importować tylko DataLoaderwyjątek, ale nadal go używać.
cbarrick

50

Czy jest coś, czego nie można osiągnąć bez nich?

Nie. Są one absolutnie równoważne definiowaniu klasy normalnie na najwyższym poziomie, a następnie kopiowaniu odniesienia do niej do klasy zewnętrznej.

Nie sądzę, by istniał jakiś szczególny powód, dla którego klasy zagnieżdżone są „dozwolone”, poza tym, że nie ma sensu jawne „zabronienie” ich dostępu.

Jeśli szukasz klasy, która istnieje w cyklu życia obiektu zewnętrznego / właściciela i zawsze ma odniesienie do wystąpienia klasy zewnętrznej - klas wewnętrznych, jak robi to Java - to zagnieżdżone klasy Pythona nie są tym czymś. Ale można włamać się coś podobnego tej rzeczy:

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

(Używa to dekoratorów klas, które są nowe w Pythonie 2.6 i 3.0. W przeciwnym razie po definicji klasy należałoby powiedzieć „Inner = innerclass (Inner)”).


5
Zastosowanie-przypadki, które wymagają , że (tj Java-owskiej klasy wewnętrzne, których przypadki mają związek z przypadkami zewnętrznej klasy) zwykle można rozwiązać w Pythonie definiując wewnętrzną klasę wewnątrz metod zewnętrznej klasy - ujrzą zewnętrzne selfbez dodatkowej pracy (po prostu użyj innego identyfikatora, w którym zwykle umieszczasz wewnętrzny self; na przykład innerself) i będziesz mógł uzyskać dostęp do zewnętrznej instancji przez to.
Evgeni Sergeev

Użycie WeakKeyDictionaryw tym przykładzie w rzeczywistości nie pozwala na zbieranie kluczy bezużytecznych, ponieważ wartości silnie odwołują się do odpowiednich kluczy za pośrednictwem ich owneratrybutu.
Kritzefitz

36

Jest coś, co musisz owinąć głowę, aby to zrozumieć. W większości języków definicje klas są dyrektywami kompilatora. Oznacza to, że klasa jest tworzona przed uruchomieniem programu. W Pythonie wszystkie instrukcje są wykonywalne. Oznacza to, że to stwierdzenie:

class foo(object):
    pass

to instrukcja wykonywana w czasie wykonywania, tak jak ta:

x = y + z

Oznacza to, że możesz nie tylko tworzyć zajęcia w ramach innych klas, ale także tworzyć je w dowolnym miejscu. Rozważ ten kod:

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

Zatem idea „klasy wewnętrznej” nie jest w rzeczywistości konstrukcją językową; to konstrukcja programisty. Guido ma bardzo dobre podsumowanie tego, jak do tego doszło tutaj . Zasadniczo jednak podstawową ideą jest to, że upraszcza to gramatykę języka.


16

Zagnieżdżanie klas w klasach:

  • Klasy zagnieżdżone nadużywają definicji klasy, utrudniając dostrzeżenie, co się dzieje.

  • Klasy zagnieżdżone mogą tworzyć sprzężenia, które utrudniałyby testowanie.

  • W Pythonie możesz umieścić więcej niż jedną klasę w pliku / module, w przeciwieństwie do Javy, więc klasa nadal pozostaje zbliżona do klasy najwyższego poziomu i może nawet mieć nazwę klasy poprzedzoną „_”, aby pomóc zasygnalizować, że inne nie powinny być Użyj tego.

Miejsce, w którym zagnieżdżone klasy mogą okazać się przydatne, znajduje się w funkcjach

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

Klasa przechwytuje wartości z funkcji, umożliwiając dynamiczne tworzenie klasy, takiej jak metaprogramowanie szablonu w C ++


7

Rozumiem argumenty przeciwko klasom zagnieżdżonym, ale w niektórych przypadkach warto ich używać. Wyobraź sobie, że tworzę podwójnie połączoną klasę listy i muszę utworzyć klasę węzłów do obsługi węzłów. Mam dwie możliwości: utworzyć klasę Node wewnątrz klasy DoublyLinkedList lub utworzyć klasę Node poza klasą DoublyLinkedList. W tym przypadku wolę pierwszy wybór, ponieważ klasa Node ma znaczenie tylko w klasie DoublyLinkedList. Chociaż nie ma korzyści z ukrycia / hermetyzacji, istnieje korzyść z grupowania, że ​​można powiedzieć, że klasa Node jest częścią klasy DoublyLinkedList.


5
Jest to prawdą, zakładając, że ta sama Nodeklasa nie jest użyteczna dla innych typów połączonych klas list, które możesz również utworzyć, w takim przypadku powinna prawdopodobnie znajdować się na zewnątrz.
Acumenus,

Ujmując to inaczej: Nodeznajduje się w przestrzeni nazw DoublyLinkedListi ma to logiczny sens. To jest pythonowy: „Przestrzenie nazw są jednym trąbić świetny pomysł - zróbmy więcej z nich!”
cbarrick

@cbarrick: Wykonywanie „większej liczby” nic nie mówi o ich zagnieżdżaniu.
Ethan Furman,

4

Czy jest coś, czego nie można osiągnąć bez nich? Jeśli tak, co to jest?

Jest coś, bez czego nie da się łatwo zrobić : dziedziczenie pokrewnych klas .

Oto minimalistyczny przykład z powiązanymi klasami Ai 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

Ten kod prowadzi do całkiem rozsądnego i przewidywalnego zachowania:

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

Gdyby Bbyła to klasa najwyższego poziomu, nie można by pisać self.B()w metodzie, make_Bale po prostu pisać B(), a tym samym tracić dynamiczne powiązanie z odpowiednimi klasami.

Zauważ, że w tej konstrukcji nigdy nie powinieneś odnosić się do class Aw treści class B. To jest motywacja do wprowadzenia parentatrybutu w klasie B.

Oczywiście to dynamiczne powiązanie można odtworzyć bez klasy wewnętrznej, kosztem żmudnej i podatnej na błędy instrumentacji klas.


1

Głównym przypadkiem użycia, w którym to wykorzystuję, jest zapobieganie rozprzestrzenianiu się małych modułów i zapobieganie zanieczyszczeniu przestrzeni nazw, gdy oddzielne moduły nie są potrzebne. Jeśli rozszerzam istniejącą klasę, ale ta istniejąca klasa musi odwoływać się do innej podklasy, która zawsze powinna być z nią połączona. Na przykład mogę mieć utils.pymoduł zawierający wiele klas pomocniczych, które niekoniecznie są ze sobą połączone, ale chcę wzmocnić sprzężenie dla niektórych z tych klas pomocniczych. Na przykład, kiedy wdrażam 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) 

Więc możemy:

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

Oczywiście mogliśmy json.JSONEnocdercałkowicie zrezygnować z dziedziczenia i po prostu nadpisać 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}'

Ale czasami dla konwencji chcesz utilsskładać się z klas w celu rozszerzalności.

Oto inny przypadek użycia: chcę mieć fabrykę mutable w mojej OuterClass bez konieczności wywoływania 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]

Wolę ten wzór od @staticmethoddekoratora, którego w innym przypadku używałbyś do funkcji fabrycznej.


1

1. Dwa funkcjonalnie równoważne sposoby

Przedstawione wcześniej dwa sposoby są funkcjonalnie identyczne. Istnieją jednak pewne subtelne różnice i są sytuacje, w których chciałbyś wybrać jedną z nich.

Sposób 1: Definicja klasy zagnieżdżonej
(= „Klasa zagnieżdżona”)

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

Sposób 2: Z klasą wewnętrzną na poziomie modułu dołączoną do klasy Outer
(= „Klasa wewnętrzna z odniesieniem”)

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

class MyOuter2:
    Inner = _InnerClass

Podkreślenie jest używane w celu śledzenia wewnętrznych interfejsów PEP8 (pakiety, moduły, klasy, funkcje, atrybuty lub inne nazwy) powinny być poprzedzone pojedynczym początkowym podkreśleniem.

2. Podobieństwa

Poniższy fragment kodu demonstruje funkcjonalne podobieństwa między „Klasą zagnieżdżoną” a „Klasą wewnętrzną z odniesieniem”; Zachowywałyby się w ten sam sposób podczas sprawdzania kodu dla typu wystąpienia klasy wewnętrznej. Nie trzeba dodawać, m.inner.anymethod()że zachowywałyby się podobnie z m1im2

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. Różnice

Poniżej wymieniono różnice między „Klasą zagnieżdżoną” i „Klasą wewnętrzną z odniesieniem”. Nie są duże, ale czasami chciałbyś wybrać jedną lub drugą na podstawie tych.

3.1 Hermetyzacja kodu

Dzięki „Klasom zagnieżdżonym” możliwe jest lepsze hermetyzowanie kodu niż w przypadku „Klasy wewnętrznej, do której istnieją odniesienia”. Klasa w przestrzeni nazw modułu jest klasą globalną zmienną . Celem klas zagnieżdżonych jest zmniejszenie bałaganu w module i umieszczenie klasy wewnętrznej wewnątrz klasy zewnętrznej.

Chociaż nikt * nie używa from packagename import *, mała ilość zmiennych na poziomie modułu może być fajna, na przykład podczas używania IDE z uzupełnianiem kodu / inteligencją.

* Prawda?

3.2 Czytelność kodu

Dokumentacja Django instruuje, aby do metadanych modelu używać wewnętrznej klasy Meta . Nieco jaśniej * jest poinstruować użytkowników frameworka, aby napisali a 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"

zamiast „napisz a class _Meta, a potem napisz class Foo(models.Model)z Meta = _Meta”;

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

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Przy podejściu „klasy zagnieżdżonej” kod można odczytać jako zagnieżdżoną listę punktowaną , ale w przypadku metody „Odniesiona klasa wewnętrzna” należy przewinąć wstecz, aby zobaczyć definicję elementu _Metapodrzędnego (atrybuty).

  • Metoda „Klasa wewnętrzna z odniesieniem” może być bardziej czytelna, jeśli poziom zagnieżdżenia kodu rośnie lub wiersze są długie z innego powodu.

* Oczywiście kwestia gustu

3.3 Nieznacznie różne komunikaty o błędach

To nie jest wielka sprawa, ale tylko dla kompletności: podczas uzyskiwania dostępu do nieistniejącego atrybutu dla klasy wewnętrznej, widzimy nieco inne wyjątki. Kontynuując przykład podany w sekcji 2:

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

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

Dzieje się tak, ponieważ types klas wewnętrznych są

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

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

0

Użyłem klas wewnętrznych Pythona, aby stworzyć celowo błędne podklasy w ramach funkcji unittest (tj. def test_something(): ), aby zbliżyć się do 100% pokrycia testami (np. Testowanie bardzo rzadko wyzwalanych instrukcji logowania przez przesłonięcie niektórych metod).

Z perspektywy czasu jest podobny do odpowiedzi Eda https://stackoverflow.com/a/722036/1101109

Takie klasy wewnętrzne powinny wyjść poza zakres i być gotowe do wyrzucania elementów bezużytecznych po usunięciu wszystkich odwołań do nich. Na przykład weź następujący inner.pyplik:

class A(object):
    pass

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

W OSX Python 2.7.6 otrzymuję następujące ciekawe wyniki:

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

Wskazówka - nie kontynuuj i nie próbuj tego robić z modelami Django, które zdawały się zachowywać inne (buforowane?) Odniesienia do moich błędnych klas.

Ogólnie rzecz biorąc, nie polecałbym używania do tego celu klas wewnętrznych, chyba że naprawdę cenisz sobie to 100% pokrycie testu i nie możesz używać innych metod. Chociaż myślę, że miło jest mieć świadomość, że jeśli używasz __subclasses__(), to czasami może zostać zanieczyszczony przez klasy wewnętrzne. Tak czy inaczej, jeśli poszedłeś tak daleko, myślę, że w tym momencie jesteśmy dość głęboko w Pythonie, prywatnych wynikach dunders i wszystkim innym.


3
Czy nie chodzi o podklasy, a nie o klasy wewnętrzne? A
klaas

W powyższym przypadku Buggy dziedziczy po A. Więc podklasa to pokazuje. Zobacz także wbudowaną funkcję issubclass ()
klaas

Dzięki @klaas, myślę, że można by było jaśniej wyjaśnić, że używam po prostu .__subclasses__()do zrozumienia, jak klasy wewnętrzne współdziałają z modułem odśmiecania pamięci, gdy rzeczy wykraczają poza zakres w Pythonie. To wizualnie wydaje się dominować w poście, więc pierwsze 1-3 akapity zasługują na nieco większe rozwinięcie.
pzrq
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.