Вложенные классы

Курс по Python ООП: https://stepik.org/a/116336

На этом занятии речь пойдет о вложенных классах. В двух словах, это когда один класс объявлен внутри другого. Да, в Python так тоже можно делать. И здесь сразу возникает два главных вопроса: как это работает и зачем это надо? Я попробую дать ответ сразу на оба этих вопроса.

На мой взгляд, хороший пример использования вложенных классов – это описание моделей во фреймворке Django. Если вы не знаете, что это такое, то не беспокойтесь, нам нужна будет только суть вложений. Итак, там часто можно встретить определения вида:

class Women:
    title = 'объект класса для поля title'
    photo = 'объект класса для поля photo'
 
    class Meta:
        ordering = ['id']

Здесь объявляется некий класс Women, далее прописаны два атрибута, которые в Django являются ссылками на определенные классы, но в нашем примере – это просто строки. Причем, атрибуты title и photo – это поля в таблице БД, с которой впоследствии идет работа, то есть, полагаем, что нам важно называть эти атрибуты именно такими именами. Затем, идет объявление вложенного класса Meta с одним атрибутом ordering. В Django с помощью такого класса Meta определяют некие вспомогательные параметры. В частности, параметр ordering определяет сортировку по полю id записей в таблице БД. Если вы ничего не поняли про записи и таблицы, то это не важно. Главное сейчас, что у нас есть класс Women, в нем атрибуты со строго определенными именами и вложенный класс Meta с одним атрибутом ordering. Это все, что нужно сейчас знать.

Но все же, зачем нужен такой вложенный класс? Почему бы этот атрибут не прописать непосредственно в классе Women? Давайте предположим, что в нашей таблице есть поле с именем ordering и для работы с ним в классе Women его следует прописать, сделаем это так:

class Women:
    title = 'объект класса для поля title'
    photo = 'объект класса для поля photo'
    ordering = 'объект класса для поля ordering'
 
    class Meta:
        ordering = ['id']

Видите, если бы не было вложенного класса Meta, то возникла бы проблема с использованием одного и того же атрибута ordering для разных целей: и для представления данных поля ordering и для управления сортировкой. Вложенный класс Meta, фактически, создает независимое пространство имен для разрешения подобных конфликтов.

Разумеется, мы можем совершенно спокойно обращаться ко всем атрибутам класса через основной класс Women, например, так:

print(Women.ordering)
print(Women.Meta.ordering)

В первом случае берем атрибут ordering непосредственно из класса Women, а во втором – из класса Meta.

То же самое можно делать и через экземпляры класса Women:

w = Women()
print(w.ordering)
print(w.Meta.ordering)

Причем внутри самого объекта у нас сейчас не будет никаких локальных свойств:

print(w.__dict__)

Давайте для примера добавим инициализатор в класс Women, запишем его следующим образом:

class Women:
    title = 'объект класса для поля title'
    photo = 'объект класса для поля photo'
    ordering = 'объект класса для поля ordering'
 
    def __init__(self, user, psw):
        self._user = user
        self._psw = psw
 
    class Meta:
        ordering = ['id']

Теперь, при создании объектов:

w = Women('root', '12345')
print(w.__dict__)

в них дополнительно будут создаваться локальные свойства user и psw:

{'_user': 'root', '_psw': '12345'}

Но обратите внимание, объект класса Meta при этом не создается. Здесь Meta – это, по сути, еще один атрибут (пространство имен) в классе Women и мы к нему обращаемся через w как к атрибуту класса, не более того. Если нам нужен объект класса Meta, его нужно явно создать в инициализаторе класса Women:

    def __init__(self, user, psw):
        self._user = user
        self._psw = psw
        self.meta = self.Meta()

Теперь в экземпляре класса Women имеется ссылка meta на объект класса Meta. Чтобы в этой операции был смысл, давайте пропишем инициализатор во вложенном классе:

class Women:
    title = 'объект класса для поля title'
    photo = 'объект класса для поля photo'
    ordering = 'объект класса для поля ordering'
 
    def __init__(self, user, psw):
        self._user = user
        self._psw = psw
        self.meta = self.Meta(user + '@' + psw)
 
    class Meta:
        ordering = ['id']
 
        def __init__(self, access):
            self._access = access

Теперь, при создании объекта класса Women будут автоматически создаваться два объекта: класса Women и класса Meta с соответствующими локальными свойствами:

w = Women('root', '12345')
print(w.__dict__)
print(w.meta.__dict__)

И, обратите внимание, мы не можем из класса Meta или из его объектов обращаться к атрибутам класса Women. Это совершенно изолированное пространство имен от внешнего класса. Например, запись вида:

    class Meta:
        ordering = ['id']
        t = Meta.title      # ошибка при обращении к атрибуту title класса Women

приведет к ошибке, так как у вложенного пространства имен класса нет ссылки на внешнее пространство имен класса Women. Мало того, непосредственно во вложенном классе мы даже не можем явно указывать класс Women:

    class Meta:
        ordering = ['id']
        t = Women.title      # ошибка при обращении к атрибуту title класса Women

то тоже приведет к ошибке, так как пространство имен Women на данный момент еще не создано. А вот обращаться к пространству имен Women внутри инициализатора класса Meta можно:

    class Meta:
        ordering = ['id']
 
        def __init__(self, access):
            self._access = access
            self._t = Women.title

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

В заключение занятия отмечу, что подобные вложения служат исключительно для удобства программирования. Какого-то особого, сакрального смысла во вложенных классах нет и все что можно делать через вложения, также можно реализовать и без них, используя обычные, независимые объявления. Но вы вполне можете столкнуться с ними в других проектах, в частности, как я отмечал вначале, во фреймворке Django. И чтобы это не вызывало у удивления, я решил привести пример работы этой конструкции. Надеюсь, вы теперь хорошо себе представляете, как работают вложенные классы.

Курс по Python ООП: https://stepik.org/a/116336

Видео по теме