¿Cuál es el propósito de las clases internas de Python?


98

Las clases internas / anidadas de Python me confunden. ¿Hay algo que no se pueda lograr sin ellos? Si es así, ¿qué es esa cosa?

Respuestas:


85

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

Ventajas de la clase interna:

  • Agrupación lógica de clases : si una clase es útil solo para otra clase, entonces es lógico incrustarla en esa clase y mantener las dos juntas. Anidar tales "clases auxiliares" hace que su paquete sea más ágil.
  • Mayor encapsulación : considere dos clases de nivel superior A y B donde B necesita acceso a miembros de A que de otro modo se declararían privados. Al ocultar la clase B dentro de la clase, los miembros de AA pueden declararse privados y B puede acceder a ellos. Además, el propio B puede ocultarse del mundo exterior.
  • Código más legible y fácil de mantener : anidar clases pequeñas dentro de clases de nivel superior coloca el código más cerca de donde se usa.

La principal ventaja es la organización. Todo lo que se puede lograr con las clases internas se puede lograr sin ellas.


50
El argumento de encapsulación, por supuesto, no se aplica a Python.
bobince

30
El primer punto tampoco se aplica a Python. Puede definir tantas clases en un archivo de módulo como desee, manteniéndolas juntas y la organización del paquete tampoco se verá afectada. El último punto es muy subjetivo y no creo que sea válido. En resumen, no encuentro ningún argumento que respalde el uso de clases internas en Python en esta respuesta.
Chris Arndt

17
Sin embargo, estas SON las razones por las que las clases internas se utilizan en programación. Solo estás tratando de derribar una respuesta competitiva. Esta respuesta que dio este tipo aquí es sólida.
Inversus

16
@Inversus: No estoy de acuerdo. Esta no es una respuesta, es una cita ampliada de la respuesta de otra persona sobre un idioma diferente (es decir, Java). Voté en contra y espero que otros hagan lo mismo.
Kevin

4
Estoy de acuerdo con esta respuesta y en desacuerdo con las objeciones. Si bien las clases anidadas no son clases internas de Java, son útiles. El propósito de una clase anidada es la organización. Efectivamente, está poniendo una clase en el espacio de nombres de otra. Cuando tiene sentido lógico hacerlo, esto es Pythonic: "Los espacios de nombres son una gran idea para tocar la bocina, ¡hagamos más de esas!". Por ejemplo, considere una DataLoaderclase que puede lanzar una CacheMissexcepción. Anidar la excepción bajo la clase principal DataLoader.CacheMisssignifica que puede importar solo DataLoaderpero aún usar la excepción.
cbarrick

50

¿Hay algo que no se pueda lograr sin ellos?

No. Son absolutamente equivalentes a definir la clase normalmente en el nivel superior y luego copiar una referencia a ella en la clase externa.

No creo que haya ninguna razón especial por la que las clases anidadas estén 'permitidas', aparte de que tampoco tiene sentido en particular 'rechazarlas' explícitamente.

Si está buscando una clase que existe dentro del ciclo de vida del objeto externo / propietario, y siempre tiene una referencia a una instancia de la clase externa, clases internas como las hace Java, entonces las clases anidadas de Python no son esa cosa. Pero puedes hackear algo como eso:

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

(Esto usa decoradores de clase, que son nuevos en Python 2.6 y 3.0. De lo contrario, tendría que decir "Inner = innerclass (Inner)" después de la definición de la clase).


5
Los casos de uso que requieren eso (es decir, clases internas al estilo de Java, cuyas instancias tienen una relación con las instancias de la clase externa) generalmente se pueden abordar en Python definiendo la clase interna dentro de los métodos de la clase externa; verán el externos selfsin que se requiera ningún trabajo adicional (solo use un identificador diferente donde normalmente colocaría los internos self; me gusta innerself), y podrá acceder a la instancia externa a través de eso.
Evgeni Sergeev

El uso de a WeakKeyDictionaryen este ejemplo no permite que las claves sean recolectadas como basura, porque los valores hacen referencia fuertemente a sus respectivas claves a través de su owneratributo.
Kritzefitz

36

Hay algo en lo que debes pensar para poder entender esto. En la mayoría de los lenguajes, las definiciones de clases son directivas para el compilador. Es decir, la clase se crea antes de que se ejecute el programa. En Python, todas las declaraciones son ejecutables. Eso significa que esta declaración:

