Десятичный контекст Python для фиксированной точки

Я хочу вычислить с числами, имеющими 3 знака до и 2 знака после десятичной точки (конечно, 2 и 3 настраиваются). Думаю, проще всего будет пояснить на примерах:

0,01 и 999,99 - это нижний и верхний предел для положительных чисел. Конечно, есть также 0,00 и отрицательные числа от -999,99 до -0,01. Расстояние между каждыми двумя последовательными числами составляет 0,01.

7.80 + 1.20 должно быть 9.00, а 999.00 + 1.00 должно быть OverflowError. 0,20 * 0,40 должно быть 0,08, а 0,34 * 0,20 должно быть 0,07 (это может установить флаг, указывающий, что оно было округлено, но не должно вызывать никаких исключений). 0,34 * 0,01 должно быть 0,00 (то же условие, что и предыдущее).

Фактически, мне нужны «целые числа» от 0 до 99999, просто записанные с точкой после третьей цифры, уменьшенные в 100 раз при умножении и в 100 раз увеличенные при делении. Должно быть возможно найти контекст именно для этого, не так ли?

Проблема в том, что я не могу найти правильную настройку для Emin, Emax, Clamp и Prec, которые будут делать то, что я хочу. Например, я попытался установить Emin и Emax на 0, но это вызвало слишком много InvalidOperations. Единственное, что я знаю, это то, что округление должно быть ROUND_HALF_EVEN. :-)


person Veky    schedule 03.03.2015    source источник
comment
Поскольку точность всегда применяется к значащим цифрам, а не к цифрам до / после десятичной точки (поэтому шесть значащих цифр допускают и 12345.0, и 0.12345), я думаю, что лучшим способом было бы создать для этого собственный класс на основе int.   -  person Tim Pietzcker    schedule 03.03.2015
comment
Да, я думаю, что в моем примере prev должно быть 5. Проблема не в этом. Проблема в том, как убедить Decimal иметь фиксированную точку, а не плавающую? Я все еще отказываюсь верить, что десятичная дробь не может этого сделать. Слоган гласит: арифметика с десятичной и плавающей запятой. Что означает фиксированная точка, если не совсем это?   -  person Veky    schedule 03.03.2015
comment
Посмотрите в самое начало - сразу после названия. :-) Да, я знаю, что фиксированная точка - это в некотором роде мифический зверь, но иногда людям она действительно нужна. Было бы стыдно писать собственный класс, если десятичный может это сделать.   -  person Veky    schedule 03.03.2015
comment
@Veky: Вы когда-нибудь находили решение своей проблемы. Я также ищу то же самое, что вы упомянули в вопросе, но мне еще предстоит найти способ сделать это с помощью элегантного, быстрого и не подверженного ошибкам решения с использованием модуля decimal, несмотря на много дней, потраченных на эту проблему. .   -  person Gustavo Bezerra    schedule 22.06.2018
comment
Нет, и я совершенно уверен, что невозможно использовать только задокументированные функции. Я просто написал свой, делегируя Decimal все, что мог.   -  person Veky    schedule 22.06.2018


Ответы (1)


Из документации:

В. Если у меня есть допустимые двухзначные входные данные, как мне поддерживать этот инвариант во всем приложении?

A. Некоторые операции, такие как сложение, вычитание и умножение на целое число, автоматически сохраняют фиксированную точку. Другие операции, такие как деление и нецелочисленное умножение, изменят количество десятичных знаков и требуют выполнения шага quantize ():

>>> TWOPLACES = Decimal(10) ** -2   # same as Decimal('0.01')
>>> a = Decimal('102.72')           # Initial fixed-point values
>>> b = Decimal('3.17')
>>> a + b                           # Addition preserves fixed-point
Decimal('105.89')
>>> a - b
Decimal('99.55')
>>> a * 42                          # So does integer multiplication
Decimal('4314.24')
>>> (a * b).quantize(TWOPLACES)     # Must quantize non-integer multiplication
Decimal('325.62')
>>> (b / a).quantize(TWOPLACES)     # And quantize division
Decimal('0.03')

При разработке приложений с фиксированной точкой удобно определять функции для обработки шага quantize ():

>>> def mul(x, y, fp=TWOPLACES):
...     return (x * y).quantize(fp)
>>> def div(x, y, fp=TWOPLACES):
...     return (x / y).quantize(fp)    
>>> mul(a, b)                       # Automatically preserve fixed-point
Decimal('325.62')
>>> div(b, a)
Decimal('0.03')

Похоже, решение состоит в том, чтобы установить точность до 5 и Emax на 2 и использовать эти функции квантования.

