Подтвердить что ты не робот

Как pythonically имеют частично взаимоисключающие необязательные аргументы?

В качестве простого примера возьмите class Ellipse, который может вернуть свои свойства, такие как площадь A, окружность C, основная/вспомогательная ось a/b, эксцентриситет e и т.д. Чтобы получить это, очевидно, что нужно предоставить ровно два из его параметров, чтобы получить все остальные, хотя в качестве специального случая, предусматривающего только один параметр, должен быть принят круг. Три или более согласованных параметра должны давать предупреждение, но работать, иначе явно возникает исключение.

Итак, некоторые примеры действительных Ellipse:

Ellipse(a=5, b=2)
Ellipse(A=3)
Ellipse(a=3, e=.1)
Ellipse(a=3, b=3, A=9*math.pi)  # note the consistency

в то время как недопустимыми будут

Ellipse()
Ellipse(a=3, b=3, A=7)

Таким образом, конструктор будет содержать либо много аргументов =None,

class Ellipse(object):
    def __init__(self, a=None, b=None, A=None, C=None, ...):

или, возможно, более разумный, простой **kwargs, возможно, добавив опцию для предоставления a,b в качестве позиционных аргументов,

class Ellipse(object):
    def __init__(self, a=None, b=None, **kwargs):
        kwargs.update({key: value
                       for key, value in (('a', a), ('b', b))
                       if value is not None})

До сих пор так хорошо. Но теперь идет фактическая реализация, то есть выяснение, какие параметры были предоставлены, а какие нет и определить все остальные в зависимости от них, или проверить согласованность, если требуется.

Мой первый подход был бы простой, но утомительной комбинацией многих

if 'a' in kwargs:
    a = kwargs['a']
    if 'b' in kwargs:
        b = kwargs['b']
        A = kwargs['A'] = math.pi * a * b
        f = kwargs['f'] = math.sqrt(a**2 - b**2)
        ...
    elif 'f' in kwargs:
        f = kwargs['f']
        b = kwargs['b'] = math.sqrt(a**2 + f**2)
        A = kwargs['A'] = math.pi * a * b
        ...
    elif ...

и т.д. *. Но нет ли лучшего способа? Или этот класс полностью блокируется, и я должен создавать конструкторы, такие как Ellipse.create_from_a_b(a, b), несмотря на то, что в принципе невозможна опция "предоставить три или более согласованных параметров"?

Бонусный вопрос: поскольку окружность эллипса включает в себя эллиптические интегралы (или эллиптические функции, если окружность предоставлена, а остальные параметры должны быть получены) которые не являются точно вычислительно тривиальными, должны ли эти вычисления фактически находиться в конструкторе или, скорее, быть помещены в @property Ellipse.C?


* Я предполагаю, что хотя бы одно улучшение читаемости будет всегда извлекать A и b и вычислять остальную часть из них, но это означает пересчет уже предоставленных значений, теряя время и точность...

4b9b3361

Ответ 1

Мое предложение сосредоточено на инкапсуляции данных и читаемости кода.

a) Выбрать пару для однозначных измерений для представления эллипса внутри

class Ellipse(object):
    def __init__(a, b):
        self.a = a
        self.b = b

b) Создайте семейство свойств для получения желаемых показателей о эллипсе

class Ellipse(object):
    @property
    def area(self):
        return math.pi * self._x * self._b

c) Создайте методы factory class/factory с однозначными именами:

class Ellipse(object):
    @classmethod
    def fromAreaAndCircumference(cls, area, circumference):
        # convert area and circumference to common format
        return cls(a, b)

Использование образца:

ellipse = Ellipse.fromLongAxisAndEccentricity(axis, eccentricity)
assert ellipse.a == axis
assert ellipse.eccentricity == eccentricity

Ответ 2

  • Убедитесь, что у вас достаточно параметров
  • Вычислить a из каждого спаривания других параметров
  • Подтвердить, что каждый a является тем же самым
  • Вычислить b из каждого спаривания a и другого параметра
  • Вычислите другие параметры из a и b

Здесь сокращенная версия с просто a, b, e и f, которая легко распространяется на другие параметры:

