2026, Jan 14 00:03

AttributeError в Python GUI: как исправить вызов методов DAO и порядок инициализации SQLite

В Python GUI на CustomTkinter возникает AttributeError при вызове методов DAO из-за self и порядка инициализации. Покажем сбой и рабочее решение для SQLite.

Когда GUI на Python обращается к слою доступа к данным, легко упустить из виду, что метод экземпляра ожидает именно экземпляр. Частая ошибка — передать объект интерфейса там, где нужен объект работы с базой, и в итоге получить AttributeError, выглядящий поначалу загадочно. В этом разборе мы покажем, почему так происходит, продемонстрируем точный сценарий сбоя и аккуратно его исправим. Заодно разберём подводный камень с порядком инициализации, из‑за которого незаметно ломается работа с SQLite.

Подготовка: неверный вызов метода DAO

Ниже — минимальный объект доступа к данным для SQLite3 и фрейм CustomTkinter, который пытается получить записи. Конструкция в целом корректная, но в неё закрались две тонкие ошибки.

import sqlite3


class Store:

    VEHICLES_DDL = """
                    CREATE TABLE IF NOT EXISTS vehicles (
                        vehicle_key integer NOT NULL PRIMARY KEY AUTOINCREMENT,
                        vehicle_description TEXT NOT NULL
                    )
                """

    SQL_VEHICLE_INSERT = "INSERT INTO vehicles (vehicle_description) VALUES (?)"

    SQL_VEHICLE_SELECT_ALL = "SELECT * FROM vehicles"

    MILEAGE_DDL = """
                  CREATE TABLE IF NOT EXISTS milage (
                    milage_date DATE NOT NULL PRIMARY KEY,
                    vehicle_id INTEGER NOT NULL,
                    milage integer NOT NULL
                  )
                """

    SQL_MILEAGE_INSERT = "INSERT INTO milage ( milage_date , vehicle_id , milage ) VALUES (? , ? , ? )"

    def __init__(self, dbname = "vehicles.sqlite3"):
        # здесь dbname устанавливается слишком поздно
        self.setup_table(self.MILEAGE_DDL)
        self.setup_table(self.VEHICLES_DDL)
        self.dbname = dbname

    def setup_table(self, sql):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(sql)

    def insert_vehicle(self, description):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_INSERT, (description,))

    def insert_milage(self, milage_date, vehicle_id, milage):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_MILEAGE_INSERT, (milage_date, vehicle_id, milage))

    def fetch_vehicles(self):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_SELECT_ALL)
            return cur.fetchall()
import customtkinter as ctk
from store import Store


class MileageInputView(ctk.CTkFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        self.section = ctk.CTkFrame(self)
        self.section.pack(fill="x", padx=5, pady=25)

        # Неверно: передаётся экземпляр GUI туда, где ожидается экземпляр Store
        records = Store.fetch_vehicles(self)

Сбой проявляется как AttributeError, указывающий, что у GUI-объекта нет нужного атрибута базы данных:

AttributeError: 'MilageEntryFrame' object has no attribute 'dbname'

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

В Python self — лишь условное имя первого параметра метода экземпляра. Интерпретатор автоматически передаёт экземпляр, когда вы вызываете метод у экземпляра. Если же вы обращаетесь к методу у класса, первый аргумент нужно передать вручную. И этот аргумент должен быть экземпляром того самого класса, чей метод вызывается; иначе поиск атрибутов внутри метода пойдёт по «чужому» объекту.

>>> class Sample:
...     def task(self):
...         pass
...
>>> type(Sample.task)
<class 'function'>
>>> type(Sample().task)
<class 'method'>

В примере с GUI вызов Store.fetch_vehicles(self) заставляет Python передать экземпляр MileageInputView первым аргументом метода fetch_vehicles. Внутри fetch_vehicles код обращается к self.dbname, но self — это уже фрейм интерфейса, а не объект доступа к данным, поэтому атрибута dbname там нет. Отсюда и AttributeError.

Есть и вторая, отдельная проблема в объекте доступа к данным: dbname присваивается после того, как в конструкторе уже вызываются методы, которые от него зависят. setup_table открывает sqlite3.connect(self.dbname), но self.dbname устанавливается лишь позже. Даже если исправить первую ошибку, такой порядок инициализации рано или поздно сломает работу с БД.

Исправление: создаём экземпляр DAO и инициализируем его правильно

Во‑первых, вызывайте метод экземпляра на реальном экземпляре класса доступа к данным. Во‑вторых, убедитесь, что путь к базе задан в объекте до вызова любых методов, которые от него зависят. В исправленном коде ниже учтены оба пункта без изменения логики.

import sqlite3


class Store:

    VEHICLES_DDL = """
                    CREATE TABLE IF NOT EXISTS vehicles (
                        vehicle_key integer NOT NULL PRIMARY KEY AUTOINCREMENT,
                        vehicle_description TEXT NOT NULL
                    )
                """

    SQL_VEHICLE_INSERT = "INSERT INTO vehicles (vehicle_description) VALUES (?)"

    SQL_VEHICLE_SELECT_ALL = "SELECT * FROM vehicles"

    MILEAGE_DDL = """
                  CREATE TABLE IF NOT EXISTS milage (
                    milage_date DATE NOT NULL PRIMARY KEY,
                    vehicle_id INTEGER NOT NULL,
                    milage integer NOT NULL
                  )
                """

    SQL_MILEAGE_INSERT = "INSERT INTO milage ( milage_date , vehicle_id , milage ) VALUES (? , ? , ? )"

    def __init__(self, dbname = "vehicles.sqlite3"):
        # Сначала установить, затем использовать
        self.dbname = dbname
        self.setup_table(self.MILEAGE_DDL)
        self.setup_table(self.VEHICLES_DDL)

    def setup_table(self, sql):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(sql)

    def insert_vehicle(self, description):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_INSERT, (description,))

    def insert_milage(self, milage_date, vehicle_id, milage):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_MILEAGE_INSERT, (milage_date, vehicle_id, milage))

    def fetch_vehicles(self):
        with sqlite3.connect(self.dbname) as conn:
            cur = conn.cursor()
            cur.execute(self.SQL_VEHICLE_SELECT_ALL)
            return cur.fetchall()
import customtkinter as ctk
from store import Store


class MileageInputView(ctk.CTkFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        self.section = ctk.CTkFrame(self)
        self.section.pack(fill="x", padx=5, pady=25)

        # Правильно: создать экземпляр и вызвать метод
        records = Store().fetch_vehicles()

Почему это важно для поддерживаемого кода на Python

Понимание того, как работают привязанные методы, критично при стыковке слоя интерфейса и слоя доступа к данным. Это предотвращает тонкие ошибки, когда в метод «просачивается» не тот объект, и избавляет от вводящих в заблуждение сообщений о «пропавших» атрибутах у не связанных классов. Не менее важно соблюдать порядок инициализации: атрибуты должны существовать до того, как их используют зависимые методы, особенно если те обращаются к внешним системам вроде базы SQLite.

Выводы

Всегда вызывайте методы экземпляра на экземплярах, а не на классе, и не подставляйте чужой объект вместо self. Задавайте критичные атрибуты — например путь к базе — до любых зависимых вызовов. Соблюдая эти два принципа, вы добьётесь корректного взаимодействия виджетов CustomTkinter и DAO на SQLite, а неожиданные AttributeError из‑за dbname не возникнут.