Как определить циклически зависимые классы данных в Python 3. 7+?

Предположим, что class A имеет член, тип которого является class B, а class B имеет член, тип которого является class A

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

Однако в Python следующий код

class A:
    b = B()

class B:
    a = A()     

выдает ошибку компиляции, потому что class B не определяется, когда определяется class A

Вы можете обойти этот простой случай, как в этом ответе

class A:
    pass

class B:
    a = A()

A.b = B()

Однако этот способ не работает для классов данных в Python, поскольку назначение членов после определения классов данных не будет обновлять автоматически генерируемые методы классов данных, что делает использование класса данных "бесполезным".

@dataclass
class A:
    b: B  # or 'b: Optional[B]'

@dataclass
class B:
    a: A  # or 'a: Optional[A]'

Как я могу избежать этой проблемы?

Ответ 1

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

Вы всегда можете применить декоратор вручную (и обновить аннотации), как показывает ответ @Nearoo.

Однако было бы проще "переслать объявление" в класс:

class A:
    pass

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B

Или просто используйте прямую ссылку:

@dataclass
class B:
    a: 'A'

@dataclass
class A:
    b: B

Самое чистое - импортировать поведение Python 4.0 (если можно):

from __future__ import annotations

@dataclass
class B:
    a: A

@dataclass
class A:
    b: B

Ответ 2

Вы можете достичь своей цели, применяя декоратор dataclass только после того, как мы ввели поле b в A Для этого нам просто нужно добавить аннотацию типа в A __annotations__ -field

Следующий код решает вашу проблему:

class A:
    b: None     # Note: __annotations__ only exists if >=1 annotation exists

@dataclass
class B:
    a: A

A.__annotations__.update(b=B) # Note: not the same as A.b: B
A = dataclass(A) # apply decorator

Что касается безопасности и обоснованности этого метода, PEP 524 заявляет, что

.. на уровне модуля или класса, если элемент, аннотированный, является простым именем, то он и аннотация будут сохранены в атрибуте __annotations__ этого модуля или класса. [Этот атрибут] доступен для записи, поэтому это разрешено:

__annotations__['s'] = str

Поэтому добавление аннотации типа позже путем редактирования __annotations__ идентично определению его при определении класса.

Ответ 3

Поскольку python - это язык скриптов - нет способа сделать это с помощью @dataclass. Потому что в python нет механизма "автоподвешенного" (зависимого впрыска). В этот момент, если вам нужна круговая зависимость, вы должны использовать один из классов как обычный.

class A:
    b = None

@dataclass
class B:
    a: A

a = A()
a.b = B(a)

Компилятор Python проходит через каждую строку, не перескакивая с определения класса/функции. И когда компилятор/интерпретатор видит следующую строку b: B и раньше не видел класс B - он будет генерировать исключение. NameError: name 'B' is not defined

Хотелось бы верить, что есть способ сделать это (круговая зависимость для @dataclass), но правда жестока. (Есть много вещей, которые вы можете сделать на Java/другом языке и не можете делать в python. Другое направление этого утверждения правдиво.)