Quel est le but des classes internes de python?

98

Les classes internes / imbriquées de Python me déroutent. Y a-t-il quelque chose qui ne peut être accompli sans eux? Si oui, quelle est cette chose?

Geo
la source

Réponses:

85

Extrait de http://www.geekinterview.com/question_details/64739 :

Avantages de la classe interne:

  • Regroupement logique des classes : si une classe n'est utile qu'à une seule autre classe, il est logique de l'intégrer dans cette classe et de garder les deux ensemble. L'imbrication de ces "classes d'assistance" rend leur package plus simple.
  • Encapsulation accrue : considérez deux classes de niveau supérieur A et B où B a besoin d'accéder aux membres de A qui seraient autrement déclarés privés. En cachant la classe B au sein de la classe AA, les membres peuvent être déclarés privés et B peut y accéder. De plus, B lui-même peut être caché du monde extérieur.
  • Code plus lisible et maintenable : l'imbrication de petites classes dans des classes de niveau supérieur rapproche le code de l'endroit où il est utilisé.

Le principal avantage est l'organisation. Tout ce qui peut être accompli avec des classes internes peut être accompli sans elles.

Aziz
la source
50
L'argument d'encapsulation ne s'applique bien sûr pas à Python.
bobince le
30
Le premier point ne s'applique pas non plus à Python. Vous pouvez définir autant de classes dans un fichier de module que vous le souhaitez, les gardant ainsi ensemble et l'organisation du package n'est pas affectée non plus. Le dernier point est très subjectif et je ne pense pas qu'il soit valable. En bref, je ne trouve aucun argument soutenant l'utilisation de classes internes en Python dans cette réponse.
Chris Arndt
17
Néanmoins, ce sont les raisons pour lesquelles les classes internes sont utilisées dans la programmation. Vous essayez simplement d'abattre une réponse concurrente. Cette réponse que ce mec a donnée ici est solide.
Inversus
16
@Inversus: Je ne suis pas d'accord. Ce n'est pas une réponse, c'est une citation étendue de la réponse de quelqu'un d'autre sur un autre langage (à savoir Java). Downvoté et j'espère que d'autres feront de même.
Kevin le
4
Je suis d'accord avec cette réponse et je ne suis pas d'accord avec les objections. Bien que les classes imbriquées ne soient pas les classes internes de Java, elles sont utiles. Le but d'une classe imbriquée est l'organisation. En fait, vous placez une classe sous l'espace de noms d'une autre. Quand cela a un sens logique de le faire, c'est Pythonic: "Les espaces de noms sont une excellente idée - faisons-en plus!". Par exemple, considérons une DataLoaderclasse qui peut lever une CacheMissexception. L'imbrication de l'exception sous la classe principale DataLoader.CacheMisssignifie que vous pouvez importer simplement DataLoadermais toujours utiliser l'exception.
cbarrick
50

Y a-t-il quelque chose qui ne peut être accompli sans eux?

Non. Ils sont absolument équivalents à définir la classe normalement au niveau supérieur, puis à copier une référence à celle-ci dans la classe externe.

Je ne pense pas qu'il y ait une raison particulière pour laquelle les classes imbriquées sont «autorisées», à part cela n'a aucun sens de les «interdire» explicitement non plus.

Si vous recherchez une classe qui existe dans le cycle de vie de l'objet externe / propriétaire et qui a toujours une référence à une instance de la classe externe - les classes internes comme Java le fait - alors les classes imbriquées de Python ne sont pas cette chose. Mais vous pouvez pirater quelque chose comme ça:

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

(Cela utilise des décorateurs de classe, qui sont nouveaux dans Python 2.6 et 3.0. Sinon, vous devrez dire «Inner = innerclass (Inner)» après la définition de la classe.)

3 tours
la source
5
Les cas d'utilisation qui appellent cela (c'est-à-dire les classes internes Java-esque, dont les instances ont une relation avec les instances de la classe externe) peuvent généralement être traités en Python en définissant la classe interne à l'intérieur des méthodes de la classe externe - ils verront le outer selfsans aucun travail supplémentaire requis (utilisez simplement un identifiant différent où vous mettriez généralement les intérieurs self; comme innerself), et pourrez accéder à l'instance externe via cela.
Evgeni Sergeev
L'utilisation de a WeakKeyDictionarydans cet exemple n'autorise pas réellement le ramassage des ordures des clés, car les valeurs référencent fortement leurs clés respectives via leur ownerattribut.
Kritzefitz
36

Vous devez comprendre quelque chose pour comprendre cela. Dans la plupart des langages, les définitions de classe sont des directives pour le compilateur. Autrement dit, la classe est créée avant l'exécution du programme. En python, toutes les instructions sont exécutables. Cela signifie que cette déclaration:

class foo(object):
    pass

est une instruction exécutée au moment de l'exécution comme celle-ci:

