Можно ли выполнять код при импорте модуля?

Я разрабатываю небольшое приложение GUI для обертывания базы данных sqlite (простые операции CRUD). Я создал три модели sqlalchemy (m_person, m_card.py, m_loan.py, все в папке /models) и ранее имел следующий код вверху каждого из них:

from sqlalchemy import Table, Column, create_engine
from sqlalchemy import Integer, ForeignKey, String, Unicode
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import backref, relation

engine = create_engine("sqlite:///devdata.db", echo=True)
declarative_base = declarative_base(engine)
metadata = declarative_base.metadata

Это немного ошиблось (DRY), поэтому было предложено переместить все это на уровень модуля (в models/__init__.py).

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Table, Column, Boolean, Unicode
from settings import setup

engine = create_engine('sqlite:///' + setup.get_db_path(), echo=False)
declarative_base = declarative_base(engine)
metadata = declarative_base.metadata

session = sessionmaker()
session = session()

.. и импортируйте declarative_base так:

from sqlalchemy import Table, Column, Unicode
from models import declarative_base


class Person(declarative_base):
    """
    Person model

    """
    __tablename__ = "people"

    id = Column(Unicode(50), primary_key=True)
    fname = Column(Unicode(50))
    sname = Column(Unicode(50))

Однако у меня было много отзывов о том, что выполнение кода, поскольку модуль импортирует, как это плохо? Я ищу окончательный ответ на правильный способ сделать это, как кажется, пытаясь удалить повторение кода. Я представил некоторые другие плохие методы. Любая обратная связь будет действительно полезна.

(Ниже приведен метод get_db_path() из settings/setup.py для полноты, так как он вызывается в приведенном выше коде models/__init__.py.)

def get_db_path():

    import sys
    from os import makedirs
    from os.path import join, dirname, exists
    from constants import DB_FILE, DB_FOLDER

    if len(sys.argv) > 1:
        db_path = sys.argv[1]
    else:
        #Check if application is running from exe or .py and adjust db path accordingly
        if getattr(sys, 'frozen', False):
            application_path = dirname(sys.executable)
            db_path = join(application_path, DB_FOLDER, DB_FILE)
        elif __file__:
            application_path = dirname(__file__)
            db_path = join(application_path, '..', DB_FOLDER, DB_FILE)

    #Check db path exists and create it if not
    def ensure_directory(db_path):
        dir = dirname(db_path)
        if not exists(dir):
            makedirs(dir)

    ensure_directory(db_path)

    return db_path

Ответ 1

Некоторые популярные фреймворки (Twisted - один из примеров) выполняют логику инициализации во время импорта. Преимущества возможности динамического построения содержимого модуля приходят по цене, одна из которых заключается в том, что IDE не всегда могут решить, что находится в модуле или нет.

В вашем конкретном случае вы можете захотеть реорганизовать, чтобы конкретный движок не поставлялся во время импорта, но позже. Вы можете создать метаданные и свой declarative_base класс во время импорта. Затем во время запуска, после того, как все классы определены, вы вызываете create_engine и привязываете результат к вашему sqlalchemy.orm.sessionmaker. Но если ваши потребности просты, вам, возможно, даже не нужно заходить так далеко.

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

То, что я определенно избегу, - это любой код в вашем модуле, который заставляет порядок импорта иметь значение для ваших пользователей (кроме обычного способа простого использования логики) или изменяет поведение кода вне вашего модуля, Затем ваш модуль становится черной магией, которая принимается в мире Perl (ala use strict;), но я нахожу, что это не "pythonic".

Например, если ваш модуль изменяет свойства sys.stdout при импорте, я бы сказал, что поведение должно быть заменено на функцию, которую пользователь может вызвать или нет.

Ответ 2

В принципе нет ничего плохого в выполнении кода Python при импорте модуля, ведь каждый модуль Python работает именно так. В конце концов, члены модуля выполняют код.

Однако в вашем конкретном случае использования я бы настоятельно советовал не создавать объект сеанса базы данных singleton в вашей базе кода. Вы потеряете способность делать много вещей, например unit test свою модель против SQLite в памяти или других механизмов базы данных.

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

Ответ 3

Я столкнулся с этим и создал файл database.py с классом менеджера баз данных, после чего я создал один глобальный объект. Таким образом, класс мог считывать настройки из моего файла settings.py для настройки базы данных, а первый класс, который должен был использовать базовый объект (или сеанс/движок), инициализировал глобальный объект, после которого все просто повторно его использовали. Мне гораздо удобнее иметь "из myproject.database import DM" в верхней части каждого класса, используя ORM SQLAlchemy, где DM - мой глобальный объект базы данных, а затем DM.getBase(), чтобы получить базовый объект.

Вот мой класс database.py:

Session = scoped_session(sessionmaker(autoflush=True))

class DatabaseManager():
    """
    The top level database manager used by all the SQLAlchemy classes to fetch their session / declarative base.
    """
    engine = None
    base = None

    def ready(self):
        """Determines if the SQLAlchemy engine and base have been set, and if not, initializes them."""
        host='<database connection details>'
        if self.engine and self.base:
            return True
        else:
            try:
                self.engine = create_engine(host, pool_recycle=3600)
                self.base = declarative_base(bind=self.engine)
                return True
            except:
                return False

    def getSession(self):
        """Returns the active SQLAlchemy session."""
        if self.ready():
            session = Session()
            session.configure(bind=self.engine)
            return session
        else:
            return None

    def getBase(self):
        """Returns the active SQLAlchemy base."""
        if self.ready():
            return self.base
        else:
            return None

    def getEngine(self):
        """Returns the active SQLAlchemy engine."""
        if self.ready():
            return self.engine
        else:
            return None

DM = DatabaseManager()

Ответ 4

Нет ничего плохого в выполнении кода во время импорта.

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

Обычно это означает определение классов, функций и глобальных имен (на самом деле уровень модуля - бит неправильного ввода там), а также импорт любых ваших классов, функций и т.д., должны работать.

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