Яка мета внутрішніх класів python?


98

Внутрішні / вкладені класи Python мене бентежать. Чи є щось, чого неможливо досягти без них? Якщо так, що це за річ?

Відповіді:


85

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

Переваги внутрішнього класу:

  • Логічне групування класів : Якщо клас корисний лише для одного іншого класу, то логічно вбудувати його в цей клас і тримати ці два разом. Вкладання таких "допоміжних класів" робить їх пакет більш впорядкованим.
  • Посилена інкапсуляція : розглянемо два класи вищого рівня A і B, де B потребує доступу до членів A, які в іншому випадку були б оголошені приватними. Приховуючи клас B у класі AA, члени можуть бути оголошені приватними, а B може отримати до них доступ. Крім того, сам В може бути прихований від зовнішнього світу.
  • Більш читабельний, ремонтопридатний код : Вкладання невеликих класів у класи верхнього рівня ставить код ближче до місця, де він використовується.

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


50
Аргумент інкапсуляції, звичайно, не стосується Python.
bobince

30
Перший пункт також не стосується Python. Ви можете визначити стільки класів в одному файлі модуля, скільки завгодно, тим самим зберігаючи їх разом, і це також не впливає на організацію пакета. Останній пункт дуже суб’єктивний, і я не вважаю, що він дійсний. Коротше кажучи, у цій відповіді я не знаходжу жодних аргументів, що підтверджують використання внутрішніх класів у Python.
Chris Arndt

17
Тим не менше, це Є причини, що внутрішні класи використовуються в програмуванні. Ви просто намагаєтеся збити конкуруючу відповідь. Ця відповідь, яку дав тут цей чувак, суцільна.
Inversus

16
@Inversus: Я не згоден. Це не відповідь, це розширене цитування чужої відповіді про іншу мову (а саме Java). Проголосував проти, і я сподіваюся, що інші роблять те саме.
Кевін

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

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), і ви зможете отримати доступ до зовнішнього екземпляра через це.
Євген Сергєєв

Використання WeakKeyDictionaryу цьому прикладі насправді не дозволяє збирати сміття для ключів, оскільки значення сильно посилаються на їх відповідні ключі через їх ownerатрибут.
Kritzefitz

36

Щоб щось зрозуміти, потрібно щось обмотати головою. У більшості мов визначення класів є директивами до компілятора. Тобто клас створюється до запуску програми. У python всі оператори виконувані. Це означає, що це твердження:

class foo(object):
    pass

це оператор, який виконується під час виконання, як і цей:

x = y + z

Це означає, що ви не тільки можете створювати класи в інших класах, ви можете створювати класи де завгодно. Розглянемо цей код:

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

Таким чином, ідея "внутрішнього класу" насправді не є мовною конструкцією; це конструкція програміста. Гвідо має дуже хороший підсумок того, як це сталося тут . Але по суті, основна ідея полягає в тому, що це спрощує граматику мови.


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клас не є корисним для інших типів зв’язаних класів списків, які ви також можете створити, і в цьому випадку він, ймовірно, повинен бути просто зовні.
Прозріння

Інший спосіб сказати це: Nodeзнаходиться під простором імен DoublyLinkedList, і логічно, що так є. Це є Pythonic: «Простір імен один сигналить відмінну ідею - давайте більше тих!»
cbarrick

@cbarrick: Якщо робити "більше таких", це нічого не говорить про їх вкладання.
Ітан Фурман,

3

Чи є щось, чого неможливо досягти без них? Якщо так, що це за річ?

Є те, без чого не можна легко обійтися : успадкування відповідних класів .

Ось мінімалістичний приклад із відповідними класами 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взагалі уникнути успадкування і просто перевизначити 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

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 для метаданих моделі. Дещо зрозуміліше * доручити користувачам фреймворку писати 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щоб побачити його «дочірні елементи» (атрибути).

  • Метод "Посилання на внутрішній клас" може бути більш читабельним, якщо рівень вкладеності коду зростає або рядки довгі з якоїсь іншої причини.

* Звичайно, справа смаку

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 на даний момент, приватні дундери і все.


3
Чи не все це стосується підкласів, а не внутрішніх класів ?? A
klaas

У наведеному вище випадку Баггі успадковує від А. Так що підклас це показує. Також див. Вбудовану функцію issubclass ()
klaas

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