best-doctor / import_me

Python library to simplify importing data from xls/xlsx
MIT License
11 stars 12 forks source link

ClassifierProcessor #16

Closed yakovistomin closed 4 years ago

yakovistomin commented 4 years ago

Добавить обработчик, позволяющий классифицировать данные

Пример:

processor = ClassifierProcessor(
    choices={
        'a': lambda x: 0 <= x <= 10,
        'b': lambda x: 10 <= x <= 100,
        'c': lambda x: isinstance(x, str),
    })

assert processor(3) == 'a'
assert processor('test') == 'c'
dodgyturtle commented 4 years ago

Hello. Есть понимание какие классы должны быть? Хочу попробовать реализовать.

yakovistomin commented 4 years ago

Я думал сделать это по аналогии с ChoiceProcessor.

1) Есть необязательный параметр raw_value_processor (в данном случае raw_value_processor=lambda x: x) Этот обработчик отвечает за обработку сырых значений и приведение их к определенному типу

Пример без указания raw_value_processor:

processor = ClassifierProcessor(
    choices={
        'd': lambda x: x in ['test', 'test2'],
        'a': lambda x: 0 <= x <= 10,
        'b': lambda x: 10 <= x <= 100,
        'c': lambda x: isinstance(x, str),
    })

assert processor(3) == 'a'
assert processor('4') == 'c'
with pytest.raises(ColumnError):
        processor(-10)
assert processor('test') == 'd'

Пример с указанием raw_value_processor для int:

processor = ClassifierProcessor(
    choices={
        'a': lambda x: 0 <= x <= 10,
        'b': lambda x: 10 <= x <= 100,
    },
    raw_value_processor=IntegerProcessor(),
)

assert processor(3) == 'a'
assert processor('4') == 'a'
with pytest.raises(ColumnError):
        processor(-10)

Пример с указанием raw_value_processor для datetime:

processor = ClassifierProcessor(
    choices={
        'a': lambda x: datetime.date(2020, 1, 1) <= x <= datetime.date(2020, 12, 31),
        'b': lambda x: datetime.date(2021, 1, 1) <= x <= datetime.date(2021, 12, 31),
    },
    raw_value_processor=DateProcessor(),
)

assert processor('2020-01-01') == 'a'
assert processor('2021-01-01') == 'b'
with pytest.raises(ColumnError):
        processor('2022-01-01')

2) Как должно работать - в process_value перебираем a) choice_value, func for value, func in choices.items() b) вызываем func(value), если функция вернула True - process_value возвращает choice_value, если функция вернула False или исключение, то пробуем следующую пару с) если перебрали все choices, но ни одна из функций не вернула True, то raise ColumnError('unknown value')

Как бы мог выглядеть process_value

class ClassifierProcessor(...):
    def process_value(self, value):
        value = self.raw_value_processor(value)
        for  item, func in self.choices.items():
            try:
                if func(value):
                    return item
            except Exception:
                pass
        raise ColumnError('unknown value')
dodgyturtle commented 4 years ago
  1. IntegerProcessor(), DateProcessor() и т.д. это существующие процессоры из proccessors.py? Идея в том, чтобы подставить любой?
  2. choices формирует пользователь при использовании?
yakovistomin commented 4 years ago
  1. Да, можно подставить любой существующий или написанный отдельно. Я в примерах указал существующие
  2. Да. Я думал сделать именно так. Есть идеи это сделать как-то логичнее?
yakovistomin commented 4 years ago

1 - raw_value_processor по умолчанию - lambda x: x (ChoiceProcessor нужно переделать, чтобы по умолчанию было так же)

dodgyturtle commented 4 years ago
  1. Да, можно подставить любой существующий или написанный отдельно. Я в примерах указал существующие
  2. Да. Я думал сделать именно так. Есть идеи это сделать как-то логичнее?
  1. Принято. Я думал, это вообще готовые тесты:)
  2. Подумаю. Мысли есть, но как сформулировать, а главное реализовать, пока не очень понятно.
yakovistomin commented 4 years ago

@dumbturtle @Melevir я вот еще подумал, что может быть нам не достаточно просто словаря choices, где key - это значение после классификации.

Может лучше как-то так определять обработчик:

processor = ClassifierProcessor(
    choices=[
        ['a', lambda x: x < 10],
        ['b', lambda x: x < 100],
        ['a', lambda x: x < 1000],
        ['b', lambda x: x < 10000],
    ],
)

assert processor(1) == 'a'
assert processor(11) == 'b'
assert processor(101) == 'a'
assert processor(1001) == 'b'

таким образом можно было бы вообще любую логику описать

yakovistomin commented 4 years ago

Тогда можно было бы вообще универсальный ChoiceProcessor сделать

processor = ChoiceProcessor(
    choices=[
        ['A', 'a'],
        ['B', 'a'],
        [lambda x: x < 10, 'a'],
        [lambda x: x < 100, 'b'],
        [lambda x: x < 1000, 'a'],
        [lambda x: x < 10000, 'b'],
    ],
)

assert processor('A') == 'a'
assert processor('B') == 'b'
assert processor(1) == 'a'
assert processor(11) == 'b'
assert processor(101) == 'a'
assert processor(1001) == 'b'

а в коде просто перебирать choices и смотреть callable item[0] или нет.

def process_value(self, value):
    ...
    for item in self.choices:
        if callable(item[0]) and item[0](value):
            return item[1]
        elif item[0] == value:
            return item[1]
    raise ColumnError('unknown value')
dodgyturtle commented 4 years ago

Да, по мне со списком лучше. Можно начать с этого, потом дописать проверку, чтобы не повторять lamba и в choices указывать только услования.