Проблема с форматированием JSON-дампа с плавающей запятой с помощью Python и Google App Engine

#python #json #google-app-engine #webapp2

#python #json #google-app-engine #webapp2

Вопрос:

В настоящее время я работаю с Python и создаю простой API в Google App Engine, который позволяет пользователю использовать глаголы GET, POST, DELETE и PUT, выполняя вызовы API с помощью командной строки. Я использую curl для обработки данных с помощью вызовов API. Мне удалось успешно добавить данные и отобразить их обратно, но проблема в том, что у меня есть атрибут для ‘price’, и эта цена хранится в хранилище данных GAE (Google App Engine) в виде числа с плавающей запятой, а число форматируется по мере отправки, но когда JSON показывает, что находится в хранилище данных, оно имеет другой формат. Например, при сохранении цены 8,99, ответ JSON показывает 8,9900000000000002. Я почти уверен, что это не проблема GAE, а проблема JSON.

Пример проблемы:

 curl --data "name=t-shirtamp;description=Star Trekamp;price=8.99amp;user=test1" -H "Accept: application/json" https://mywebsite.appspot.com/product
  

Правильно сохраняется в хранилище данных, но возвращает JSON следующим образом:

 {"description": "Star Trek", "price": 8.9900000000000002, "name": "t-shirt", "user": "test1", "key": 5206065687822336}
  

Вот мои models.py

 from google.appengine.ext import ndb

class Model(ndb.Model):
    def to_dict(self):
        d = super(Model, self).to_dict()
        d['key'] = self.key.id()
        return d

class User(Model):
    username = ndb.StringProperty(required=True)
    password = ndb.StringProperty(required=True)

class Product(Model):
    name = ndb.StringProperty(required=True)
    description = ndb.StringProperty(required=True)
    price = ndb.FloatProperty()
    color = ndb.StringProperty()
    size = ndb.StringProperty()
    user = ndb.StringProperty(required=True)

class Sales(Model):
    products = ndb.KeyProperty(repeated=True)
    datetime = ndb.DateTimeProperty(required=True)
    quantity = ndb.IntegerProperty(repeated=True)
    product_cost = ndb.FloatProperty(repeated=True)
    total_cost = ndb.FloatProperty()
    latitude = ndb.FloatProperty()
    longitude = ndb.FloatProperty()
    user = ndb.KeyProperty(required=True)


    def to_dict(self):
        d = super(Sales, self).to_dict()
        d['products'] = [i.id() for i in d['products']]
        return d 
  

Вот product.py:

 import webapp2
from google.appengine.ext import ndb
import models
import json

class Product(webapp2.RequestHandler):
    #Create a Product entity
    def post(self):
        if 'application/json' not in self.request.accept:
            self.response.status = 406
            self.response.status_message = 'Not acceptable, API only supports application/json MIME type.'
            return
        new_product = models.Product()
        name = self.request.get('name', default_value=None)
        description = self.request.get('description', default_value=None)
        price = self.request.get('price', default_value=0)
        color = self.request.get('color', default_value=None)
        size = self.request.get('size', default_value=None)
        user = self.request.get('user', default_value=None)

        if name:
            new_product.name = name
        else:
            self.response.status = 400
            self.response.status_message = 'Invalid request, name required'
        if description:
            new_product.description = description
        else:
            self.response.status = 400
            self.response.status_message = 'Invalid request, description required'
        if price:
            new_product.price = float(price)
        if color:
            new_product.color = color
        if size:
            new_product.size = size
        if user:
            new_product.user = user
        else:
            self.response.status = 400
            self.response.status_message = 'Invalid request, username required'
        key = new_product.put()
        out = new_product.to_dict()
        self.response.write(json.dumps(out))
        return

    #Return an Product entity
    def get(self, **kwargs):
        if 'application/json' not in self.request.accept:
            self.response.status = 406
            self.response.status_message = 'Not acceptable, API only supports application/json MIME type.'
            self.response.write(self.response.status_message)
            return
        #Return selected product details
        if 'id' in kwargs:
            out = ndb.Key(models.Product, int(kwargs['id'])).get().to_dict()
            self.response.write(json.dumps(out))
        #Return all product ids
        else:
            q = models.Product.query()
            keys = q.fetch(keys_only=False)
            results = {x.key.id() : x.to_dict() for x in keys}
            self.response.write(json.dumps(results))
  

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

Заранее благодарю вас!

Ответ №1:

Я не думаю, что это на самом деле проблема. Если вы хотите понять, почему существует это несоответствие, вы можете прочитать статью Википедии о числах с плавающей запятой. Проблема с TLDR заключается в том, что уже существует бесконечно много рациональных чисел от 0 до 1, но компьютер способен хранить только конечный объем данных. Более того, вы обычно хотите хранить действительные числа в пределах 32 (одинарных) или 64 (двойных) бит данных, чтобы обеспечить эффективное вычисление. Таким образом, номера компьютеров являются лишь подмножеством действительных чисел, и вам часто приходится округлять до ближайшего номера компьютера. Это не было бы проблемой, если бы точка всегда находилась в одном и том же месте, и вы использовали базовые числа 10, чтобы вы знали, что нет ошибки при округлении «8,99», но это не относится к обычным числам с плавающей запятой, как определено в IEEE 754.

В основном здесь выполняется 8.99 = 8.9900000000000002.

У вас есть несколько вариантов устранения проблемы:

1) Не устраняйте проблему и просто округляйте отображаемые значения

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

2) Используйте целочисленные значения [вероятно, лучший вариант здесь]

Вместо сохранения 8,99 вы сохраняете 899 как целое число в базе данных. В целочисленной арифметике нет проблем с округлением, если вы используете только сложение / вычитание.

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

3) Используйте числа с фиксированной точкой с правильным основанием

Если вы выполняете много арифметических операций с такими числами, возможно, было бы удобнее использовать библиотеку чисел с фиксированной запятой и в основном использовать специально разработанное представление рациональных чисел для вашей проблемы. Это в основном то же самое, что и вариант 2, но вы не реализуете все самостоятельно и обладаете большей гибкостью. Однако я лично выбрал бы вариант 2, поскольку я не знаю, существует ли такая библиотека, которую вы могли бы использовать как в Python, так и в Javascript, а также это может просто включать необязательную зависимость от проекта.