python 中的模板化对象生成

Templated object generation in python

在 python 中实现模板化对象生成(不确定是这个名字)的好的设计模式是什么?

我的意思是具有如下功能:

from typing import TypeVar

T = TypeVar('T')

def mk_templated_obj_factory(template: T) -> Callable[..., T]:
    """Returns a f(**kwargs) function that returns an object of type T created by a template of the same type."""

Python 有模板字符串。像“这{是}一个{模板}”.format 之类的东西将是如何实现上述目标的。如果我们想获得一个具有签名的“正确”函数(对用户有用,这样他们就知道他们需要提供什么参数!),我们可以这样做:

from inspect import signature, Signature, Parameter
from operator import itemgetter
from typing import Callable

f = "hello {name} how are you {verb}?".format

def templated_string_func(template: str) -> Callable:
    """A function making templated strings. Like template.format, but with a signature"""
    f = partial(str.format, template)
    names = filter(None, map(itemgetter(1), string.Formatter().parse(template)))
    params = [Parameter(name=name, kind=Parameter.KEYWORD_ONLY) for name in names]
    f.__signature__ = Signature(params)

    return f

f = templated_string_func("hello {name} how are you {verb}?")
assert f(name='Christian', verb='doing') == 'hello Christian how are you doing?'
assert str(signature(f)) == '(*, name, verb)'

但是如果我们想建造 dict 工厂呢?有这种行为的东西:

g = templated_dict_func(template={'hello': '$name', 'how are you': ['$verb', 2]})
assert g(name='Christian', verb='doing') == {'hello': '$name', 'how are you': ['doing', 2]}

其他类型的对象呢?

似乎有一个可靠的设计模式...

我建议使用装饰器在从类型映射到处理它们的函数的字典中注册模板函数生成函数。需要字典以便能够以可扩展的方式模板化任何类型的对象,而无需在单个大函数中编写所有模板化逻辑,而是根据需要为新类型添加处理逻辑。

核心代码在Templaterclass,这里只是整理整理:

class Templater:
    templater_registry: dict[type, Callable[[Any], TemplateFunc]] = {}

    @classmethod
    def register(cls, handles_type: type):
        def decorator(f):
            cls.templater_registry[handles_type] = f
            return f

        return decorator
    ...

其中 TemplateFunc 被定义为 Generator[str, None, Callable[..., T]],生成器生成 strs 和 returns 一个 returns 某种类型的函数 T.这样选择是为了使模板处理程序可以生成它们的关键字参数的名称,然后 return 它们的模板函数。 Templater.template_func 方法使用 TemplateFunc 类型的东西来生成具有正确签名的函数。

上面的 register 装饰器是这样写的:

@Templater.register(dict)
def templated_dict_func(template: dict[K, V]):
    pass

相当于:

def templated_dict_func(template: dict[K, V]):
    pass

Templater.templater_registry[dict] = templated_dict_func

模板化任何类型的代码是不言自明的:

class Templater:
    ...

    @classmethod
    def template_func_generator(cls, template: T) -> TemplateFunc[T]:
        # if it is a type that can be a template
        if type(template) in cls.templater_registry:
            # then return the template handler
            template_factory = cls.templater_registry[type(template)]
            return template_factory(template)
        else:
            # else: an empty generator that returns a function that returns the template unchanged,
            # since we don't know how to handle it
            def just_return():
                return lambda: template
                yield  # this yield is needed to tell python that this is a generator

            return just_return()

用于模板化字符串的代码基本没有变化,除了生成参数名称而不是放入函数签名中:

@Templater.register(str)
def templated_string_func(template: str) -> TemplateFunc[str]:
    """A function making templated strings. Like template.format, but with a signature"""
    f = partial(str.format, template)
    yield from filter(None, map(itemgetter(1), string.Formatter().parse(template)))

    return f

list 模板函数可能如下所示:

@Templater.register(list)
def templated_list_func(template: list[T]) -> TemplateFunc[list[T]]:
    entries = []
    for item in template:
        item_template_func = yield from Templater.template_func_generator(item)

        entries.append(item_template_func)

    def template_func(**kwargs):
        return [
            item_template_func(**kwargs)
            for item_template_func in entries
        ]

    return template_func

虽然,如果您不能保证每个模板函数都能处理额外的参数,您需要跟踪哪些参数属于哪些元素,并且只传递必要的参数。我使用 get_generator_return 效用函数(稍后定义)来捕获递归调用的产生值和 return 值。

