Меня смущают внутренние / вложенные классы Python. Есть ли что-то, чего нельзя добиться без них? Если да, то что это?
Ответы:
Цитируется по http://www.geekinterview.com/question_details/64739 :
Преимущества внутреннего класса:
- Логическая группировка классов : если класс полезен только для одного другого класса, то логично встроить его в этот класс и сохранить их вместе. Вложение таких «вспомогательных классов» делает их пакет более упрощенным.
- Повышенная инкапсуляция : рассмотрим два класса верхнего уровня A и B, где B требуется доступ к членам A, которые в противном случае были бы объявлены частными. Скрывая класс B внутри класса, члены AA могут быть объявлены закрытыми, и B может получить к ним доступ. Кроме того, сам B можно скрыть от внешнего мира.
- Более читаемый, поддерживаемый код : вложение небольших классов в классы верхнего уровня помещает код ближе к тому месту, где он используется.
Главное преимущество - организованность. Все, что можно сделать с помощью внутренних классов, можно сделать и без них.
DataLoader
класс, который может генерировать CacheMiss
исключение. Вложение исключения в основной класс DataLoader.CacheMiss
означает, что вы можете просто импортировать, DataLoader
но по-прежнему использовать исключение.
Есть ли что-то, чего нельзя добиться без них?
Нет. Они абсолютно эквивалентны определению класса обычно на верхнем уровне и последующему копированию ссылки на него во внешний класс.
Я не думаю, что есть какая-то особая причина, по которой вложенные классы «разрешены», кроме того, что нет особого смысла явно «запрещать» их.
Если вы ищете класс, который существует в жизненном цикле внешнего объекта / объекта-владельца и всегда имеет ссылку на экземпляр внешнего класса - внутренние классы, как это делает 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)» после определения класса.)
self
без какой-либо дополнительной работы (просто используйте другой идентификатор, где вы обычно помещаете внутренний self
; например innerself
), и вы сможете получить доступ к внешнему экземпляру через него.
WeakKeyDictionary
в этом примере фактически не позволяет использовать сборку мусора для ключей, поскольку значения строго ссылаются на соответствующие ключи через их owner
атрибут.
Есть кое-что, что вам нужно подумать, чтобы понять это. В большинстве языков определения классов являются директивами для компилятора. То есть класс создается до того, как программа запускается. В Python все операторы являются исполняемыми. Это означает, что это утверждение:
class foo(object):
pass
это оператор, который выполняется во время выполнения, как этот:
x = y + z
Это означает, что вы можете не только создавать классы внутри других классов, вы можете создавать классы где угодно. Рассмотрим этот код:
def foo():
class bar(object):
...
z = bar()
Таким образом, идея «внутреннего класса» на самом деле не является языковой конструкцией; это конструкция программиста. Guido имеет очень хорошее резюме , как это произошло здесь . Но по сути, основная идея состоит в том, что это упрощает грамматику языка.
Вложенность классов внутри классов:
Вложенные классы раздувают определение класса, что затрудняет понимание того, что происходит.
Вложенные классы могут создавать связи, которые затрудняют тестирование.
В Python вы можете поместить более одного класса в файл / модуль, в отличие от Java, поэтому класс по-прежнему остается близким к классу верхнего уровня и даже может иметь имя класса с префиксом "_", чтобы показать, что другие не должны быть используй это.
Место, где вложенные классы могут оказаться полезными, находится внутри функций.
def some_func(a, b, c):
class SomeClass(a):
def some_method(self):
return b
SomeClass.__doc__ = c
return SomeClass
Класс захватывает значения из функции, позволяя вам динамически создавать класс, подобный метапрограммированию шаблона в C ++.
Я понимаю аргументы против вложенных классов, но в некоторых случаях их можно использовать. Представьте, что я создаю класс двусвязного списка, и мне нужно создать класс узла для обслуживания узлов. У меня есть два варианта: создать класс Node внутри класса DoublyLinkedList или создать класс Node вне класса DoublyLinkedList. В этом случае я предпочитаю первый вариант, потому что класс Node имеет смысл только внутри класса DoublyLinkedList. Хотя нет никакого преимущества скрытия / инкапсуляции, есть преимущество группировки, заключающееся в возможности сказать, что класс Node является частью класса DoublyLinkedList.
Node
класс бесполезен для других типов классов связанных списков, которые вы также можете создать, и в этом случае он, вероятно, должен быть просто снаружи.
Node
находится в пространстве имен DoublyLinkedList
, и в этом есть логический смысл. Это является Pythonic: «Пространство имен один сигналит отличную идею - давайте больше тех!»
Есть ли что-то, чего нельзя добиться без них? Если да, то что это?
Есть кое-что, без чего не обойтись : наследование связанных классов .
Вот минималистичный пример со связанными классами 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
.
Конечно, это динамическое связывание можно воссоздать без внутреннего класса за счет утомительной и подверженной ошибкам инструментовки классов.
В основном я использую это для предотвращения распространения небольших модулей и предотвращения загрязнения пространства имен, когда отдельные модули не нужны. Если я расширяю существующий класс, но этот существующий класс должен ссылаться на другой подкласс, который всегда должен быть связан с ним. Например, у меня может быть 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: определение вложенного класса
(= «Вложенный класс»)
class MyOuter1:
class Inner:
def show(self, msg):
print(msg)
Способ 2: с внутренним классом на уровне модуля, присоединенным к внешнему классу
(= "Внутренний класс, на который имеется ссылка")
class _InnerClass:
def show(self, msg):
print(msg)
class MyOuter2:
Inner = _InnerClass
Подчеркивание используется, чтобы следовать PEP8: «внутренние интерфейсы (пакеты, модули, классы, функции, атрибуты или другие имена) должны - иметь префикс с одним ведущим подчеркиванием».
Ниже приведен фрагмент кода, демонстрирующий функциональное сходство «Вложенного класса» и «Ссылочного внутреннего класса»; Они будут вести себя таким же образом при проверке кода для типа экземпляра внутреннего класса. Излишне говорить, что они 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)
Различия между «вложенным классом» и «внутренним классом, на который имеется ссылка» перечислены ниже. Они не большие, но иногда хочется выбрать то или иное на их основе.
С «Вложенными классами» можно инкапсулировать код лучше, чем с «Внутренним классом, на который есть ссылка». Класс в пространстве имен модуля - это глобальная переменная. Цель вложенных классов - уменьшить беспорядок в модуле и поместить внутренний класс внутрь внешнего класса.
Пока никто * не использует from packagename import *
, небольшое количество переменных уровня модуля может быть приятным, например, при использовании IDE с автозавершением кода / intellisense.
* Верно?
Документация 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
чтобы увидеть его «дочерние элементы» (атрибуты).
Метод «Внутренний класс, на который имеется ссылка» может быть более читабельным, если уровень вложенности вашего кода растет или строки становятся длинными по какой-либо другой причине.
* Конечно, дело вкуса
Это не имеет большого значения, но просто для полноты: при доступе к несуществующему атрибуту для внутреннего класса мы видим несколько разные исключения. Продолжая пример, приведенный в разделе 2:
innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'
innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'
Это потому, что type
s внутренних классов
type(innercls1())
#mypackage.outer1.MyOuter1.Inner
type(innercls2())
#mypackage.outer2._InnerClass
Я использовал внутренние классы 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 и все такое.
.__subclasses__()
чтобы понять, как внутренние классы взаимодействуют со сборщиком мусора, когда что-то выходит за рамки в Python. Это визуально кажется доминирующим в посте, поэтому первые 1-3 абзаца заслуживают немного большего расширения.