con = decimal.getcontext()
con.prec = 5
con.Emax = 2
con.Emin = 0

try:
    Decimal(1) * 1000
except decimal.Overflow as e:
    print(e)
else:
    assert False

assert Decimal("0.99") * 1000 == Decimal("990.00")
assert div(Decimal(1), 3) == Decimal("0.33")

Создание десятичного класса с фиксированной запятой

Кажется, что на удивление легко изменить десятичный модуль, чтобы он стал фиксированной точкой (за счет потери десятичных знаков с плавающей запятой). Это связано с тем, что на класс Decimal ссылается глобальное имя в модуле decimal. Мы можем вставить наш класс, совместимый с вниз, и все будет работать нормально. Сначала вам нужно запретить python импортировать модуль C _decimal и заставить его использовать реализацию модуля decimal на чистом питоне (чтобы мы могли переопределить частный метод Decimal). Как только вы это сделаете, вам нужно переопределить только один метод - _fix. Он вызывается для каждого нового Decimal, которое создается, для которого возможно, что он может не соответствовать текущему десятичному контексту.

настройка модуля

# setup python to not import _decimal (c implementation of Decimal) if present
import sys

if "_decimal" in sys.modules or "decimal" in sys.modules:
    raise ImportError("fixedpointdecimal and the original decimal module do not work"
        " together")

import builtins
_original_import = __import__
def _import(name, *args, **kwargs):
    if name == "_decimal":
        raise ImportError
    return _original_import(name, *args, **kwargs)
builtins.__import__ = _import

# import pure-python implementation of decimal
import decimal

# clean up
builtins.__import__ = _original_import # restore original __import__
del sys, builtins, _original_import, _import # clean up namespace

основной десятичный класс

from decimal import *

class FixedPointDecimal(Decimal):

    def _fix(self, context):
        # always fit to 2dp
        return super()._fix(context)._rescale(-2, context.rounding)
        # use context to find number of decimal places to use
        # return super()._fix(context)._rescale(-context.decimal_places, context.rounding)

# setup decimal module to use FixedPointDecimal
decimal.Decimal = FixedPointDecimal
Decimal = FixedPointDecimal

тесты

getcontext().prec = 5
getcontext().Emax = 2
a = Decimal("0.34")
b = Decimal("0.20")
assert a * b == Decimal("0.07")

Использование настраиваемого контекста

Класс контекста используется для отслеживания используемых переменных, управляющих созданием новых десятичных знаков. Таким образом, каждая программа или даже поток смогут установить количество десятичных знаков, которое они хотят использовать для своих десятичных знаков. Изменение класса Context немного сложнее. Ниже приведен полный класс для создания совместимого Context.

class FixedPointContext(Context):

    def __init__(self, prec=None, rounding=None, Emin=None, Emax=None,
                       capitals=None, clamp=None, flags=None, traps=None,
                       _ignored_flags=None, decimal_places=None):
        super().__init__(prec, rounding, Emin, Emax, capitals, clamp, flags, 
                traps, _ignored_flags)
        try:
            dc = DefaultContext
        except NameError:
            pass
        self.decimal_places = decimal_places if decimal_places is not None else dc.decimal_places

    def __setattr__(self, name, value):
        if name == "decimal_places":
            object.__setattr__(self, name, value)
        else:
            super().__setattr__(name, value)

    def __reduce__(self):
        flags = [sig for sig, v in self.flags.items() if v]
        traps = [sig for sig, v in self.traps.items() if v]
        return (self.__class__,
                (self.prec, self.rounding, self.Emin, self.Emax,
                 self.capitals, self.clamp, flags, traps, self._ignored_flags,
                 self.decimal_places))

    def __repr__(self):
        """Show the current context."""
        s = []
        s.append('Context(prec=%(prec)d, rounding=%(rounding)s, '
                 'Emin=%(Emin)d, Emax=%(Emax)d, capitals=%(capitals)d, '
                 'clamp=%(clamp)d, decimal_places=%(decimal_places)d'
                 % vars(self))
        names = [f.__name__ for f, v in self.flags.items() if v]
        s.append('flags=[' + ', '.join(names) + ']')
        names = [t.__name__ for t, v in self.traps.items() if v]
        s.append('traps=[' + ', '.join(names) + ']')
        return ', '.join(s) + ')'

    def _shallow_copy(self):
        """Returns a shallow copy from self."""
        nc = Context(self.prec, self.rounding, self.Emin, self.Emax,
                     self.capitals, self.clamp, self.flags, self.traps,
                     self._ignored_flags, self.decimal_places)
        return nc

    def copy(self):
        """Returns a deep copy from self."""
        nc = Context(self.prec, self.rounding, self.Emin, self.Emax,
                     self.capitals, self.clamp,
                     self.flags.copy(), self.traps.copy(),
                     self._ignored_flags, self.decimal_places)
        return nc
    __copy__ = copy

