ОК, так: у меня есть пакет, построенный на 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
(нажмите на любом из них, чтобы увидеть детали на уровне пикселей):
… 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 для этой цели?
Я знаю, что фон и примеры были довольно длинными, но если вы читали это далеко, я ценю любые практические знания, которые вы могли бы иметь в этой области,