Можно ли создать рекурсивный класс данных с помощью make_dataclass в python?

#python #python-typing #python-dataclasses

Вопрос:

Вот простой пример, в котором я пытаюсь создать рекурсивное определение узла, содержащее необязательный дочерний элемент, который также является узлом. Код компилируется, но когда я пытаюсь получить доступ к определениям типов, которые я получаю node , они не определены. Можно ли обойти эту ошибку?

 import dataclasses
import typing as t

node_type = dataclasses.make_dataclass(
    "node", [("child", t.Optional["node"], dataclasses.field(default=None))]
)
print(t.get_type_hints(node_type))
 

Выходы

 NameError: name 'node' is not defined
 

Я использую python 3.9.2.

Ответ №1:

Здесь есть три проблемы. Они разрешимы, но они могут быть не совсем разрешимы в тех ситуациях, когда вы бы их действительно использовали dataclasses.make_dataclass .

Первая проблема заключается в том , что typing.get_type_hints ищется класс с именем 'node' , но вы назвали глобальную переменную node_type . Имя , которое вы передаете make_dataclass , имя, которое вы используете в аннотациях, и имя, которому вы назначаете класс данных, должны быть одинаковыми:

 Node = dataclasses.make_dataclass(
    "Node", [("child", t.Optional["Node"], dataclasses.field(default=None))]
)
 

Но этого все равно будет недостаточно, потому typing.get_type_hints что это не поиск в правильном пространстве имен. Это вторая проблема.

Когда вы вызываете typing.get_type_hints класс, typing.get_type_hints попытаетесь разрешить строковые аннотации, заглянув в модуль, в котором был определен класс. Он определяет этот модуль, просматривая __module__ запись в классе __dict__ . Поскольку вы создали свой класс узлов странным способом, который не проходит через обычный class оператор, класс __module__ не настроен для ссылки на нужный модуль. Вместо этого он настроен на 'types' .

Вы можете исправить это, вручную предварительно настроив __module__ __name__ текущий модуль:

 Node = dataclasses.make_dataclass(
    "Node",
    [("child", t.Optional["Node"], dataclasses.field(default=None))],
    namespace={'__module__': __name__}
)
 

Затем typing.get_type_hints вы сможете разрешить аннотации строк.

Мета-проблема в том, что если вы используете dataclasses.make_dataclass на практике, вы, вероятно, не знаете имени класса. Вероятно, вы используете его в функции и/или внутри цикла. typing.get_type_hints должен быть в состоянии найти класс с помощью глобальной переменной, соответствующей имени класса, но имена динамических переменных запутаны.

Вы можете воспользоваться простым подходом, просто установив глобальную globals() :

 globals()[your_dataclass.__name__] = your_dataclass
 

но это опасно. Если два сгенерированных класса имеют одно и то же имя, второй заменит первый. Если созданный класс имеет то же имя, что и что-то другое в глобальном пространстве имен , например, если вы сделали from some_dependency import Thing , а затем создали класс с именем Thing , созданный класс уничтожит существующее глобальное значение.

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

Комментарии:

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

2. Не обращай внимания на мой комментарий. Я могу просто использовать globals() .

3. @LukeMurray: Обратите внимание, что это зависит от того, что никакие два класса, созданные таким образом, не имеют одного и того же имени, и никакие имена классов никогда не сталкиваются с именем, которое вы уже используете в этой глобальной области.