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

Предположим, что у class A есть член типа class B, а у class B есть член типа class A.

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

Однако в 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]`

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


person Naetmul    schedule 06.10.2018    source источник
comment
Я думаю просто закрыть это как дубликат Подсказки типа: разрешить циклическую зависимость, потому что это просто еще одна проблема циклической зависимости подсказки типа.   -  person Martijn Pieters    schedule 09.10.2018


Ответы (3)


Есть несколько способов решить подобные циклические зависимости, см. Подсказки типов: решить циклическую зависимость

Вы всегда можете применить декоратор вручную (и обновить аннотации), как показано в ответе @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
person Acorn    schedule 08.10.2018
comment
Импорт __future__ хорош, но не работает, если вам нужно сделать что-то вроде a: A = field=(default_factory=A). Есть ли что-то дополнительное, что можно сделать в этом случае, или вперед объявлять единственный вариант? - person process91; 12.10.2018
comment
На самом деле, похоже, что форвардное объявление в этом случае тоже не работает... - person process91; 12.10.2018

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

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

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__ идентично ее определению в определении класса.

person Nearoo    schedule 08.10.2018

Поскольку python является языком сценариев, с @dataclass это сделать невозможно. Потому что в python нет механизма «autowired» (внедрение зависимостей). На данный момент, если вам нужна циклическая зависимость, вы должны использовать один из классов как обычный.

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/другом языке и не можете сделать на питоне. Другое направление этого утверждения также верно.)

person Yuriy Leonov    schedule 08.10.2018