Циркулярно-зависимые атрибуты класса и локальность кода - PullRequest
0 голосов
/ 24 июня 2019

Если у вас есть два класса, которые должны иметь атрибуты, которые ссылаются друг на друга

# DOESN'T WORK
class A:
    b = B()

class B:
    a = A()
# -> ERROR: B is not defined

Стандарт отвечает говорит, что использует тот факт, что python является динамическим, т. Е.

class A:
    pass

class B:
    a = A()

A.b = B()

Что технически решает проблему. Однако, когда есть три или более взаимозависимых класса или когда длина классов превышает несколько строк, такой подход приводит к спагетти-коду, по которому чрезвычайно сложно ориентироваться. Например, я пишу код вроде:

class A:
    <50 lines>
    # a = B() but its set later
    <200 more lines>

class B:
    <50 lines>
    a = A()
    <100 lines>

A.b = B()  # to allow for circular referencing

Это приводит к нарушению DRY (так как я пишу код в двух местах) и / или перемещает связанный код в противоположные концы моего модуля, поскольку я не могу поместить A.b = B() в класс, к которому он относится.

Есть ли лучший способ разрешить циклически зависимые свойства класса в Python, который не включает в себя рассеяние связанного кода на часто удаленных частях модуля?

1 Ответ

0 голосов
/ 24 июня 2019

После небольшого количества экспериментов я нашел способ (в основном) делать то, что я хочу.

class DeferredAttribute:
    """ A single attribute that has had its resolution deferred """
    def __init__(self, fn):
        """fn - when this attribute is resolved, it will be set to fn() """
        self.fn = fn

    def __set_name__(self, owner, name):
        DeferredAttribute.DEFERRED_ATTRIBUTES.add((owner, name, self))

    @classmethod
    def resolve_all(cls):
        """ Resolves all deferred attributes """
        for owner, name, da in cls.DEFERRED_ATTRIBUTES:
            setattr(owner, name, da.fn())
        cls.DEFERRED_ATTRIBUTES.clear()

Идиомы, используемые для этого:

class A:
    @DeferredAttribute
    def b():
        return B()

class B:
    a = A()

DeferredAttribute.resolve_all()

И это приводит кклассы A и B, точно такие же, как если бы вы запустили код

class A:
    pass

class B:
    a = A()

A.b = B()

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

С другой стороны, это не оправдывает некоторые ожидания динамического программирования;пока не будет вызван resolve_deferred_attributes, значение A.b будет специальным значением, а не экземпляром B.Кажется возможным частично исправить это, добавив соответствующие методы к DeferredAttribute, но я не вижу способа сделать его идеальным.

Примечание редактора: Приведенный выше код заставляет мою IDE (PyCharm) кричать на меня с ошибкой, говоря, что def b(): должен принимать параметр (хотя он работает нормально).Если вы хотите, вы можете изменить ошибку на предупреждение, изменив код:

In the resolve_all method, change:
    setattr(owner, name, da.fn())

    ->

    fn = da.fn
    if isinstance(fn, staticmethod):
        setattr(owner, name, fn.__func__())
    else:
        setattr(owner, name, fn())

And in the use code, change:
    @defer_attribute
    def b():
        ...

    -> 

    @defer_attribute
    @staticmethod
    def b():
        ...

Я не нашел способа полностью исключить предупреждения, кроме их отключения.

...