class Ellipse():
    def __init__(self, a=None, b=None, e=None, f=None):
        if [a, b, e, f].count(None) > 2:
            raise Exception('Not enough parameters to make an ellipse')
        self.a, self.b, self.e, self.f = a, b, e, f
        self.calculate_a()
        for parameter in 'b', 'e', 'f':  # Allows any multi-character parameter names
            if self.__dict__[parameter] is None:
                Ellipse.__dict__['calculate_' + parameter](self)

    def calculate_a(self):
        """Calculate and compare a from every pair of other parameters

        :raises Exception: if the ellipse parameters are inconsistent
        """
        a_raw = 0 if self.a is None else self.a
        a_be = 0 if not all((self.b, self.e)) else self.b / math.sqrt(1 - self.e**2)
        a_bf = 0 if not all((self.b, self.f)) else math.sqrt(self.b**2 + self.f**2)
        a_ef = 0 if not all((self.e, self.f)) else self.f / self.e
        if len(set((a_raw, a_be, a_bf, a_ef)) - set((0,))) > 1:
            raise Exception('Inconsistent parameters')
        self.a = a_raw + a_be + a_bf + a_ef

    def calculate_b(self):
        """Calculate and compare b from every pair of a and another parameter"""
        b_ae = 0 if self.e is None else self.a * math.sqrt(1 - self.e**2)
        b_af = 0 if self.f is None else math.sqrt(self.a**2 - self.f**2)
        self.b = b_ae + b_af

    def calculate_e(self):
        """Calculate e from a and b"""
        self.e = math.sqrt(1 - (self.b / self.a)**2)

    def calculate_f(self):
        """Calculate f from a and b"""
        self.f = math.sqrt(self.a**2 - self.b**2)

Это довольно Pythonic, хотя использование __dict__ может и не быть. __dict__ путь меньше строк и менее повторяющийся, но вы можете сделать его более явным, разбив его на отдельные строки if self.b is None: self.calculate_b().

Я только кодировал e и f, но он расширялся. Просто подпишите e и f код с помощью уравнений для того, что вы хотите добавить (область, окружность и т.д.) Как функция a и b.

Я не включил ваш запрос для однопараметрических эллипсов, чтобы стать кругами, но это просто проверка в начале calculate_a для того, есть ли только один параметр, и в этом случае a должен быть установлен, чтобы сделать эллипс круга (b должен быть установлен, если только a):

def calculate_a(self):
    """..."""
    if [self.a, self.b, self.e, self.f].count(None) == 3:
        if self.a is None:
            # Set self.a to make a circle
        else:
            # Set self.b to make a circle
        return
    a_raw = ...

Ответ 3

Если такая функциональность нужна только для этого одного класса, я бы посоветовал пойти со вторым упомянутым вами решением, используя ответ Nsh.

В противном случае, если эта проблема возникает в количестве мест в вашем проекте, вот решение, с которым я столкнулся:

class YourClass(MutexInit):
    """First of all inherit the MutexInit class by..."""

    def __init__(self, **kwargs):
        """...calling its __init__ at the end of your own __init__. Then..."""
        super(YourClass, self).__init__(**kwargs)

    @sub_init
    def _init_foo_bar(self, foo, bar):
        """...just decorate each sub-init method with @sub_init"""
        self.baz = foo + bar

    @sub_init
    def _init_bar_baz(self, bar, baz):
        self.foo = bar - baz

Это сделает ваш код более читаемым, и вы скроете уродливые детали за этими декораторами, которые не требуют пояснений.

Примечание. Мы могли бы также устранить декоратор @sub_init, но я думаю, что это единственный законный способ отметить метод как суб-init. В противном случае вариантом было бы согласиться с тем, чтобы поставить префикс перед именем метода, скажем _init, но я думаю, что плохая идея.

Вот реализации:

import inspect


class MutexInit(object):
    def __init__(self, **kwargs):
        super(MutexInit, self).__init__()

        for arg in kwargs:
            setattr(self, arg, kwargs.get(arg))

        self._arg_method_dict = {}
        for attr_name in dir(self):
            attr = getattr(self, attr_name)
            if getattr(attr, "_isrequiredargsmethod", False):
                self._arg_method_dict[attr.args] = attr

        provided_args = tuple(sorted(
            [arg for arg in kwargs if kwargs[arg] is not None]))
        sub_init = self._arg_method_dict.get(provided_args, None)

        if sub_init:
            sub_init(**kwargs)
        else:
            raise AttributeError('Insufficient arguments')