x = y + z

Cela signifie que non seulement vous pouvez créer des classes dans d'autres classes, mais vous pouvez également créer des classes où vous le souhaitez. Considérez ce code:

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

Ainsi, l'idée d'une «classe interne» n'est pas vraiment une construction de langage; c'est une construction de programmeur. Guido a un très bon résumé de la façon dont cela s'est produit ici . Mais essentiellement, l'idée de base est que cela simplifie la grammaire de la langue.

Jason Baker
la source
16

Imbrication de classes dans les classes:

  • Les classes imbriquées gonflent la définition de classe, ce qui rend plus difficile de voir ce qui se passe.

  • Les classes imbriquées peuvent créer un couplage qui rendrait les tests plus difficiles.

  • En Python, vous pouvez mettre plus d'une classe dans un fichier / module, contrairement à Java, de sorte que la classe reste toujours proche de la classe de niveau supérieur et pourrait même avoir le nom de classe préfixé avec un "_" pour aider à signifier que les autres ne devraient pas être En l'utilisant.

L'endroit où les classes imbriquées peuvent s'avérer utiles est dans les fonctions

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

La classe capture les valeurs de la fonction vous permettant de créer dynamiquement une classe comme la métaprogrammation de modèle en C ++

Ed.
la source
7

Je comprends les arguments contre les classes imbriquées, mais il y a lieu de les utiliser dans certaines occasions. Imaginez que je crée une classe de liste à double lien et que je dois créer une classe de nœuds pour maintenir les nœuds. J'ai deux choix, créer la classe Node dans la classe DoublyLinkedList, ou créer la classe Node en dehors de la classe DoublyLinkedList. Je préfère le premier choix dans ce cas, car la classe Node n'a de sens qu'à l'intérieur de la classe DoublyLinkedList. Bien qu'il n'y ait aucun avantage de masquage / encapsulation, il y a un avantage de regroupement à pouvoir dire que la classe Node fait partie de la classe DoublyLinkedList.

utilisateur411279
la source
5
Cela est vrai en supposant que la même Nodeclasse n'est pas utile pour d'autres types de classes de liste chaînée que vous pourriez également créer, auquel cas elle devrait probablement être juste à l'extérieur.
Acumenus
Une autre façon de le dire: Nodeest sous l'espace de noms de DoublyLinkedList, et il est logique de l'être. C'est Pythonic: « Les espaces de noms sont une klaxonnant grande idée - nous allons faire plus de ceux - là! »
cbarrick
@cbarrick: Faire "plus de ceux-ci" ne dit rien sur leur imbrication.
Ethan Furman
3

Y a-t-il quelque chose qui ne peut être accompli sans eux? Si oui, quelle est cette chose?

Il y a quelque chose qui ne peut pas être facilement fait sans : l' héritage des classes liées .

Voici un exemple minimaliste avec les classes associées Aet 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

Ce code conduit à un comportement assez raisonnable et prévisible:

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

S'il Bs'agissait d'une classe de premier niveau, vous ne pourriez pas écrire self.B()dans la méthode make_Bmais simplement écrire B(), et ainsi perdre la liaison dynamique aux classes adéquates.

Notez que dans cette construction, vous ne devez jamais faire référence à une classe Adans le corps de la classe B. C'est la motivation pour introduire l' parentattribut en classe B.

Bien entendu, cette liaison dynamique peut être recréée sans classe interne au prix d'une instrumentation fastidieuse et sujette aux erreurs des classes.

Pascal Sotin
la source
1

Le cas d'utilisation principal pour lequel j'utilise ceci est la prévention de la prolifération de petits modules et la prévention de la pollution de l'espace de noms lorsque des modules séparés ne sont pas nécessaires. Si j'étends une classe existante, mais que cette classe existante doit référencer une autre sous-classe qui doit toujours lui être couplée. Par exemple, je peux avoir un utils.pymodule qui contient de nombreuses classes d'assistance, qui ne sont pas nécessairement couplées entre elles, mais je veux renforcer le couplage pour certaines de ces classes d'assistance. Par exemple, lorsque j'implémente 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) 

Alors nous pouvons:

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

Bien sûr, nous aurions pu éviter json.JSONEnocdercomplètement d' hériter et simplement remplacer 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}'

Mais parfois, juste pour la convention, vous voulez utilsêtre composé de classes pour l'extensibilité.

Voici un autre cas d'utilisation: je veux une fabrique pour les mutables dans mon OuterClass sans avoir à appeler 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]

Je préfère ce modèle au @staticmethoddécorateur que vous utiliseriez autrement pour une fonction d'usine.

Cowbert
la source
1

1. Deux méthodes fonctionnellement équivalentes

Les deux méthodes illustrées précédemment sont fonctionnellement identiques. Cependant, il existe des différences subtiles et il y a des situations où vous voudriez choisir l'un plutôt que l'autre.

