Python: подкласс целой иерархии

Мое решение находится в конце вопроса, на основе примера Мистера Мияги


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

import abc


# the abstract class
class X(abc.ABC):
    @abc.abstractmethod
    def f(self):
        raise NotImplementedError()

# first implementation
class X1(X):
    def f(self):
        return 'X1'

# second implementation, using instance of first implementation
class X2(X):
    def __init__(self):
        self.x1 = X1()

    def f(self):
        return self.x1.f() + 'X2'

# demonstration
x = X2()
print(x.f())  # X1X2
print(x.x1.f())  # X1

Теперь я хочу использовать эти классы где-нибудь, скажем, в другом модуле. Однако я хочу добавить некоторые дополнительные функции (например, функцию g) для всех классов в иерархии. Я мог бы сделать это, добавив его к базовому классу X, но я хочу, чтобы функциональность определялась отдельно. Например, я мог бы захотеть определить новую функциональность следующим образом:

class Y(X):
    def g(self):
        return self.f() + 'Y1'

Это создает еще один базовый класс с новой функциональностью, но, конечно, не добавляет его к существующим реализациям X1 и X2. Для этого мне пришлось бы использовать алмазное наследование:

class Y1(X1, Y):
    pass

class Y2(X2, Y):
    pass

# now I can do this:
y = Y2()
print(y.g())  # X1X2Y1

Вышеупомянутое работает правильно, но проблема все еще существует. В X2.__init__ создается экземпляр X1. Чтобы моя идея сработала, это должно стать Y1 в Y2.__init__. Но это, конечно, не так:

print(y.x1.g())  # AttributeError: 'X1' object has no attribute 'g'

Я думаю, что я, возможно, ищу способ превратить X в абстрактный метакласс, чтобы его реализациям требовался «базовый» параметр, чтобы стать классами, которые затем могут быть созданы. Затем этот параметр используется в классе для создания экземпляров других реализаций с правильной базой.

Создание экземпляра с новой функциональностью в базовом классе будет выглядеть примерно так:

class Y:
    def g(self):
        return self.f() + 'Y1'

X2(Y)()

В результате будет получен объект, эквивалентный экземпляру следующего класса:

class X2_with_Y:
    def __init__(self):
        self.x1 = X1(Y)()

    def f(self):
        return self.x1.f() + 'X2'

    def g(self):
        return self.f() + 'Y1'

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


Решение

Используя пример Мистера Мияги, я смог получить кое-что, что, как мне кажется, сработает. Поведение близко к моей идее метакласса.

import abc


class X(abc.ABC):
    base = object  # default base class

    @classmethod
    def __class_getitem__(cls, base):
        if cls.base == base:
            # makes normal use of class possible
            return cls
        else:
            # construct a new type using the given base class and also remember the attribute for future instantiations
            name = f'{cls.__name__}[{base.__name__}]'
            return type(name, (base, cls), {'base': base})


    @abc.abstractmethod
    def f(self):
        raise NotImplementedError()


class X1(X):
    def f(self):
        return 'X1'


class X2(X):
    def __init__(self):
        # use the attribute here to get the right base for the other subclass
        self.x1 = X1[self.base]()

    def f(self):
        return self.x1.f() + 'X2'


# just the wanted new functionality
class Y(X):
    def g(self):
        return self.f() + 'Y1'

Использование такое:

# demonstration
y = X2[Y]()
print(y.g())  # X1X2Y1
print(y.x1.g())  # X1Y1
print(type(y))  # <class 'abc.X2[Y]'>
# little peeve here: why is it not '__main__.X2[Y]'?

# the existing functionality also still works
x = X2()
print(x.f())  # X1X2
print(x.x1.f())  # X1
print(type(x))  # <class '__main__.X2'>