def sub_init(func):
    args = sorted(inspect.getargspec(func)[0])
    self_arg = 'self'
    if self_arg in args:
        args.remove(self_arg)

    def wrapper(funcself, **kwargs):
        if len(kwargs) == len(args):
            for arg in args:
                if (arg not in kwargs) or (kwargs[arg] is None):
                    raise AttributeError
        else:
            raise AttributeError

        return func(funcself, **kwargs)
    wrapper._isrequiredargsmethod = True
    wrapper.args = tuple(args)

    return wrapper

Ответ 4

Здесь моя попытка. Если вы делаете это для некоторых конечных пользователей, вы можете пропустить. Что я сделал, вероятно, хорошо работает для создания библиотеки быстрых математических объектов, но только тогда, когда пользователь знает, что происходит.

Идея заключалась в том, что все переменные, описывающие математический объект, следуют одному и тому же шаблону: a = something * smntng.

Итак, при вычислении переменной irl, в худшем случае я бы отсутствовал "что-то", тогда я бы пошел и вычислил это значение и любые значения, которые мне не хватало при расчете этого, и верну его обратно закончив вычисление исходной переменной, которую я искал. Там заметна определенная рекурсивная картина.

При вычислении переменной поэтому, при каждом доступе переменной, я должен проверить, существует ли она, и если она не вычисляет ее. Поскольку при каждом доступе я должен использовать __getattribute__.

Мне также нужна функциональная связь между переменными. Поэтому я привяжу атрибут класса relations, который будет служить именно этой цели. Это будет переменная и соответствующая функция.

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

Итак, теперь это похоже на то, что мы получим совпадение пинг-понга полурекурсии, где функция _calc вызовет __getattribute__, которая снова вызовет функцию _calc. До этого времени у нас заканчиваются переменные или мы действительно что-то вычисляем.

Хорошее:

  • Нет if s
  • Может инициализироваться с помощью разных переменных init. Пока отправляемые переменные позволяют вычислять другие.
  • Он довольно общий и выглядит так, как будто он может работать для любого другого математического объекта, описываемого аналогичным образом.
  • После расчета все ваши переменные будут запомнены.

Плохой:

  • Это довольно "нерепутичный" для того, что означает это слово (явное всегда лучше).
  • Не подходит для пользователей. Любое сообщение об ошибке, которое вы получите, будет равно количеству вызовов __getattribute__ и _calc. Также нет красивого способа разработки довольно печатной ошибки.
  • У вас проблема согласованности. Это, вероятно, можно решить, переопределив сеттеры.
  • В зависимости от исходных параметров существует вероятность, что вам придется долго ждать, чтобы вычислить определенную переменную, особенно если вычисление запрашиваемой переменной должно пройти несколько других вычислений.
  • Если вам нужна сложная функция, вы должны убедиться, что она была объявлена ​​до relations, которая может сделать код уродливым (также см. последнюю точку). Я не мог понять, как заставить их быть методами экземпляра, а не методами класса или другими более глобальными функциями, потому что я в основном переопределял оператор ..
  • Циркулярные функциональные зависимости также вызывают озабоченность. (a требуется b, которому требуется e, который нуждается в a снова и в бесконечном цикле).
  • relations заданы в типе dict. Это означает, что здесь может быть только одна функциональная зависимость, которая может иметь имя переменной, что не обязательно верно в математических терминах.
  • Это уже уродливо: value = self.relations[var]["func"]( *[self.__getattribute__(x) for x in requirements["req"]] )

Кроме того, строка в _calc, которая вызывает __getattribute__, которая либо вызывает _calc снова, либо если переменная существует, возвращает значение. Также в каждом __init__ вы должны установить все свои атрибуты None, потому что иначе будет вызываться _getattr.

def cmplx_func_A(e, C):
    return 10*C*e

class Elipse():
    def __init__(self, a=None, b=None, **kwargs):
        self.relations = {
        "e": {"req":["a", "b"], "func": lambda a,b: a+b},
        "C": {"req":["e", "a"], "func": lambda e,a: e*1/(a*b)},
        "A": {"req":["C", "e"], "func": lambda e,C: cmplx_func_A(e, C)},
        "a": {"req":["e", "b"], "func": lambda e,b: e/b},
        "b": {"req":["e", "a"], "func": lambda e,a: e/a}
                   }
        self.a = a
        self.b = b
        self.e = None
        self.C = None
        self.A = None
        if kwargs:
            for key in kwargs:
                setattr(self, key, kwargs[key])

    def __getattribute__(self, attr):
        val = super(Elipse, self).__getattribute__(attr)
        if val: return val
        return self._calc(attr)

    def _calc(self, var):
        requirements = self.relations[var]
        value = self.relations[var]["func"](
            *[self.__getattribute__(x) for x in requirements["req"]]
            )
        setattr(self, var, value)
        return value

