#python #keyword-argument #dispatch #multimethod
#python #ключевое слово-аргумент #отправка #мультиметод
Вопрос:
РЕДАКТИРОВАТЬ # 1: я обновил пример «ручным» решением на основе if / else, как было предложено, чтобы продемонстрировать необходимость дальнейшей автоматизации.
Как эффективно отправлять функции (т. Е. Реализовать Что-то вроде мультиметодов), где целевая функция выбирается на основе имен аргументов ключевых слов вместо типов?
Мой вариант использования для этого — реализация нескольких заводских методов для классов данных, поля которых являются взаимозависимыми и которые могут быть инициализированы на основе разных подмножеств этих полей, например
Ниже приведен пример использования рекурсии, которая работает нормально, но требует большого количества рукописного кода, подверженного ошибкам, и на самом деле не масштабируется для более сложных случаев.
def a_from_b_c(b, c):
return b c
def b_from_a_c(a, c):
return a c
def c_from_a_b(a, b):
return a**b
@datalass
class foo(object):
a: float
b: float
c: float
@classmethod
def init_from(cls, **kwargs):
if "a" not in kwargs and all(k in kwargs for k in ("b", "c")):
kwargs["a"] = a_from_b_c(kwargs["b"], kwargs["c"])
cls.init_from(**kwargs)
if "b" not in kwargs and all(k in kwargs for k in ("a", "c")):
kwargs["b"] = b_from_a_c(kwargs["a"], kwargs["c"])
cls.init_from(**kwargs)
if "c" not in kwargs and all(k in kwargs for k in ("a", "b")):
kwargs["c"] = c_from_a_b(kwargs["a"], kwargs["b"])
cls.init_from(**kwargs)
return cls(**kwargs)
Я ищу решение, которое масштабируется до классов данных со многими полями и сложными путями инициализации, в то же время требуя меньше рукописного кода с большим количеством дубликатов и источников ошибок.. шаблоны в приведенном выше коде довольно очевидны и могут быть автоматизированы, но я хочу быть уверен, что здесь используются правильные инструменты.
Комментарии:
1.
@datalass
, реальный код, это??2. Я думаю, что классы данных были введены в Python 3. См.: docs.python.org/3/library/dataclasses.html
Ответ №1:
После недавнего редактирования, которое, честно говоря, является довольно большим изменением, вы могли бы сделать что-то вроде этого:
import inspect
from dataclasses import dataclass
from collections import defaultdict
class Initializer:
"""This class collects all registered functions and allows
multiple ways to calculate your field.
"""
def __init__(self):
self.mappings = defaultdict(list)
def __call__(self, arg):
def wrapper(func):
self.mappings[arg].append(func)
return wrapper
# Create an instance and register your functions
init = Initializer()
# Add the `kwargs` for convenience
@init("a")
def a_from_b_c(b, c, **kwargs):
return b c
@init("a")
def a_from_b_d(b, d, **kwargs):
return b d
@init("b")
def b_from_a_c(a, c, **kwargs):
return a c
@init("c")
def c_from_a_b(a, b, **kwargs):
return a ** b
@init("d")
def d_from_a_b_c(a, b, c, **kwargs):
return a ** b c
@dataclass
class foo(object):
a: float
b: float
c: float
d: float
@classmethod
def init_from(cls, **kwargs):
# Not sure if there is a better way to access the fields
for field in foo.__dataclass_fields__:
if field not in kwargs:
funcs = init.mappings[field]
# Multiple functions means a loop. If you're sure
# you have a 1-to-1 mapping then change the defaultdict
# to a dict[field->function]
for func in funcs:
func_args = inspect.getfullargspec(func).args
if all(arg in kwargs for arg in func_args):
kwargs[field] = func(**kwargs)
return foo(**kwargs)
Затем используйте его:
>>> foo.init_from(a=3, b=2, d=3)
foo(a=3, b=2, c=9, d=3)
>>> foo.init_from(a=3, b=2, c=3)
foo(a=3, b=2, c=3, d=12)
Комментарии:
1. Извините за основное редактирование .. первоначальный пример был слишком простым, хотя я и сказал, что я собирался использовать не такой тривиальный пример, а масштабируемое решение. В любом случае, спасибо за ваше предложение, оно выглядит довольно многообещающим, и я попробую его как можно скорее
2. Не переживайте. Я был немного соленым перед утренним кофе. Другой способ, которым я думал справиться с этим, заключался в использовании какого-нибудь причудливого
eval
или «сопоставительного» словаря (напримерfield -> tuple(func, params)
, но это казалось дополнительной работой. В любом случае, дайте мне знать, если вы хотите, чтобы я добавил больше деталей в код или объяснений о том, как это работает
Ответ №2:
Вот решение, основанное на идеях @kostas-mouratidis о сохранении сопоставления полей с методами, используемыми для инициализации этих полей. Используя декоратор класса, сопоставление может быть сохранено с классом (где оно принадлежит имхо). При использовании другого декоратора для методов инициализации полей результирующий код выглядит довольно чистым и читаемым.
Есть предложения по улучшению?
from dataclasses import dataclass
import inspect
def dataclass_greedy_init(cls):
"""Dataclass decorator that adds an 'init_from' class method to recursively initialize
all fields and fully initialize an instance of the class from a given subset of
fields specified as keyword arguments.
In order to achieve this, the class is searched for *field init methods*, i.e. static
methods decoarted with the 'init_field' decorator. A mapping from field names to these
methods is built and stored as an attribute of the class. The 'init_from' method looks
up appropriate methods given the set fields specified as keyword arguments in the
'init_from' class method. It initializes missing fields recursively in a greedy fashion,
i.e. it initializes the first missing field for which a field init method can be found
and all arguments to this field init method can be supplied.
"""
# Collect all field init methods
init_methods = inspect.getmembers(cls, lambda f: inspect.isfunction(f) and not inspect.ismethod(f) and hasattr(f, "init_field"))
# Create a mapping from field names to signatures (i.e. required fields)
# and field init methods.
cls.init_mapping = {}
for init_method_name, init_method in init_methods:
init_field = init_method.init_field
if not init_field in cls.init_mapping:
cls.init_mapping[init_field] = []
cls.init_mapping[init_field].append((inspect.signature(init_method), init_method))
# Add classmethod 'init_from'
def init_from(cls, **kwargs):
for field in cls.__dataclass_fields__:
if field not in kwargs and field in cls.init_mapping:
for init_method_sig, init_method in cls.init_mapping[field]:
try:
mapped_kwargs = {p: kwargs[p] for p in init_method_sig.parameters if p in kwargs}
bound_args = init_method_sig.bind(**mapped_kwargs)
bound_args.apply_defaults()
kwargs[field] = init_method(**bound_args.arguments)
return cls.init_from(**kwargs)
except TypeError:
pass
return cls(**kwargs)
cls.init_from = classmethod(init_from)
return cls
def init_field(field_name):
"""Decorator to be used in combination with 'dataclass_greedy_init' to generate
static methods with an additional 'field_name' attribute that indicates for which
of the dataclass's fields this method should be used during initialization."""
def inner(func):
func.init_field = field_name
return staticmethod(func)
return inner
@dataclass_greedy_init
@dataclass
class foo(object):
a: float
b: float
c: float
d: float
@init_field("a")
def init_a_from_b_c(b,c):
return c-b
@init_field("b")
def init_b_from_a_c(a,c):
return c-a
@init_field("c")
def init_c_from_a_b(a,b):
return a b
@init_field("c")
def init_c_from_d(d):
return d/2
@init_field("d")
def init_d_from_a_b_c(a,b,c):
return a b c
@init_field("d")
def init_d_from_a(a):
return 6*a
print(foo.init_from(a=1, b=2))
print(foo.init_from(a=1, c=3))
print(foo.init_from(b=2, c=3))
print(foo.init_from(a=1))