Какова цель внутренних классов Python?


98

Меня смущают внутренние / вложенные классы Python. Есть ли что-то, чего нельзя добиться без них? Если да, то что это?

Ответы:


86

Цитируется по http://www.geekinterview.com/question_details/64739 :

Преимущества внутреннего класса:

  • Логическая группировка классов : если класс полезен только для одного другого класса, то логично встроить его в этот класс и сохранить их вместе. Вложение таких «вспомогательных классов» делает их пакет более упрощенным.
  • Повышенная инкапсуляция : рассмотрим два класса верхнего уровня A и B, где B требуется доступ к членам A, которые в противном случае были бы объявлены частными. Скрывая класс B внутри класса, члены AA могут быть объявлены закрытыми, и B может получить к ним доступ. Кроме того, сам B можно скрыть от внешнего мира.
  • Более читаемый, поддерживаемый код : вложение небольших классов в классы верхнего уровня помещает код ближе к тому месту, где он используется.

Главное преимущество - организованность. Все, что можно сделать с помощью внутренних классов, можно сделать и без них.


50
Аргумент инкапсуляции, конечно, не относится к Python.
bobince

31
Первый пункт также не относится к Python. Вы можете определить столько классов в одном файле модуля, сколько захотите, таким образом, сохраняя их вместе, и это не влияет на организацию пакета. Последний пункт очень субъективен, и я не считаю его верным. Короче говоря, в этом ответе я не нахожу аргументов в пользу использования внутренних классов в Python.
Крис Арндт

17
Тем не менее, это причины, по которым внутренние классы используются в программировании. Вы просто пытаетесь опровергнуть конкурирующий ответ. Этот чувак дал твердый ответ.
Inversus

16
@Inversus: Я не согласен. Это не ответ, это расширенная цитата из чьего-то ответа о другом языке (а именно Java). Проголосовали против, и я надеюсь, что другие сделают то же самое.
Кевин,

5
Я согласен с этим ответом и не согласен с возражениями. Хотя вложенные классы не являются внутренними классами Java, они полезны. Назначение вложенного класса - организация. Фактически, вы помещаете один класс в пространство имен другого. Когда это имеет логический смысл делать так, то это является Pythonic: «Пространство имен один сигналит отличную идею - давайте больше тех!». Например, рассмотрим DataLoaderкласс, который может генерировать CacheMissисключение. Вложение исключения в основной класс DataLoader.CacheMissозначает, что вы можете просто импортировать, DataLoaderно по-прежнему использовать исключение.
cbarrick 01

50

Есть ли что-то, чего нельзя добиться без них?

Нет. Они абсолютно эквивалентны определению класса обычно на верхнем уровне и последующему копированию ссылки на него во внешний класс.

Я не думаю, что есть какая-то особая причина, по которой вложенные классы «разрешены», кроме того, что нет особого смысла явно «запрещать» их.

Если вы ищете класс, который существует в жизненном цикле внешнего объекта / объекта-владельца и всегда имеет ссылку на экземпляр внешнего класса - внутренние классы, как это делает Java, - то вложенные классы Python - не то. Но можно взломать что-то вроде этого:

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

(Здесь используются декораторы классов, которые появились в Python 2.6 и 3.0. В противном случае вам пришлось бы сказать «Inner = innerclass (Inner)» после определения класса.)


5
Потребительная случаи , которые требуют , что (то есть Java-эск внутренние классы, чьи экземпляры имеют отношения с экземплярами внешнего класса) обычно могут быть решены в Python, определяя внутренний класс внутри методов внешнего класса - они будут видеть external selfбез какой-либо дополнительной работы (просто используйте другой идентификатор, где вы обычно помещаете внутренний self; например innerself), и вы сможете получить доступ к внешнему экземпляру через него.
Евгений Сергеев

Использование a WeakKeyDictionaryв этом примере фактически не позволяет использовать сборку мусора для ключей, поскольку значения строго ссылаются на соответствующие ключи через их ownerатрибут.
Kritzefitz

36

Есть кое-что, что вам нужно подумать, чтобы понять это. В большинстве языков определения классов являются директивами для компилятора. То есть класс создается до того, как программа запускается. В Python все операторы являются исполняемыми. Это означает, что это утверждение:

class foo(object):
    pass