Méthode 1: définition de classe imbriquée
(= "classe imbriquée")

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

Méthode 2: Avec la classe interne de niveau module attachée à la classe externe
(= "classe interne référencée")

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

class MyOuter2:
    Inner = _InnerClass

Le soulignement est utilisé pour suivre PEP8 "Les interfaces internes (packages, modules, classes, fonctions, attributs ou autres noms) doivent - être précédées d'un seul trait de soulignement."

2. Similitudes

L'extrait de code ci-dessous illustre les similitudes fonctionnelles de la «classe imbriquée» et de la «classe interne référencée»; Ils se comporteraient de la même manière dans le code de vérification du type d'une instance de classe interne. Inutile de dire que le m.inner.anymethod()se comporterait de la même manière avec m1etm2

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. Différences

Les différences de «Classe imbriquée» et «Classe interne référencée» sont répertoriées ci-dessous. Ils ne sont pas grands, mais parfois vous aimeriez choisir l'un ou l'autre en fonction de ceux-ci.

3.1 Encapsulation de code

Avec "Classes imbriquées", il est possible d'encapsuler le code mieux qu'avec "Classe interne référencée". Une classe dans l'espace de noms du module est une variable globale . Le but des classes imbriquées est de réduire l'encombrement dans le module et de placer la classe interne dans la classe externe.

Bien que personne ne l'utilise from packagename import *, une faible quantité de variables au niveau du module peut être intéressante, par exemple lors de l'utilisation d'un IDE avec complétion de code / intellisense.

* N'est-ce pas?

3.2 Lisibilité du code

La documentation de Django indique d'utiliser la classe interne Meta pour les métadonnées du modèle. Il est un peu plus clair * de demander aux utilisateurs du framework d'écrire un class Foo(models.Model)avec inner class Meta;

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

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

au lieu de "écrire un class _Meta, puis écrire un class Foo(models.Model)avec Meta = _Meta";

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

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • Avec l'approche "Classe imbriquée", le code peut être lu dans une liste à puces imbriquée , mais avec la méthode "Classe interne référencée", il faut faire défiler vers le haut pour voir la définition de _Metapour voir ses "éléments enfants" (attributs).

  • La méthode "Classe interne référencée" peut être plus lisible si votre niveau d'imbrication de code augmente ou si les lignes sont longues pour une autre raison.

* Bien sûr, une question de goût

3.3 Messages d'erreur légèrement différents

Ce n'est pas un gros problème, mais juste pour être complet: lors de l'accès à un attribut inexistant pour la classe interne, nous voyons des exceptions légèrement différentes. Poursuivant l'exemple donné dans la section 2:

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

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

C'est parce que les types des classes internes sont

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

type(innercls2())
#mypackage.outer2._InnerClass
np8
la source
0

J'ai utilisé les classes internes de Python pour créer des sous-classes délibérément boguées dans les fonctions unittest (c'est-à-dire à l'intérieur def test_something():) afin de me rapprocher de la couverture de test à 100% (par exemple, tester très rarement les déclarations de journalisation déclenchées en remplaçant certaines méthodes).

Rétrospectivement, c'est similaire à la réponse d'Ed https://stackoverflow.com/a/722036/1101109

Ces classes internes doivent sortir du champ d'application et être prêtes pour le garbage collection une fois que toutes les références à celles-ci ont été supprimées. Par exemple, prenez le inner.pyfichier suivant :

class A(object):
    pass

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

J'obtiens les résultats curieux suivants sous 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__()
[]

Astuce - N'essayez pas de faire cela avec les modèles Django, qui semblaient garder d'autres références (en cache?) À mes classes boguées.

Donc, en général, je ne recommanderais pas d'utiliser des classes internes pour ce genre d'objet, à moins que vous ne valorisiez vraiment cette couverture de test à 100% et que vous ne puissiez pas utiliser d'autres méthodes. Même si je pense qu'il est agréable de savoir que si vous utilisez le __subclasses__(), il peut parfois être pollué par les classes internes. Quoi qu'il en soit, si vous avez suivi jusqu'ici, je pense que nous sommes assez profondément dans Python à ce stade, les dunderscores privés et tout.

pzrq
la source
3
N'est-ce pas une question de sous-classes et pas de classes internes? A
klaas
Dans la casse ci-dessus, Buggy hérite de A. Donc, le sous- type le montre. Voir aussi la fonction intégrée issubclass ()
klaas
Merci @klaas, je pense qu'il pourrait être plus clair que je n'utilise que .__subclasses__()pour comprendre comment les classes internes interagissent avec le ramasse-miettes lorsque les choses sortent du cadre de Python. Cela semble visuellement dominer le message, donc les 1 à 3 premiers paragraphes méritent un peu plus d'élargissement.
pzrq