Декоратор для изменения поведения функции - PullRequest
0 голосов
/ 27 февраля 2019

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

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

def is_close(a, b, rel_tol=1e-09, abs_tol=0.0):
    return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)


def _equal(val_a, val_b):
"""Wrapper for equality test to send in place of is_close."""
    return val_a == val_b

    @staticmethod
def get_equivalence(obj_a, obj_b, check_name=True, exact_match=False):
    equivalence_func = is_close
    if exact_match:
        # If we're looking for an exact match, changing the function we use to the equality tester.
        equivalence_func = _equal

    if check_name:
        return obj_a.name == obj_b.name

    # Check minimum resolutions if they are specified
    if 'min_res' in obj_a and 'min_res' in obj_b and not equivalence_func(obj_a['min_res'], obj_b['min_res']):
        return False

    return False

Как видите, стандартная процедура заставляет нас использовать функцию is_close, когда нам не нужно точное соответствие, но мы меняем вызов функции, когда мыделать.Теперь другая функция нуждается в той же логике, заменяя функцию.Есть ли способ использовать декораторы или что-то подобное для обработки логики такого типа, когда я знаю, что может потребоваться заменить конкретный вызов функции?

Ответы [ 2 ]

0 голосов
/ 27 февраля 2019

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

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

class EquivalenceUtil(object):
    def __init__(self, equal_comparator=_equals, inexact_comparator=is_close):
        self.equals = equal_comparator
        self.default_comparator = inexact_comparator

    def check_equivalence(self, obj_a, obj_b, exact_match=False, **kwargs):
        return self.equals(obj_a, obj_b, **kwargs) if exact_match else self.default_comparator(obj_a, obj_b, **kwargs)

Это простой класс, который можно использовать так:

class BBOX(object):
    _equivalence = EquivalenceUtil()

    def __init__(self, **kwargs):
        ...

    @classmethod
    def are_equivalent(cls, bbox_a, bbox_b, exact_match=False):
        """Test for equivalence between two BBOX's."""
        bbox_list = bbox_a.as_list
        other_list = bbox_b.as_list
        for _index in range(0, 3):
            if not cls._equivalence.check_equivalence(bbox_list[_index], 
                                                      other_list[_index], 
                                                      exact_match=exact_match):
            return False
        return True

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

В моем исходном примере код может выглядеть так:

class TileGrid(object):

    def __init__(self, **kwargs):
        ...

    @staticmethod
    def are_equivalent(grid1, grid2, check_name=False, exact_match=False):
        if check_name:
            return grid1.name == grid2.name
        # Check minimum resolutions if they are specified
        if 'min_res' in grid1 and 'min_res' in grid2 and not cls._equivalence.check_equivalence(grid1['min_res'], grid2['min_res'], exact_match=exact_match):
            return False

        # Compare the bounding boxes of the two grids if they exist in the grid
        if 'bbox' in grid1 and 'bbox' in grid2:
            return BBOX.are_equivalent(grid1.bbox, grid2.bbox, exact_mach=exact_match)

        return False

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

0 голосов
/ 27 февраля 2019

Декоратор не требуется;просто передайте желаемую функцию в качестве аргумента get_equivalence (который теперь немного больше, чем обертка, применяющая аргумент).

def make_eq_with_tolerance(rel_tol=1e-09, abs_tol=0.0):
    def _(a, b):
        return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
    return _    

# This is just operator.eq, by the way
def _equal(val_a, val_b-):
    return val_a == val_b

def same_name(a, b):
    return a.name == b.name

Теперь get_equivalence принимает три аргумента: два объекта для сравнения ифункция, которая вызывается по этим двум аргументам.

@staticmethod
def get_equivalence(obj_a, obj_b, equivalence_func):

    return equivalence_func(obj_a, obj_b)

Некоторые примеры вызовов:

get_equivalence(a, b, make_eq_with_tolerance())
get_equivalence(a, b, make_eq_with_tolerance(rel_tol=1e-12))  # Really tight tolerance
get_equivalence(a, b, _equal)
get_equivalence(a, b, same_name)
...