@Templater.register(list)
def templated_list_func(template: list[T]) -> TemplateFunc[list[T]]:
    entries = []
    for item in template:
        params, item_template_func = get_generator_return(Templater.template_func_generator(item))
        params = tuple(params)
        yield from params

        entries.append((item_template_func, params))

    def template_func(**kwargs):
        return [
            item_template_func(**{arg: kwargs[arg] for arg in args})
            for item_template_func, args in entries
        ]

    return template_func

dict 处理程序的实现方式类似。该系统可以扩展以支持各种不同的对象,包括任意 dataclasses 等等,但我将其留作 reader 的练习! 这是整个工作示例:

import string
from functools import partial
from inspect import Signature, Parameter
from operator import itemgetter
from typing import Callable, Any, TypeVar, Generator, Tuple, Dict, List
from collections import namedtuple

T = TypeVar('T')
U = TypeVar('U')


def get_generator_return(gen: Generator[T, Any, U]) -> Tuple[Generator[T, Any, U], U]:
    return_value = None

    def inner():
        nonlocal return_value
        return_value = yield from gen

    gen_items = list(inner())

    def new_gen():
        yield from gen_items
        return return_value

    return new_gen(), return_value


# TemplateFunc: TypeAlias = Generator[str, None, Callable[..., T]]
TemplateFunc = Generator[str, None, Callable[..., T]]

class Templater:
    templater_registry: Dict[type, Callable[[Any], TemplateFunc]] = {}

    @classmethod
    def register(cls, handles_type: type):
        def decorator(f):
            cls.templater_registry[handles_type] = f
            return f

        return decorator

    @classmethod
    def template_func_generator(cls, template: T) -> TemplateFunc[T]:
        if type(template) in cls.templater_registry:
            template_factory = cls.templater_registry[type(template)]
            return template_factory(template)
        else:
            # an empty generator that returns a function that returns the template unchanged,
            # since we don't know how to handle it
            def just_return():
                return lambda: template
                yield  # this yield is needed to tell python that this is a generator

            return just_return()

    @classmethod
    def template_func(cls, template: T) -> Callable[..., T]:
        gen = cls.template_func_generator(template)
        params, f = get_generator_return(gen)

        f.__signature__ = Signature(Parameter(name=param, kind=Parameter.KEYWORD_ONLY) for param in params)
        return f


@Templater.register(str)
def templated_string_func(template: str) -> TemplateFunc[str]:
    """A function making templated strings. Like template.format, but with a signature"""
    f = partial(str.format, template)
    yield from filter(None, map(itemgetter(1), string.Formatter().parse(template)))

    return f


K = TypeVar('K')
V = TypeVar('V')


@Templater.register(dict)
def templated_dict_func(template: Dict[K, V]) -> TemplateFunc[Dict[K, V]]:
    DictEntryInfo = namedtuple('DictEntryInfo', ['key_func', 'value_func', 'key_args', 'value_args'])
    entries: list[DictEntryInfo] = []
    for key, value in template.items():
        key_params, key_template_func = get_generator_return(Templater.template_func_generator(key))
        value_params, value_template_func = get_generator_return(Templater.template_func_generator(value))
        key_params = tuple(key_params)
        value_params = tuple(value_params)
        yield from key_params
        yield from value_params

        entries.append(DictEntryInfo(key_template_func, value_template_func, key_params, value_params))

    def template_func(**kwargs):
        return {
            entry_info.key_func(**{arg: kwargs[arg] for arg in entry_info.key_args}):
                entry_info.value_func(**{arg: kwargs[arg] for arg in entry_info.value_args})
            for entry_info in entries
        }

    return template_func


@Templater.register(list)
def templated_list_func(template: List[T]) -> TemplateFunc[List[T]]:
    entries = []
    for item in template:
        params, item_template_func = get_generator_return(Templater.template_func_generator(item))
        params = tuple(params)
        yield from params

        entries.append((item_template_func, params))

    def template_func(**kwargs):
        return [
            item_template_func(**{arg: kwargs[arg] for arg in args})
            for item_template_func, args in entries
        ]

    return template_func


g = Templater.template_func(template={'hello': '{name}', 'how are you': ['{verb}', 2]})
assert g(name='Christian', verb='doing') == {'hello': 'Christian', 'how are you': ['doing', 2]}
print(g.__signature__)