# reinitialise default context
DefaultContext = FixedPointContext(decimal_places=2)

# copy changes over to decimal module
decimal.Context = FixedPointContext
decimal.DefaultContext = DefaultContext
Context = FixedPointContext

# test
decimal.getcontext().decimal_places = 1
decimal.getcontext().prec = 5
decimal.getcontext().Emax = 2
a = Decimal("0.34")
b = Decimal("0.20")
assert a * b == Decimal("0.1")
person Dunes    schedule 03.03.2015
comment
Хорошо, я боялся, что ответ будет примерно таким. Конечно, я видел этот FAQ, но я предполагал, что он предназначен только для людей, которым нужна смешанная арифметика, так как имитировать фиксированное внутри плавающее. Я не думал, что это канонический способ реализовать только фиксированную точку. Честно говоря, это некрасиво. Конечно, первое, что я сделаю, это создам подкласс, который будет определять * и /, чтобы делать именно то, что делают mul и div. Тем не менее, ваш ответ был полезен, потому что он показал мне асимметрию Emin и Emax (и я думаю, что 2 - это 3-1, а не количество десятичных знаков :). Вы знаете, может быть, как установить con.clamp, BTW? - person Veky; 03.03.2015
comment
Создать подкласс Decimal сложно. Вам придется либо создать подкласс каждого метода Decimal, который возвращает Decimal, а не только __mul__ и __div__. Кроме того, вам нужно будет быть осторожным с бинарными операциями, используя в вашей программе как Decimal, так и FixedPointDecimal. В зависимости от порядка операций результат может быть Decimal или FixedPointDecimal. В качестве альтернативы вы можете использовать decimal.Decmal = FixedPointDecimal, но это помешает вам использовать десятичные дроби с плавающей запятой. - person Dunes; 03.03.2015
comment
Я все это знаю. Вот почему я так расстроен этим. Людям, написавшим модуль decimal, было бы намного проще иметь еще один параметр в контексте (на самом деле я думаю, что просто другое значение для фиксированного значения поможет), что позволит использовать арифметику с фиксированной запятой. Что касается вашего последнего предложения, я бы с радостью отказался от десятичных дробей с плавающей запятой, но я не думаю, что переназначение десятичного числа. Десятичное число будет работать таким образом. import действительно копирует модуль, и я изменяю только свою копию, а не то, что decimal считает Decimal. - person Veky; 04.03.2015
comment
Я добавил код, который показывает, как легко и просто переопределить Decimal, чтобы сделать его фиксированной точкой. Надеюсь, это поможет. Кстати import не копирует модуль. Он проверяет, импортирован ли он уже, и просто присваивает ему имя в заданном пространстве имен. - person Dunes; 04.03.2015
comment
Извините, что не принял это раньше, я почему-то пропустил ваше более позднее обновление. Может быть, у вас есть какая-нибудь информация о настройке зажима? Я вижу, в вашем решении вы оставляете значение «Нет» ... это намеренно? - person Veky; 09.03.2015
comment
Извините, я действительно не знаю. В документации предлагается значение 1 для совместимости со спецификацией IEEE. Но все, что он, кажется, делает, это находит способы представить числа вне заданных показателей, изменяя значение коэффициента (умножая или деля на 10). Что из вашего вопроса, я думал, что не то, что вы хотели. - person Dunes; 09.03.2015
comment
Конечно. Чтобы объяснить, почему я был так одержим зажимом: вначале он казался именно тем, что мне нужно. Моя первоначальная мысль заключалась в том, чтобы поставить Emin = Emax и clip = 1, тем самым заставив все числа иметь один и тот же показатель степени - но теперь кажется, что я действительно хотел обратного: я думаю, clamp = -1 было бы фантастической настройкой для того, что вы сделали , если decimal когда-либо решит реализовать это. :-D - person Veky; 10.03.2015
comment
И да, я вижу, где была ошибка в моем предыдущем комментарии: чистые модули Python не копируются и действительно доступны для оперативной хирургии, но когда я это тестировал, модуль _decimal C был импортирован и, конечно же, настройка атрибутов не работать там (хотя было бы неплохо сообщение об ошибке вместо молчаливого отказа :). И мне более чем грустно, что пришлось отказаться от скорости во имя правильности (с использованием чистой реализации Python). И в-третьих, невероятно, что у них есть идеальный крючок (_fix вы нашли), но они решили не реализовывать с его помощью фиксированную точку. :-( - person Veky; 10.03.2015