2025, Oct 06 19:16

ttk.Widget и tkinter.Widget: как устроено наследование без путаницы

Почему ttk.Widget наследуется от tkinter.Widget, а не от самого себя: разбор исходников, иерархии классов, __init__.py и практические советы по чтению кода.

Чтение исходников tkinter поначалу может сбивать с толку, особенно когда два класса носят одно и то же имя. Частый камень преткновения — увидеть, что в ttk объявлен класс Widget(tkinter.Widget), и решить, будто он каким‑то образом расширяет сам себя. Это не так. Ниже — что на самом деле происходит и почему это спроектировано именно так.

Откуда берётся путаница

Модуль ttk импортирует пакет tkinter, а затем объявляет Widget, который является подклассом tkinter.Widget. Если просматривать только модуль ttk, может показаться, что других Widget нигде нет и что ttk.Widget наследуется сам от себя. Ключевой момент в том, что сам tkinter определяет базовый класс Widget внутри файла __init__.py своего пакета, и уже на нём строится ttk.

Минимальный пример, который вызывает вопрос

Структуру, порождающую сомнение, можно свести к такому шаблону:

import tkinter as ui
class Panel(ui.Widget):
    """Base class for themed widgets."""
    pass

На первый взгляд это выглядит как круговая зависимость. На деле — нет, потому что ui.Widget ссылается на класс, который уже существует внутри пакета tkinter.

Что происходит на самом деле

Внутри пакета tkinter базовый класс виджета определён в __init__.py. По сути, это похоже на следующее:

class BaseItem(CoreBase, PackMixin, PlaceMixin, GridMixin):
    pass

Его назначение описано так:

Внутренний класс. Базовый класс для виджета, который можно позиционировать менеджерами компоновки Pack, Place или Grid.

Слой ttk затем наследуется от этого базового класса. Иными словами, ttk.Widget наследует от tkinter.Widget, а не от самого себя. Поэтому ttk обычно импортируют через from tkinter import ttk: ttk построен поверх tkinter и расширяет его виджеты, в первую очередь их возможности оформления.

Задуманная иерархия наследования

Связь наследования в ttk можно набросать так:

import tkinter as ui
class StyledPanel(ui.Widget):
    """Base class for themed widgets."""
    def __init__(self, parent, kind, opts=None):
        parent = init_container(parent)
        ui.Widget.__init__(self, parent, kind, kw=opts)

Логика повторяет оригинал: импортировать пакет tkinter, унаследоваться от его Widget, нормализовать родительский контейнер и вызвать инициализатор базового класса.

Решение: смотрите на полное имя

Исправлять здесь нечего — нужно лишь распознать совпадение имён. Важно полное квалифицированное имя. tkinter.Widget указывает на класс, определённый в __init__.py пакета tkinter. ttk.Widget — отдельный класс, который от него наследуется. Имена совпадают, но модули разные, поэтому никакого циклического наследования нет.

Почему это важно

Понимание этого «слоёного» устройства помогает ориентироваться в стандартной библиотеке и правильно строить собственные иерархии. Вы можете создать свой класс, унаследованный от tkinter.Widget, от ttk.Widget или вовсе ни от одного из них — в зависимости от требуемого поведения и основы для стилей. Это также подчёркивает практический урок читаемости: переиспользовать одно и то же имя класса в разных модулях допустимо, но без внимательного чтения квалификаторов модулей это может замедлять понимание кода.

Выводы

Когда в коде tkinter и ttk встречается конструкция вида class X(Y.Z), внимательно читайте квалификатор. ttk строится поверх tkinter, наследуя его базовый Widget изнутри пакета tkinter. Такой дизайн преднамерен и часто встречается в многоуровневых библиотеках. Перенося похожие приёмы в свой проект, предпочитайте явные, полностью квалифицированные ссылки и по возможности подбирайте различающиеся имена классов — так иерархии легче воспринимать.

Статья основана на вопросе с StackOverflow от Darien Marks и ответе от JRiggles.