Есть ли способ статически разрешить эти циклические зависимости?

У меня есть несколько классов Python, которые связаны друг с другом, они пытаются имитировать схему graphql (сама схема не имеет значения, я публикую здесь базовый вариант, чтобы воспроизвести проблему).

Схема GraphQL выглядит так:

type User {
  name: String
  orders: [Order]
}

type Order {
  key: String
  user: User
}

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

На стороне Python все становится немного запутанным.

Я ожидаю, что следующий код будет работать:

файл: модели/Model.py

import attr

@attr.s
class Model():
  pass # Model internal workings not relevant to the example

файл: модели/User.py

from typing import List
import attr
from . import Model

@attr.s
class User(Model):
  name: str = 'Name'
  orders: List[Order] = attr.ib(factory=list)

файл: модели/Order.py

import attr
from . import Model

@attr.s
class Order(Model):
  key: str = 'abc'
  user: User = attr.ib(factory=User)

то я могу делать такие вещи:

файл: основной.py

import models as m
user = m.User.query(name='John', with='orders')
user.name # "John"
user.orders # [m.Order(key='1'), m.Order(key='2'), m.Order(key='3')...]
order = m.Order.query(key='1', with='user')
order.key # "1"
order.user # m.User(name="John")

Этот код не работает из-за циклической зависимости (пользователю требуется, чтобы тип заказа был определен ранее, а для заказа требуется пользователь).

Обходной путь, который я нашел, заключался в позднем импорте моделей с использованием importlib:

# current solution:
# using the importlib to import dynamically

from typing import List
import attr
from helpers import convert_to, list_convert_to, 

# Note: "convert_to" receives a class name and returns a function to instantiate it dinamically

@attr.s
class Model():
  pass

@attr.s
class User(Model):
  name: str = 'Name'
  orders: List[Model] = attr.ib(factory=list_convert_to('Order'))

@attr.s
class Order(Model):
  key: str = 'abc'
  user: Model = attr.ib(factory=list_convert_to('User'))

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

Вот почему я ищу лучшие способы решения этой проблемы, есть идеи?


person Baruc Almaguer    schedule 08.04.2020    source источник


Ответы (1)


Предполагая, что вы используете Python 3.7 или более позднюю версию, следующая строка заставит его работать:

from __future__ import annotations

Это также позволяет вам ссылаться на класс при его определении. Например.

class C:
    @classmethod
    def factory(cls) -> C:
        ...

работает сейчас.

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

from typing import TYPE_CHECKING

# ...

if TYPE_CHECKING:
    from module import User
person hynek    schedule 11.04.2020
comment
Даже без annotations вы можете просто использовать строку 'C' вместо ссылки C. (Это почти все, что делает annotations: откладывает вычисление выражения и просто сохраняет его как строку.) - person chepner; 11.04.2020
comment
@hynek, кажется, это работает нормально, если классы определены в одном и том же файле, но все равно не работает, когда каждый класс находится в своем собственном файле: <Model>.py. Есть ли способ обойти это? Каждый файл имеет свой собственный from __future__ import annotations в верхней части файла. - person Baruc Almaguer; 11.04.2020
comment
@chepner Мне нужен конкретный тип, а не строка, потому что я использую его позже для приведения типов (я не уверен, что следую вашему предложению) - person Baruc Almaguer; 11.04.2020
comment
@BarucAlmaguer Я обновил ответ: для этого есть конструкция, но она противоречит вашему требованию к конкретным классам. Вот где, к сожалению, подход Python к типам вступает в противоречие с тем, как работает импорт. - person hynek; 12.04.2020
comment
@hynek Когда вы говорите: ... но это противоречит вашим требованиям к конкретным классам, что вы имеете в виду? TYPE_CHECKING не будет работать с этим примером? если да, есть ли способ реорганизовать эти классы, чтобы разрешить этот тип защищенного импорта, или вы бы предложили включить все модели в один файл? (Файлы модели создаются с помощью сценария, поэтому было бы легко провести рефакторинг, если это лучшая практика) - person Baruc Almaguer; 20.04.2020
comment
Я обновил вопрос, чтобы показать, что модели находятся в разных файлах. - person Baruc Almaguer; 20.04.2020
comment
На самом деле это уже не типичный вопрос: у вас не может быть циклического импорта между файлами. Guard позволяет вам использовать эти типы для ввода, но вы не можете использовать их для создания экземпляров. Вы тоже не должны этого делать, см. Принцип инверсии зависимостей. - person hynek; 23.04.2020