Яка мета внутрішніх класів 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.