person Community    schedule 25.09.2018    source источник
comment
Возможно ли сделать X2 наследовать от X1 вместо того, чтобы создавать его экземпляр (используя super().f() + 'X2')? Таким образом можно было просто увеличить X1 и все.   -  person a_guest    schedule 25.09.2018
comment
Поведение между различными реализациями Xi в действительности сильно различается, так что наследование их друг от друга, а не от базового класса, я думаю, не имеет смысла. Кроме того, как я уже сказал, я не хочу добавлять эту новую функциональность в семейство X. Это новая функциональность, которую я хочу добавить к ней как к семейству Y, сохранив при этом исходное семейство X. Я хочу сделать это для разделения функций, а также для того, чтобы иметь возможность иметь семейство Z, которое также основано на X, но не имеет ничего общего с Y.   -  person    schedule 25.09.2018
comment
Тогда как насчет использования метакласса в целом, которому вы можете передать семейство в качестве аргумента, например class Y(X, family='Y'). Затем вы можете соответствующим образом настроить переменные в метаклассе __new__.   -  person a_guest    schedule 25.09.2018
comment
Я думаю, что это может быть похоже на то, о чем я думаю, но я не знаю, как это реализовать. Например, я не знаю, куда идет параметр family или почему 'Y' - это строка?   -  person    schedule 25.09.2018
comment
Как вы это описываете, существует некоторая сильная связь между отдельными иерархиями классов. Вы уверены, что ищете специализацию, то есть class Y2(Y, X2[Y1]), а не украшения, то есть class Y2(Y(X2(Y1)))?   -  person MisterMiyagi    schedule 25.09.2018
comment
На какую версию Python вы нацеливаетесь?   -  person MisterMiyagi    schedule 25.09.2018
comment
Я не имел в виду связывание: иерархия X завершена и не полагается на Y, но Y строится на X. Я думаю, что украшение - это действительно то, что я ищу: я хочу добавить ту же функциональность к существующей группе классов (и когда классы создают экземпляры друг друга, я также хочу, чтобы эти новые экземпляры обладали новой функциональностью). Хотя я не понимаю обозначение классов в вашем комментарии? Я использую Python 3.7.   -  person    schedule 25.09.2018


Ответы (2)


Поскольку вы ищете способ настроить свои классы, самый простой подход - это сделать именно это:

class X2(X):
    component_type = X1

    def __init__(self):
        self.x1 = self.component_type()

class Y2(X2, Y):
    component_type = Y1

Поскольку component_type является атрибутом класса, он позволяет специализировать различные варианты (читай: подклассы) одного и того же класса.


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

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

class X2(X):
    hierarchy = X

    @classmethod
    def specialise(cls, hierarchy_base):
        class Foo(cls, hierarchy_base):
            hierarchy = hierarchy_base
        return Foo

Y2 = X2.specialise(Y)

Начиная с Python3.7 можно использовать __class_getitem__ для напишите выше как Y2 = X2[Y], аналогично тому, как Tuple может быть специализирован для Tuple[int].

class X2(X):
    hierarchy = X

    def __class_getitem__(cls, hierarchy_base):
        class Foo(cls, hierarchy_base):
            hierarchy = hierarchy_base
        return Foo

Y2 = X2[Y]

Атрибуты класса часто служат функциям полей метакласса, поскольку они выражают именно это. Теоретически установка атрибута класса эквивалентна добавлению поля метакласса с последующей установкой его в качестве атрибута для каждого класса. Здесь используется то, что Python позволяет экземплярам иметь атрибуты без классов, определяющих их поля. Атрибуты класса можно рассматривать как метаклассы для набора текста.