class foo(object):
    pass

es una declaración que se ejecuta en tiempo de ejecución como esta:

x = y + z

Esto significa que no solo puede crear clases dentro de otras clases, puede crear clases en cualquier lugar que desee. Considere este código:

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

Por tanto, la idea de una "clase interna" no es realmente una construcción del lenguaje; es una construcción de programador. Guido tiene un muy buen resumen de cómo sucedió aquí . Pero esencialmente, la idea básica es que esto simplifica la gramática del idioma.


16

Clases anidadas dentro de clases:

  • Las clases anidadas aumentan la definición de la clase, lo que dificulta ver qué está pasando.

  • Las clases anidadas pueden crear un acoplamiento que dificultaría las pruebas.

  • En Python, puede poner más de una clase en un archivo / módulo, a diferencia de Java, por lo que la clase aún permanece cerca de la clase de nivel superior e incluso podría tener el nombre de la clase con el prefijo "_" para ayudar a significar que otros no deberían ser usándolo.

El lugar donde las clases anidadas pueden resultar útiles es dentro de las funciones

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

La clase captura los valores de la función permitiéndole crear dinámicamente una clase como plantilla de metaprogramación en C ++


7

Entiendo los argumentos en contra de las clases anidadas, pero hay razones para usarlas en algunas ocasiones. Imagina que estoy creando una clase de lista doblemente enlazada y necesito crear una clase de nodo para mantener los nodos. Tengo dos opciones, crear la clase Node dentro de la clase DoublyLinkedList o crear la clase Node fuera de la clase DoublyLinkedList. Prefiero la primera opción en este caso, porque la clase Node solo es significativa dentro de la clase DoublyLinkedList. Si bien no hay ningún beneficio de ocultación / encapsulación, existe un beneficio de agrupación al poder decir que la clase Node es parte de la clase DoublyLinkedList.


5
Esto es cierto asumiendo que la misma Nodeclase no es útil para otros tipos de clases de listas vinculadas que también podría crear, en cuyo caso probablemente debería estar afuera.
Acumenus

Otra forma de decirlo: Nodeestá bajo el espacio de nombres de DoublyLinkedList, y tiene sentido lógico que sea así. Esto es Pythonic: "Los espacios de nombres son una gran idea para tocar la bocina. ¡Hagamos más de esas!"
cbarrick

@cbarrick: Hacer "más de esos" no dice nada sobre anidarlos.
Ethan Furman

3

¿Hay algo que no se pueda lograr sin ellos? Si es así, ¿qué es esa cosa?

Hay algo que no se puede hacer fácilmente sin : la herencia de clases relacionadas .

Aquí hay un ejemplo minimalista con las clases relacionadas Ay 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

Este código conduce a un comportamiento bastante razonable y predecible:

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

Si Bfuera una clase de nivel superior, no podría escribir self.B()en el método, make_Bsino que simplemente escribiría B()y, por lo tanto, perdería la vinculación dinámica a las clases adecuadas.

Tenga en cuenta que en esta construcción, nunca debe hacer referencia a la clase Aen el cuerpo de la clase B. Esta es la motivación para introducir el parentatributo en clase B.

Por supuesto, este enlace dinámico se puede recrear sin una clase interna a costa de una instrumentación tediosa y propensa a errores de las clases.


1

El caso de uso principal para el que uso esto es para evitar la proliferación de módulos pequeños y para evitar la contaminación del espacio de nombres cuando no se necesitan módulos separados. Si estoy ampliando una clase existente, pero esa clase existente debe hacer referencia a otra subclase que siempre debería estar acoplada a ella. Por ejemplo, puedo tener un utils.pymódulo que tiene muchas clases auxiliares, que no están necesariamente acopladas, pero quiero reforzar el acoplamiento para algunas de esas clases auxiliares. Por ejemplo, cuando 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) 

Entonces podemos:

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

Por supuesto, podríamos haber evitado heredar por json.JSONEnocdercompleto y simplemente anular 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}'

Pero a veces, solo por convención, desea utilsestar compuesto de clases para la extensibilidad.

Aquí hay otro caso de uso: quiero una fábrica de mutables en mi OuterClass sin tener que invocar 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]

Prefiero este patrón sobre el @staticmethoddecorador que de otra manera usaría para una función de fábrica.


1

1. Dos formas funcionalmente equivalentes

