Python: проверка графа вызовов иерархии объектов, которая часто использует композицию - PullRequest
0 голосов
/ 19 марта 2019

ОК, так: у меня есть пакет, построенный на PIL / Pillow (далее просто «Pillow»), классов обработки изображений.Основная идея состоит в том, что процессор изображений - это класс, экземпляры которого имеют метод process(…), который принимает один экземпляр изображения Pillow и возвращает один (возможно, мутированный, возможно, другой) экземпляр изображения Pillow.

Все процессоры изображений в пакете основаны на ABC.Корневой ABC выглядит следующим образом:

from abc import ABC, abstractmethod

class Processor(ABC):

    @abstractmethod
    def process(self, image):
        """ Process an image instance, per the processor instance,
            returning the processed image data.
        """
        ...

(NB, фактический рабочий опубликованный ABC, о котором идет речь , имеет немного больше вещей, но я и буду упрощать вещи дляради этого Q.)

И многие из более простых реализаций процессора изображений выглядят так:

class Brightness(Processor):
    # __init__() sets some parameters on self

    def process(self, image):
        # … adjust brightness …
        return brightened

class Hue(Processor):
    # __init__() sets some parameters on self

    def process(self, image):
        # … adjust hue …
        return recolorized

… они сначала создаются (processor = Hue(2.7)), а затем применяются к одному или несколькимэкземпляры изображений (recolorized = processor.process(image)).

Однако не все процессоры так просты: одним из наиболее часто используемых процессоров в пакете является процессор Mode, основанный на enum(используя enum34 в Python 2.7):

from enum import Enum, unique, auto

@unique
class Mode(Enum, Processor):

    MONO    = auto()
    GRAY    = auto()
    RGB     = auto()
    CMYK    = auto()
    YCbCr   = auto() # …

    def process(self, image):
        if image.mode == self.to_string():
            return image
        return image.convert(self.to_string())

(Внимание: рабочая версия этого немного более сложная.)

Oneиспользует такой процессор enum на основе простого доступа к одному из перечисленных отдельных экземпляров, например: rgbimage = Mode.RGB.process(image).

Кроме того, существуют процессорные контейнеры (или контейнерные процессоры, в зависимости от того, что меньше.неудобно), которые объединяют типы контейнеров Pythonс процессорной логикой.Два (упрощенных) примера: процессор Pipeline на основе list и процессор ChannelFork на основе defaultdict:

from collections import defaultdict

class Pipeline(Processor):

    def __init__(self, *processors):
        self.list = [*processors]
        for processor in self.list:
            assert callable(getattr(processor, 'process', None)) # ITS A DUCK

    def process(self, image):
        for processor in self.list:
            image = processor.process(image)
        return image

class ChannelFork(defaultdict):

    # … lots of implementation stuff skipped …

    def __init__(self, processor_factory, *args, **kwargs):
        self.mode = kwargs.pop('mode', Mode.CMYK)
        super(ChannelFork, self).__init__(processor_factory, *args, **kwargs)

    def process(self, image):
        # … possibly pre-process “image” e.g. GCR
        bands = []
        for channel_label, channel in zip(self.mode.bands,
                                          mode.process(image).split()):
            # R, G, B; C, M, Y, K; etc.
            bands.append(self[channel_label].process(channel))
        recomposed = Image.merge(self.mode.to_string(), bands)
        # … possibly post-process “recomposed”
        return recomposed

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

class Dither(Processor):

    def process(self, image):
        brightened = Brightness(self.brightness).process(image)
        monochrome = Mode.MONO.process(brightened)
        # … do the dithering …
        return dithered

def ColorHalftone(Processor):

    def __init__(self, gcr_percentage=20, OutputMode=None, Ditherer=Dither):
        # Using a Pipeline in leu of instance variables:
        # self.channelfork = ChannelFork(Ditherer)
        # self.gcr = BasicGCR(percentage=gcr_percentage)
        # self.output_mode = output_mode
        self.pipeline = Pipeline(ChannelFork(Ditherer),
                                 BasicGCR(percentage=gcr_percentage),
                                 OutputMode or Mode.RGB)

    def process(self, image):
        return self.pipeline.process(image)

Теперь: несмотря на абстрактную неопределенность моих примеров, все это в высшей степени не теоретическое;Вот два изображения, которые только что были получены на моем ноутбуке с использованием процессора, похожего на приведенный выше пример ColorHalftone (нажмите на любом из них, чтобы увидеть детали на уровне пикселей):

image

image

… so yes! Everything thus far is working out great. But, as you may have gathered during my lengthy introductory example-code rollout, these processors can become a complex nesting of sub-processors and sub-sub-processors.

And so I come now to the actual question, per the post title: I am interested in developing a way to inspect the call graph of all of these Processor subclasses, and the interdependent network of process(…) calls that exists between them all.

Personally I have never written anything to generate any kind of call graph. I’m only familiar with the phrase “call graph” from using предоставлены утилиты как части других пакетов программ - хотя я не новичок в программировании, это просто одна из многих ниш, которые мне еще предстоит исследовать.

Итак, вот вопрос: как мне составить график всех моих вызовов «процесса (…)»?Так, что я могу видеть, когда один такой вызов содержит другие и когда они выполняются последовательно, каким вызываемым абонентом?

Я предполагаю, что захочу подключиться к корневому абстрактному базовому классу и его реализации (или его отсутствию)«процесса (…)», но я не уверен, куда идти дальше:

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

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

...