person MisterMiyagi    schedule 25.09.2018
comment
Спасибо! Хотя проблема с этим будет в том, что на самом деле у меня есть классы X1...Xn, которым все нужны свои Y1...Yn, и всем им нужно знать обо всем друг друга, поэтому для каждого потребуются атрибутыcomponent_type_1...component_type_n. Я надеюсь на менее подробное и подробное решение. - person ; 25.09.2018
comment
@Marein Это очень расплывчатое описание. Откуда берутся X1...Xn - определяются ли они вручную или генерируются? n всегда одно и то же? Xn нужно все Xn-1, Xn-2, ... или только Xn-1? Является ли отношение Xn, Xn-1, ... присущим классам или более подходящим для обработки внешней фабрикой? - person MisterMiyagi; 25.09.2018
comment
Извините за расплывчатость. Я имел в виду, что в моем примере у меня есть только X1 и X2, но в моем фактическом варианте использования у меня есть больше реализаций X. Они определяются вручную, в настоящее время n=4. Я хотел сказать, что для (n^2) становится очень многословным определять все типы компонентов. - person ; 25.09.2018
comment
Я понимаю, что у вас классов больше, но совершенно непонятно, как они соотносятся друг с другом. Например, если каждому Xn нужны все остальные X1...Xn, он может получить их из своего базового класса. Если вместо этого каждому Xn нужен конкретный Xn-1, он должен это знать. - person MisterMiyagi; 25.09.2018
comment
Классы Xi ссылаются на другие Xj классы и создают их экземпляры в определенных точках как часть своей функциональности. Не каждый Xi класс обязательно будет использовать любой другой Xj класс, в зависимости от их функциональности, но они могут. Если бы это помогло, я мог бы дать обзор фактического использования. - person ; 25.09.2018
comment
@Marein не обязательно, но они могут не проблема, которую я могу решить за вас. Как Xi знает, какой Xj использовать? Если вам все равно нужно это сказать, с таким же успехом можно написать эту единственную строку ... В любом случае, я добавил пример создания X2_with_Y из X2.specialise(Y) и X2[Y]. - person MisterMiyagi; 25.09.2018
comment
Думаю, мне удалось это сделать на твоем примере! Я отредактировал вопрос, чтобы показать результат. - person ; 25.09.2018

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

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

Это можно рассматривать как вмешательство в чужие дела. Я имею в виду, если класс X2 хочет создать экземпляр X1, кто вы (простой пользователь X2!), Чтобы сказать ему, что он вместо этого создаст что-то еще ?? Возможно, он не работает должным образом с чем-то, не относящимся к типу X1!

Но конечно. Был там. Готово. Я знаю, что иногда такая потребность возникает.

Самый простой способ добиться этого - заставить класс X2 сотрудничать. Это означает, что вместо создания экземпляра класса X1 он может вместо этого создать экземпляр класса, переданного в качестве параметра:

class X2(X):
    def __init__(self, x1_class=X1):
        self.x1 = x1_class()

Это также можно было бы красиво встроить, используя переопределение метода вместо передачи параметров:

class X2(X):
    @classmethod
    def my_x1(cls):
         return X1

    def __init__(self):
        self.x1 = self.my_x1()

а затем в другом модуле:

class Y2(X2, Y):
    @classmethod
    def my_x1(cls):
        return Y1

Но все это просто работает, если вы можете изменить X2, а в некоторых случаях вы не можете этого сделать (потому что модуль X предоставляется сторонними поставщиками или даже встроенной библиотекой, поэтому фактически доступен только для чтения).

В этих случаях вы можете рассмотреть возможность установки патчей обезьяны:

def patched_init(self):
    self.x1 = Y1()

X1.__init__ = patched_init

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

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

person Alfe    schedule 25.09.2018
comment
Это можно рассматривать как вмешательство в чужие дела. Вот почему я подумал, что метакласс с «базовым» параметром может быть хорошей идеей, поскольку X сам по себе раскрывает возможность создания подкласса внешнего класса. При использовании решения для передачи параметров / переопределения метода мне все равно придется вручную определять все Yi варианты всех Xi реализаций. и мне нужно было бы передать их все как параметры. Я надеюсь на менее подробное и ручное решение. - person ; 25.09.2018