Python: вся иерархия подкласса - PullRequest
       14

Python: вся иерархия подкласса

0 голосов
/ 25 сентября 2018

Мое решение находится в нижней части вопроса, основываясь на примере Мистера Мияги


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

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 в абстрактный метакласс, чтобы его реализации требовался параметр 'base' длястать классами, которые затем могут быть созданы.Этот параметр затем используется в классе для создания экземпляров других реализаций с правильной базой.

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

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'>

Ответы [ 2 ]

0 голосов
/ 25 сентября 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) для вашего проекта и сделать его более гибким для вашего варианта использования.

0 голосов
/ 25 сентября 2018

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

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 позволяет экземплярам иметь атрибуты без классов, определяющих их поля.Атрибуты класса можно рассматривать как метаклассы с типом утки.

...