Какова цель внутренних классов Python? - PullRequest
83 голосов
/ 06 апреля 2009

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

Ответы [ 7 ]

74 голосов
/ 06 апреля 2009

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

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

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

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

47 голосов
/ 06 апреля 2009

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

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

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

Если вы ищете класс, который существует в жизненном цикле внешнего объекта / владельца и всегда имеет ссылку на экземпляр внешнего класса - внутренние классы, как это делает 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)» после определения класса.)

25 голосов
/ 06 апреля 2009

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

class foo(object):
    pass

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

x = y + z

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

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

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

13 голосов
/ 06 апреля 2009

Вложенность классов в классах:

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

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

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

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

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

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

4 голосов
/ 30 апреля 2012

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

0 голосов
/ 12 сентября 2017

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

Но иногда просто для соглашения, вы хотите, чтобы 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 декоратором, который вы иначе использовали бы для заводской функции.

0 голосов
/ 18 февраля 2015

Я использовал внутренние классы 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 на данный момент, частные оценки и все.

...