это оператор, который выполняется во время выполнения, как этот:

x = y + z

Это означает, что вы можете не только создавать классы внутри других классов, вы можете создавать классы где угодно. Рассмотрим этот код:

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

Таким образом, идея «внутреннего класса» на самом деле не является языковой конструкцией; это конструкция программиста. Guido имеет очень хорошее резюме , как это произошло здесь . Но по сути, основная идея состоит в том, что это упрощает грамматику языка.


16

Вложенность классов внутри классов:

  • Вложенные классы раздувают определение класса, что затрудняет понимание того, что происходит.

  • Вложенные классы могут создавать связи, которые затрудняют тестирование.

  • В Python вы можете поместить более одного класса в файл / модуль, в отличие от Java, поэтому класс по-прежнему остается близким к классу верхнего уровня и даже может иметь имя класса с префиксом "_", чтобы показать, что другие не должны быть используй это.

Место, где вложенные классы могут оказаться полезными, находится внутри функций.

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

Класс захватывает значения из функции, позволяя вам динамически создавать класс, подобный метапрограммированию шаблона в C ++.


7

Я понимаю аргументы против вложенных классов, но в некоторых случаях их можно использовать. Представьте, что я создаю класс двусвязного списка, и мне нужно создать класс узла для обслуживания узлов. У меня есть два варианта: создать класс Node внутри класса DoublyLinkedList или создать класс Node вне класса DoublyLinkedList. В этом случае я предпочитаю первый вариант, потому что класс Node имеет смысл только внутри класса DoublyLinkedList. Хотя нет никакого преимущества скрытия / инкапсуляции, есть преимущество группировки, заключающееся в возможности сказать, что класс Node является частью класса DoublyLinkedList.


5
Это верно, если один и тот же Nodeкласс бесполезен для других типов классов связанных списков, которые вы также можете создать, и в этом случае он, вероятно, должен быть просто снаружи.
Acumenus

Другими словами: Nodeнаходится в пространстве имен DoublyLinkedList, и в этом есть логический смысл. Это является Pythonic: «Пространство имен один сигналит отличную идею - давайте больше тех!»
cbarrick 01

@cbarrick: Выполнение «большего количества таких» ничего не говорит о их вложении.
Итан Фурман

4

Есть ли что-то, чего нельзя добиться без них? Если да, то что это?

Есть кое-что, без чего не обойтись : наследование связанных классов .

Вот минималистичный пример со связанными классами Aи 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

Этот код приводит к вполне разумному и предсказуемому поведению:

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

Если бы Bэто был класс верхнего уровня, вы не могли бы писать self.B()в методе, make_Bа просто записали бы B()и, таким образом, потеряли бы динамическую привязку к адекватным классам.

Обратите внимание, что в этой конструкции вы никогда не должны ссылаться на класс Aв теле класса B. Это мотивация для введения parentатрибута в класс B.

Конечно, это динамическое связывание можно воссоздать без внутреннего класса за счет утомительной и подверженной ошибкам инструментовки классов.


1

В основном я использую это для предотвращения распространения небольших модулей и предотвращения загрязнения пространства имен, когда отдельные модули не нужны. Если я расширяю существующий класс, но этот существующий класс должен ссылаться на другой подкласс, который всегда должен быть связан с ним. Например, у меня может быть utils.pyмодуль, в котором есть много вспомогательных классов, которые не обязательно связаны вместе, но я хочу усилить связь для некоторых из этих вспомогательных классов. Например, когда я реализую 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) 

Тогда мы сможем:

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

Конечно, мы могли бы избегали наследования json.JSONEnocderвообще и просто переопределить по умолчанию ():

:

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

Но иногда просто по соглашению вы хотите, utilsчтобы он состоял из классов для расширяемости.

Вот еще один вариант использования: мне нужна фабрика изменяемых в моем OuterClass без необходимости вызывать 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]

Я предпочитаю этот шаблон @staticmethodдекоратору, который вы в противном случае использовали бы для фабричной функции.


1

1. Два функционально эквивалентных способа

Два показанных выше способа функционально идентичны. Однако есть некоторые тонкие различия, и бывают ситуации, когда вы хотите выбрать одно из них.

Способ 1: определение вложенного класса
(= «Вложенный класс»)

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