Oputput:

>>> a = Elipse(1,1)
>>> a.A #cal to calculate this will fall through
        #and calculate every variable A depends on (C and e)
20
>>> a.C #C is not calculated this time.
1 
>>> a = Elipse(1,1, e=3)
>>> a.e #without a __setattribute__ checking the validity, there is no 
3       #insurance that this makes sense.
>>> a.A #calculates this and a.C, but doesn't recalc a.e
30
>>> a.e
3
>>> a = Elipse(b=1, e=2) #init can be anything that makes sense
>>> a.a                  #as it defined by relations dict.
2.0
>>> a = Elipse(a=2, e=2) 
>>> a.b
1.0

Здесь есть еще одна проблема, связанная со следующим последним пунктом в "плохом". То есть предположим, что мы можем определить elipse с C и a. Поскольку мы можем связывать каждую переменную с другими только с одной функциональной зависимостью, если вы определили переменные a и b поверх e и a|b, как я, вы не сможете их вычислить. Всегда будет по крайней мере некоторое миниатюрное подмножество переменных, которое вам нужно будет отправить. Это можно смягчить, убедившись, что вы определяете столько своих переменных, сколько мало других переменных, которые вы можете, но их нельзя избежать.

Если вы ленивы, это хороший способ коротко замыкать что-то, что вам нужно сделать быстро, но я бы не сделал этого где-нибудь, где я ожидаю, что кто-то еще сможет его использовать!

Ответ 5

Для бонусного вопроса, вероятно, разумно (в зависимости от вашего варианта использования) рассчитывать по запросу, но помните вычисленное значение, если оно было вычислено ранее. Например.

@property
def a(self):
    return self._calc_a()

def _calc_a(self):
    if self.a is None:
        self.a = ...?
    return self.a

Ответ 6

Ниже приведен подход, который я использовал ранее для частичной зависимости данных и кэширования результатов. Это на самом деле напоминает ответ @ljetibo со следующими существенными отличиями:

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

Я написал это с нуля, поэтому могут быть некоторые вещи, которые я пропустил, но он должен адекватно покрыть следующее:

  • Определить зависимости данных и отклонить инициализирующие данные, которые являются неадекватными
  • Загрузите результаты вычислений, чтобы избежать дополнительной работы.
  • Возвращает значимое исключение с именами переменных, которые не выводятся из указанной информации.

Конечно, это можно разбить на базовый класс для выполнения основной работы и подкласса, который определяет только основные отношения и вычисления. Разделение логики для расширенного сопоставления отношений из подкласса может быть интересной проблемой, хотя, поскольку отношения должны, по-видимому, быть указаны в подклассе.

Изменить: важно отметить, что эта реализация не отклоняет несогласованные инициализирующие данные (например, указывая a, b, c и A, чтобы они не выполняли взаимные выражения для вычисления). Предполагалось, что только минимальный набор значимых данных должен использоваться приложением. Требование от OP может быть принудительно введено без особых проблем с помощью оценки времени выполнения последовательности между предоставленными kwargs.

import itertools


