Tout d'abord, la fonction, pour ceux qui veulent juste du code copier-coller:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '{}'.format(f)
if 'e' in s or 'E' in s:
return '{0:.{1}f}'.format(f, n)
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Ceci est valable dans Python 2.7 et 3.1+. Pour les versions plus anciennes, il n'est pas possible d'obtenir le même effet «d'arrondi intelligent» (du moins, non sans beaucoup de code compliqué), mais arrondir à 12 décimales avant la troncature fonctionnera la plupart du temps:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '%.12f' % f
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Explication
Le cœur de la méthode sous-jacente consiste à convertir la valeur en une chaîne avec une précision maximale, puis à couper tout ce qui dépasse le nombre de caractères souhaité. La dernière étape est facile; cela peut être fait soit avec une manipulation de chaîne
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
ou le decimal
module
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
La première étape, la conversion en chaîne, est assez difficile car il existe des paires de littéraux à virgule flottante (c'est-à-dire ce que vous écrivez dans le code source) qui produisent à la fois la même représentation binaire et qui doivent cependant être tronquées différemment. Par exemple, considérons 0,3 et 0,29999999999999998. Si vous écrivez 0.3
dans un programme Python, le compilateur l'encode en utilisant le format à virgule flottante IEEE dans la séquence de bits (en supposant un flottant de 64 bits)
0011111111010011001100110011001100110011001100110011001100110011
Il s'agit de la valeur la plus proche de 0,3 qui peut être représentée avec précision par un flottant IEEE. Mais si vous écrivez 0.29999999999999998
dans un programme Python, le compilateur le traduit exactement dans la même valeur . Dans un cas, vous vouliez qu'il soit tronqué (à un chiffre) comme 0.3
, alors que dans l'autre cas, vous vouliez qu'il soit tronqué comme 0.2
, mais Python ne peut donner qu'une seule réponse. C'est une limitation fondamentale de Python, ou en fait de tout langage de programmation sans évaluation paresseuse. La fonction de troncature n'a accès qu'à la valeur binaire stockée dans la mémoire de l'ordinateur, et non à la chaîne que vous avez réellement tapée dans le code source. 1
Si vous décodez la séquence de bits en un nombre décimal, à nouveau en utilisant le format à virgule flottante IEEE 64 bits, vous obtenez
0.2999999999999999888977697537484345957637...
donc une implémentation naïve arriverait 0.2
même si ce n'est probablement pas ce que vous voulez. Pour plus d'informations sur l'erreur de représentation en virgule flottante, consultez le didacticiel Python .
Il est très rare de travailler avec une valeur à virgule flottante qui est si proche d'un nombre rond et qui n'est pas intentionnellement égale à ce nombre rond. Ainsi, lors de la troncature, il est probablement logique de choisir la "plus belle" représentation décimale parmi tout ce qui pourrait correspondre à la valeur en mémoire. Python 2.7 et plus (mais pas 3.0) inclut un algorithme sophistiqué pour faire exactement cela , auquel nous pouvons accéder via l'opération de formatage de chaîne par défaut.
'{}'.format(f)
La seule mise en garde est que cela agit comme une g
spécification de format, dans le sens où il utilise la notation exponentielle ( 1.23e+4
) si le nombre est suffisamment grand ou petit. La méthode doit donc saisir ce cas et le gérer différemment. Il y a quelques cas où l'utilisation d'une f
spécification de format à la place pose un problème, comme essayer de tronquer 3e-10
à 28 chiffres de précision (cela produit 0.0000000002999999999999999980
), et je ne suis pas encore sûr de la meilleure façon de les gérer.
Si vous réellement êtes travaillez avec float
s qui sont très proches de chiffres ronds , mais intentionnellement ne les égale à (comme ,29999999999999998 ou 99,959999999999994), cela produira des faux positifs, à savoir qu'il y aura des chiffres ronds que vous ne vouliez pas arrondi. Dans ce cas, la solution est de spécifier une précision fixe.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Le nombre de chiffres de précision à utiliser ici n'a pas vraiment d'importance, il doit seulement être suffisamment grand pour s'assurer que tout arrondi effectué dans la conversion de chaîne ne "augmente" pas la valeur à sa belle représentation décimale. Je pense que cela sys.float_info.dig + n + 2
peut suffire dans tous les cas, mais sinon, il faudra 2
peut-être augmenter ce montant, et cela ne fait pas de mal de le faire.
Dans les versions antérieures de Python (jusqu'à 2.6 ou 3.0), le formatage des nombres à virgule flottante était beaucoup plus grossier et produisait régulièrement des choses comme
>>> 1.1
1.1000000000000001
Si tel est votre cas, si vous ne voulez utiliser des représentations décimales « BELLES » pour tronquer, tout ce que vous pouvez faire (pour autant que je sache) est de choisir un certain nombre de chiffres, moins représentable pleine de précision par un float
, et autour de la nombre à autant de chiffres avant de le tronquer. Un choix typique est 12,
'%.12f' % f
mais vous pouvez l'ajuster en fonction des nombres que vous utilisez.
1 Eh bien ... j'ai menti. Techniquement, vous pouvez demander à Python de ré-analyser son propre code source et d'extraire la partie correspondant au premier argument que vous passez à la fonction de troncature. Si cet argument est un littéral à virgule flottante, vous pouvez simplement le couper un certain nombre de places après la virgule décimale et le renvoyer. Cependant, cette stratégie ne fonctionne pas si l'argument est une variable, ce qui la rend assez inutile. Ce qui suit est présenté à titre de divertissement uniquement:
def trunc_introspect(f, n):
'''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
current_frame = None
caller_frame = None
s = inspect.stack()
try:
current_frame = s[0]
caller_frame = s[1]
gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
for token_type, token_string, _, _, _ in gen:
if token_type == tokenize.NAME and token_string == current_frame[3]:
next(gen) # left parenthesis
token_type, token_string, _, _, _ = next(gen) # float literal
if token_type == tokenize.NUMBER:
try:
cut_point = token_string.index('.') + n + 1
except ValueError: # no decimal in string
return token_string + '.' + '0' * n
else:
if len(token_string) < cut_point:
token_string += '0' * (cut_point - len(token_string))
return token_string[:cut_point]
else:
raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
break
finally:
del s, current_frame, caller_frame
Généraliser cela pour gérer le cas où vous passez une variable semble être une cause perdue, car vous devrez remonter en arrière tout au long de l'exécution du programme jusqu'à ce que vous trouviez le littéral à virgule flottante qui a donné sa valeur à la variable. S'il y en a même un. La plupart des variables seront initialisées à partir de l'entrée utilisateur ou d'expressions mathématiques, auquel cas la représentation binaire est tout ce qu'il y a.