Способ 2: с внутренним классом на уровне модуля, присоединенным к внешнему классу
(= "Внутренний класс, на который имеется ссылка")

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

class MyOuter2:
    Inner = _InnerClass

Подчеркивание используется, чтобы следовать PEP8: «внутренние интерфейсы (пакеты, модули, классы, функции, атрибуты или другие имена) должны - иметь префикс с одним ведущим подчеркиванием».

2. Сходства

Ниже приведен фрагмент кода, демонстрирующий функциональное сходство «Вложенного класса» и «Ссылочного внутреннего класса»; Они будут вести себя таким же образом при проверке кода для типа экземпляра внутреннего класса. Излишне говорить, что они m.inner.anymethod()будут вести себя аналогичным образом с m1иm2

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. Различия

Различия между «вложенным классом» и «внутренним классом, на который имеется ссылка» перечислены ниже. Они не большие, но иногда хочется выбрать то или иное на их основе.

3.1 Инкапсуляция кода

С «Вложенными классами» можно инкапсулировать код лучше, чем с «Внутренним классом, на который есть ссылка». Класс в пространстве имен модуля - это глобальная переменная. Цель вложенных классов - уменьшить беспорядок в модуле и поместить внутренний класс внутрь внешнего класса.

Пока никто * не использует from packagename import *, небольшое количество переменных уровня модуля может быть приятным, например, при использовании IDE с автозавершением кода / intellisense.

* Верно?

3.2 Читаемость кода

Документация Django инструктирует использовать внутренний класс Meta для метаданных модели. Немного понятнее * указать пользователям фреймворка писать с class Foo(models.Model)помощью inner class Meta;

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

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

вместо "напишите a class _Meta, затем напишите a class Foo(models.Model)с Meta = _Meta";

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

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • При использовании подхода «Вложенный класс» код может быть прочитан как вложенный список маркеров , но с помощью метода «Внутренний класс, на который имеется ссылка» нужно прокрутить назад, чтобы увидеть определение, _Metaчтобы увидеть его «дочерние элементы» (атрибуты).

  • Метод «Внутренний класс, на который имеется ссылка» может быть более читабельным, если уровень вложенности вашего кода растет или строки становятся длинными по какой-либо другой причине.

* Конечно, дело вкуса

3.3 Несколько иные сообщения об ошибках

Это не имеет большого значения, но просто для полноты: при доступе к несуществующему атрибуту для внутреннего класса мы видим несколько разные исключения. Продолжая пример, приведенный в разделе 2:

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

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

Это потому, что types внутренних классов

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

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

0

Я использовал внутренние классы Python для создания подклассов с намеренными ошибками в функциях unittest (т.е. внутри def test_something():), чтобы приблизиться к 100% тестового покрытия (например, тестирование очень редко запускаемых операторов регистрации путем переопределения некоторых методов).

Оглядываясь назад, это похоже на ответ Эда https://stackoverflow.com/a/722036/1101109

Такие внутренние классы должны выходить за пределы области видимости и быть готовыми к сборке мусора после удаления всех ссылок на них. Например, возьмите следующий inner.pyфайл:

class A(object):
    pass

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

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

Подсказка - не пытайтесь делать это с моделями Django, которые, похоже, хранят другие (кэшированные?) Ссылки на мои ошибочные классы.

В общем, я бы не рекомендовал использовать внутренние классы для таких целей, если вы действительно не цените это 100% тестовое покрытие и не можете использовать другие методы. Хотя я думаю, что приятно осознавать, что если вы используете __subclasses__(), то он иногда может загрязняться внутренними классами. В любом случае, если вы зашли так далеко, я думаю, что на данный момент мы довольно глубоко погрузились в Python, частные dunderscores и все такое.


3
Разве это не подклассы, а не внутренние классы ?? A
klaas

В приведенном выше примере Багги наследуется от A. Итак, подкласс показывает это. Также см. Встроенную функцию issubclass ()
klaas

Спасибо @klaas, я думаю, можно было бы прояснить, что я просто использую, .__subclasses__()чтобы понять, как внутренние классы взаимодействуют со сборщиком мусора, когда что-то выходит за рамки в Python. Это визуально кажется доминирующим в посте, поэтому первые 1-3 абзаца заслуживают немного большего расширения.
pzrq
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.