Las dos formas mostradas anteriormente son funcionalmente idénticas. Sin embargo, existen algunas diferencias sutiles y hay situaciones en las que le gustaría elegir una sobre otra.

Forma 1: definición de clase anidada
(= "clase anidada")

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

Modo 2: Con clase interna de nivel de módulo adjunta a clase externa
(= "Clase interna referenciada")

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

class MyOuter2:
    Inner = _InnerClass

El subrayado se utiliza para seguir PEP8 "las interfaces internas (paquetes, módulos, clases, funciones, atributos u otros nombres) deben ir precedidas de un único subrayado inicial ".

2. Similitudes

El siguiente fragmento de código demuestra las similitudes funcionales de la "clase anidada" frente a la "clase interna referenciada"; Se comportarían de la misma manera en la verificación de código para el tipo de instancia de clase interna. No hace falta decir que m.inner.anymethod()se comportarían de manera similar con m1ym2

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

Las diferencias de "Clase anidada" y "Clase interna referenciada" se enumeran a continuación. No son grandes, pero a veces le gustaría elegir uno u otro en función de estos.

3.1 Encapsulación de código

Con "Clases anidadas" es posible encapsular código mejor que con "Clase interna referenciada". Una clase en el espacio de nombres del módulo es una variable global . El propósito de las clases anidadas es reducir el desorden en el módulo y colocar la clase interna dentro de la clase externa.

Si bien nadie * está usando from packagename import *, una pequeña cantidad de variables de nivel de módulo puede ser agradable, por ejemplo, cuando se usa un IDE con finalización de código / intellisense.

* ¿Verdad?

3.2 Legibilidad del código

La documentación de Django indica el uso de la clase Meta interna para los metadatos del modelo. Es un poco más claro * instruir a los usuarios del framework para que escriban un 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"

en lugar de "escribe a class _Meta, luego escribe 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 el enfoque "Clase anidada", el código puede leerse como una lista de viñetas anidadas , pero con el método "Clase interna referenciada" uno tiene que desplazarse hacia arriba para ver la definición de _Metapara ver sus "elementos secundarios" (atributos).

  • El método "Clase interna referenciada" puede ser más legible si su nivel de anidación de código aumenta o las filas son largas por alguna otra razón.

* Por supuesto, es cuestión de gustos

3.3 Mensajes de error ligeramente diferentes

Esto no es un gran problema, sino solo para completar: cuando accedemos a un atributo inexistente para la clase interna, vemos excepciones ligeramente diferentes. Continuando con el ejemplo dado en la Sección 2:

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

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

Esto se debe a que las types de las clases internas son

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

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

0

He usado las clases internas de Python para crear deliberadamente subclases con errores dentro de las funciones de unittest (es decir, adentro def test_something():) con el fin de acercarme al 100% de la cobertura de prueba (por ejemplo, probar muy raramente declaraciones de registro activadas anulando algunos métodos).

En retrospectiva, es similar a la respuesta de Ed https://stackoverflow.com/a/722036/1101109

Dichas clases internas deberían salir del alcance y estar listas para la recolección de basura una vez que se hayan eliminado todas las referencias a ellas. Por ejemplo, tome el siguiente inner.pyarchivo:

class A(object):
    pass

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

Obtengo los siguientes resultados curiosos en 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__()
[]

Sugerencia: no intente hacer esto con los modelos de Django, que parecían mantener otras referencias (¿en caché?) A mis clases con errores.

Entonces, en general, no recomendaría usar clases internas para este tipo de propósito, a menos que realmente valore esa cobertura de prueba del 100% y no pueda usar otros métodos. Aunque creo que es bueno saber que si usa el __subclasses__(), a veces puede contaminarse por clases internas. De cualquier manera, si has seguido hasta aquí, creo que estamos bastante metidos en Python en este punto, con puntuaciones privadas y todo.


3
¿No se trata de subclases y no de clases internas? A
klaas

En el caso anterior, Buggy hereda de A. Así que la subclase muestra eso. También vea la función incorporada issubclass ()
klaas

Gracias @klaas, creo que podría quedar más claro que solo estoy usando .__subclasses__()para comprender cómo las clases internas interactúan con el recolector de basura cuando las cosas se salen del alcance en Python. Eso parece dominar visualmente la publicación, por lo que los primeros 1-3 párrafos merecen un poco más de expansión.
pzrq
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.