Відповіді:
Цитується з http://www.geekinterview.com/question_details/64739 :
Переваги внутрішнього класу:
- Логічне групування класів : Якщо клас корисний лише для одного іншого класу, то логічно вбудувати його в цей клас і тримати ці два разом. Вкладання таких "допоміжних класів" робить їх пакет більш впорядкованим.
- Посилена інкапсуляція : розглянемо два класи вищого рівня A і B, де B потребує доступу до членів A, які в іншому випадку були б оголошені приватними. Приховуючи клас B у класі AA, члени можуть бути оголошені приватними, а 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()
Таким чином, ідея "внутрішнього класу" насправді не є мовною конструкцією; це конструкція програміста. Гвідо має дуже хороший підсумок того, як це сталося тут . Але по суті, основна ідея полягає в тому, що це спрощує граматику мови.
Класи вкладеності в класи:
Вкладені класи роздувають визначення класу, ускладнюючи розуміння того, що відбувається.
Вкладені класи можуть створити зв'язок, що ускладнить тестування.
У 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
взагалі уникнути успадкування і просто перевизначити 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
декоратор, який ви використовували б для заводських функцій.
Два способи, показані раніше, функціонально ідентичні. Однак існують деякі тонкі відмінності, і бувають ситуації, коли ви хотіли б вибрати один перед іншим.
Спосіб 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 для метаданих моделі. Дещо зрозуміліше * доручити користувачам фреймворку писати a class Foo(models.Model)
з внутрішнім class Meta
;
class Ox(models.Model):
horn_length = models.IntegerField()
class Meta:
ordering = ["horn_length"]
verbose_name_plural = "oxen"
замість того , щоб «написати class _Meta
, а потім написати 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 на даний момент, приватні дундери і все.
.__subclasses__()
щоб зрозуміти, як внутрішні класи взаємодіють із збирачем сміття, коли речі виходять за рамки Python. Здається, це візуально домінує на посаді, тому перші 1–3 абзаци заслуговують на трохи більше розширення.