class Foo(object):
    # Define the base set of dependencies
    relationships = {
        ("a", "b", "c"): "A",
        ("c", "d"): "B",
    }

    # Forumulate inverse relationships from the base set
    # This is a little wasteful but gives cheap dependency set lookup at
    # runtime
    for deps, target in relationships.items():
        deps = set(deps)
        for dep in deps:
            alt_deps = deps ^ set([dep, target])
            relationships[tuple(alt_deps)] = dep

    def __init__(self, **kwargs):
        available = set(kwargs)
        derivable = set()
        # Run through the permutations of available variables to work out what
        # other variables are derivable given the dependency relationships
        # defined above
        while True:
            for r in range(1, len(available) + 1):
                for permutation in itertools.permutations(available, r):
                    if permutation in self.relationships:
                        derivable.add(self.relationships[permutation])
            if derivable.issubset(available):
                # If the derivable set adds nothing to what is already noted as
                # available, that all we can get
                break
            else:
                available |= derivable

        # If any of the variables are underivable, raise an exception
        underivable = set(self.relationships.values()) - available
        if len(underivable) > 0:
            raise TypeError(
                "The following properties cannot be derived:\n\t{0}"
                .format(tuple(underivable))
            )
        # Store the kwargs in a mapping where we'll also cache other values as
        # are calculated
        self._value_dict = kwargs

    def __getattribute__(self, name):
        # Try to collect the value from the stored value mapping or fall back
        # to the method which calculates it below
        try:
            return super(Foo, self).__getattribute__("_value_dict")[name]
        except (AttributeError, KeyError):
            return super(Foo, self).__getattribute__(name)

    # This is left hidden but not treated as a staticmethod since it needs to
    # be run at definition time
    def __storable_property(getter):
        name = getter.__name__

        def storing_getter(inst):
            # Calculates the value using the defined getter and save it
            value = getter(inst)
            inst._value_dict[name] = value
            return value

        def setter(inst, value):
        # Changes the stored value and invalidate saved values which depend
        # on it
            inst._value_dict[name] = value
            for deps, target in inst.relationships.items():
                if name in deps and target in inst._value_dict:
                    delattr(inst, target)

        def deleter(inst):
            # Delete the stored value
            del inst._value_dict[name]

        # Pass back a property wrapping the get/set/deleters
        return property(storing_getter, setter, deleter, getter.__doc__)

    ## Each variable must have a single defined calculation to get its value
    ## Decorate these with the __storable_property function
    @__storable_property
    def a(self):
        return self.A - self.b - self.c

    @__storable_property
    def b(self):
        return self.A - self.a - self.c

    @__storable_property
    def c(self):
        return self.A - self.a - self.b

    @__storable_property
    def d(self):
        return self.B / self.c

    @__storable_property
    def A(self):
        return self.a + self.b + self.c

    @__storable_property
    def B(self):
        return self.c * self.d


if __name__ == "__main__":
    f = Foo(a=1, b=2, A=6, d=10)
    print f.a, f.A, f.B
    f.d = 20
    print f.B

Ответ 7

Я бы проверял согласованность данных каждый раз, когда вы устанавливаете параметр.

import math
tol = 1e-9
class Ellipse(object):
    def __init__(self, a=None, b=None, A=None, a_b=None):
        self.a = self.b = self.A = self.a_b = None 
        self.set_short_axis(a)
        self.set_long_axis(b)
        self.set_area(A)
        self.set_maj_min_axis(a_b)

    def set_short_axis(self, a):
        self.a = a
        self.check()

    def set_long_axis(self, b):
        self.b = b
        self.check()

    def set_maj_min_axis(self, a_b):
        self.a_b = a_b
        self.check()

    def set_area(self, A):
        self.A = A
        self.check()

    def check(self):
        if self.a and self.b and self.A:
            if not math.fabs(self.A - self.a * self.b * math.pi) <= tol:
                raise Exception('A=a*b*pi does not check!')
        if self.a and self.b and self.a_b:
            if not math.fabs(self.a / float(self.b) - self.a_b) <= tol:
                raise Exception('a_b=a/b does not check!')

Основной:

e1 = Ellipse(a=3, b=3, a_b=1)
e2 = Ellipse(a=3, b=3, A=27)

Первый объект эллипса согласован; set_maj_min_axis(1) проходит отлично.

Второе - нет; set_area(27) выходит из строя, по крайней мере, в пределах указанного допуска 1е-9 и вызывает ошибку.

Изменить 1

Некоторые дополнительные строки необходимы для случаев, когда использование a, a_b и a использует значения check():

    if self.a and self.A and self.a_b:
        if not math.fabs(self.A - self.a **2 / self.a_b * math.pi) <= tol:
            raise Exception('A=a*a/a_b*pi does not check!')
    if self.b and self.A and self.a_b:
        if not math.fabs(self.A - self.b **2 * self.a_b * math.pi) <= tol:
            raise Exception('A=b*b*a_b*pi does not check!')

Main:

e3 = Ellipse(b=3.0, a_b=1.0, A=27) 

Возможно, более разумным способом было бы рассчитать self.b = self.a / float(self.a_b) непосредственно в методе set a_b. Поскольку вы сами определяете порядок заданных методов в конструкторе, это может быть более управляемым, чем писать десятки проверок.