Existe-t-il un équivalent de range()
pour les flottants en Python?
>>> range(0.5,5,1.5)
[0, 1, 2, 3, 4]
>>> range(0.5,5,0.5)
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
range(0.5,5,0.5)
ValueError: range() step argument must not be zero
Je ne connais pas de fonction intégrée, mais en écrire une comme ceci ne devrait pas être trop compliqué.
def frange(x, y, jump):
while x < y:
yield x
x += jump
Comme le mentionnent les commentaires, cela pourrait produire des résultats imprévisibles tels que:
>>> list(frange(0, 100, 0.1))[-1]
99.9999999999986
Pour obtenir le résultat attendu, vous pouvez utiliser l'une des autres réponses à cette question ou, comme l'a mentionné @Tadhg, vous pouvez utiliser decimal.Decimal
comme argument jump
. Assurez-vous de l'initialiser avec une chaîne plutôt qu'un float.
>>> import decimal
>>> list(frange(0, 100, decimal.Decimal('0.1')))[-1]
Decimal('99.9')
Ou même:
import decimal
def drange(x, y, jump):
while x < y:
yield float(x)
x += decimal.Decimal(jump)
Et alors:
>>> list(drange(0, 100, '0.1'))[-1]
99.9
Vous pouvez soit utiliser:
[x / 10.0 for x in range(5, 50, 15)]
ou utilisez lambda/map:
map(lambda x: x/10.0, range(5, 50, 15))
J'avais l'habitude d'utiliser numpy.arange
, mais des complications ont été rencontrées pour contrôler le nombre d'éléments renvoyés, en raison d'erreurs en virgule flottante. Alors maintenant, j'utilise linspace
, par exemple:
>>> import numpy
>>> numpy.linspace(0, 10, num=4)
array([ 0. , 3.33333333, 6.66666667, 10. ])
Pylab a frange
(un wrapper, en fait, pour matplotlib.mlab.frange
):
>>> import pylab as pl
>>> pl.frange(0.5,5,0.5)
array([ 0.5, 1. , 1.5, 2. , 2.5, 3. , 3.5, 4. , 4.5, 5. ])
Eagerly évalué (2.x range
):
[x * .5 for x in range(10)]
Lazly évalué (2.x xrange
, 3.x range
):
itertools.imap(lambda x: x * .5, xrange(10)) # or range(10) as appropriate
Alternativement:
itertools.islice(itertools.imap(lambda x: x * .5, itertools.count()), 10)
# without applying the `islice`, we get an infinite stream of half-integers.
avec itertools
: plage de virgule flottante évaluée paresseusement
>>> from itertools import count, takewhile
>>> def frange(start, stop, step):
return takewhile(lambda x: x< stop, count(start, step))
>>> list(frange(0.5, 5, 1.5))
# [0.5, 2.0, 3.5]
J'ai aidé à ajouter la fonction numeric_range au paquet more-itertools .
more_itertools.numeric_range(start, stop, step)
agit comme la plage de fonctions intégrée, mais peut gérer les types float, Decimal et Fraction.
>>> from more_itertools import numeric_range
>>> Tuple(numeric_range(.1, 5, 1))
(0.1, 1.1, 2.1, 3.1, 4.1)
Il n’existe pas de fonction intégrée de ce type, mais vous pouvez utiliser les éléments suivants (code Python 3) pour effectuer le travail en toute sécurité, comme le permet Python.
from fractions import Fraction
def frange(start, stop, jump, end=False, via_str=False):
"""
Equivalent of Python 3 range for decimal numbers.
Notice that, because of arithmetic errors, it is safest to
pass the arguments as strings, so they can be interpreted to exact fractions.
>>> assert Fraction('1.1') - Fraction(11, 10) == 0.0
>>> assert Fraction( 0.1 ) - Fraction(1, 10) == Fraction(1, 180143985094819840)
Parameter `via_str` can be set to True to transform inputs in strings and then to fractions.
When inputs are all non-periodic (in base 10), even if decimal, this method is safe as long
as approximation happens beyond the decimal digits that Python uses for printing.
For example, in the case of 0.1, this is the case:
>>> assert str(0.1) == '0.1'
>>> assert '%.50f' % 0.1 == '0.10000000000000000555111512312578270211815834045410'
If you are not sure whether your decimal inputs all have this property, you are better off
passing them as strings. String representations can be in integer, decimal, exponential or
even fraction notation.
>>> assert list(frange(1, 100.0, '0.1', end=True))[-1] == 100.0
>>> assert list(frange(1.0, '100', '1/10', end=True))[-1] == 100.0
>>> assert list(frange('1', '100.0', '.1', end=True))[-1] == 100.0
>>> assert list(frange('1.0', 100, '1e-1', end=True))[-1] == 100.0
>>> assert list(frange(1, 100.0, 0.1, end=True))[-1] != 100.0
>>> assert list(frange(1, 100.0, 0.1, end=True, via_str=True))[-1] == 100.0
"""
if via_str:
start = str(start)
stop = str(stop)
jump = str(jump)
start = Fraction(start)
stop = Fraction(stop)
jump = Fraction(jump)
while start < stop:
yield float(start)
start += jump
if end and start == stop:
yield(float(start))
Vous pouvez tout vérifier en exécutant quelques assertions:
assert Fraction('1.1') - Fraction(11, 10) == 0.0
assert Fraction( 0.1 ) - Fraction(1, 10) == Fraction(1, 180143985094819840)
assert str(0.1) == '0.1'
assert '%.50f' % 0.1 == '0.10000000000000000555111512312578270211815834045410'
assert list(frange(1, 100.0, '0.1', end=True))[-1] == 100.0
assert list(frange(1.0, '100', '1/10', end=True))[-1] == 100.0
assert list(frange('1', '100.0', '.1', end=True))[-1] == 100.0
assert list(frange('1.0', 100, '1e-1', end=True))[-1] == 100.0
assert list(frange(1, 100.0, 0.1, end=True))[-1] != 100.0
assert list(frange(1, 100.0, 0.1, end=True, via_str=True))[-1] == 100.0
assert list(frange(2, 3, '1/6', end=True))[-1] == 3.0
assert list(frange(0, 100, '1/3', end=True))[-1] == 100.0
Code disponible sur GitHub
Une solution sans dépendances numériques, etc. a été fournie par Kichik, mais en raison de l'arithmétique à virgule flottante , elle se comporte souvent de manière inattendue. Comme indiqué par moi et blubberdiblub , d’autres éléments se glissent facilement dans le résultat. Par exemple, naive_frange(0.0, 1.0, 0.1)
donnerait 0.999...
comme dernière valeur et donnerait donc 11 valeurs au total.
Une version robuste est fournie ici:
def frange(x, y, jump=1.0):
'''Range for floats.'''
i = 0.0
x = float(x) # Prevent yielding integers.
x0 = x
epsilon = jump / 2.0
yield x # yield always first value
while x + epsilon < y:
i += 1.0
x = x0 + i * jump
yield x
En raison de la multiplication, les erreurs d'arrondi ne s'accumulent pas. L’utilisation de epsilon
permet d’éviter les erreurs d’arrondi de la multiplication, même si des problèmes peuvent bien sûr se poser à propos des très petites et des très grandes extrémités. Maintenant, comme prévu:
> a = list(frange(0.0, 1.0, 0.1))
> a[-1]
0.9
> len(a)
10
Et avec des nombres un peu plus grands:
> b = list(frange(0.0, 1000000.0, 0.1))
> b[-1]
999999.9
> len(b)
10000000
Le code est également disponible sous la forme un GitHub Gist .
Pourquoi n'y a-t-il aucune implémentation de plage à virgule flottante dans la bibliothèque standard?
Comme le montrent clairement tous les articles, il n’existe pas de version à virgule flottante de range()
. Cela dit, l’omission a du sens si l’on considère que la fonction range()
est souvent utilisée en tant qu’index (et bien sûr, cela signifie un générateur accessor). Ainsi, lorsque nous appelons range(0,40)
, nous disons en réalité que nous voulons 40 valeurs commençant de 0 à 40, mais non compris 40.
Lorsque nous considérons que la génération d'index concerne autant le nombre d'index que leurs valeurs, l'utilisation d'une implémentation flottante de range()
dans la bibliothèque standard est moins logique. Par exemple, si nous appelions la fonction frange(0, 10, 0.25)
, nous nous attendrions à ce que 0 et 10 soient inclus, mais cela produirait un vecteur avec 41 valeurs.
Ainsi, une fonction frange()
dépendant de son utilisation présentera toujours un comportement intuitif contre-intuitif; il contient soit trop de valeurs que celles perçues du point de vue de l'indexation, soit ne comprend pas un nombre qui devrait raisonnablement être renvoyé du point de vue mathématique.
Le cas d'utilisation mathématique
Cela dit, comme indiqué, numpy.linspace()
exécute bien la génération avec la perspective mathématique:
numpy.linspace(0, 10, 41)
array([ 0. , 0.25, 0.5 , 0.75, 1. , 1.25, 1.5 , 1.75,
2. , 2.25, 2.5 , 2.75, 3. , 3.25, 3.5 , 3.75,
4. , 4.25, 4.5 , 4.75, 5. , 5.25, 5.5 , 5.75,
6. , 6.25, 6.5 , 6.75, 7. , 7.25, 7.5 , 7.75,
8. , 8.25, 8.5 , 8.75, 9. , 9.25, 9.5 , 9.75, 10.
])
Le cas d'utilisation de l'indexation
Et pour la perspective d’indexation, j’ai écrit une approche légèrement différente avec une magie de chaîne astucieuse qui nous permet de spécifier le nombre de décimales.
# Float range function - string formatting method
def frange_S (start, stop, skip = 1.0, decimals = 2):
for i in range(int(start / skip), int(stop / skip)):
yield float(("%0." + str(decimals) + "f") % (i * skip))
De même, nous pouvons également utiliser la fonction round
intégrée et spécifier le nombre de décimales:
# Float range function - rounding method
def frange_R (start, stop, skip = 1.0, decimals = 2):
for i in range(int(start / skip), int(stop / skip)):
yield round(i * skip, ndigits = decimals)
Une comparaison rapide & Performance
Bien entendu, compte tenu de ce qui précède, ces fonctions ont un cas d'utilisation relativement limité. Néanmoins, voici une comparaison rapide:
def compare_methods (start, stop, skip):
string_test = frange_S(start, stop, skip)
round_test = frange_R(start, stop, skip)
for s, r in Zip(string_test, round_test):
print(s, r)
compare_methods(-2, 10, 1/3)
Les résultats sont identiques pour chacun:
-2.0 -2.0
-1.67 -1.67
-1.33 -1.33
-1.0 -1.0
-0.67 -0.67
-0.33 -0.33
0.0 0.0
...
8.0 8.0
8.33 8.33
8.67 8.67
9.0 9.0
9.33 9.33
9.67 9.67
Et quelques timings:
>>> import timeit
>>> setup = """
... def frange_s (start, stop, skip = 1.0, decimals = 2):
... for i in range(int(start / skip), int(stop / skip)):
... yield float(("%0." + str(decimals) + "f") % (i * skip))
... def frange_r (start, stop, skip = 1.0, decimals = 2):
... for i in range(int(start / skip), int(stop / skip)):
... yield round(i * skip, ndigits = decimals)
... start, stop, skip = -1, 8, 1/3
... """
>>> min(timeit.Timer('string_test = frange_s(start, stop, skip); [x for x in string_test]', setup=setup).repeat(30, 1000))
0.024284090992296115
>>> min(timeit.Timer('round_test = frange_r(start, stop, skip); [x for x in round_test]', setup=setup).repeat(30, 1000))
0.025324633985292166
On dirait que la méthode de formatage de chaîne gagne par un cheveu sur mon système.
Les limites
Et enfin, une démonstration du point de la discussion ci-dessus et une dernière limitation:
# "Missing" the last value (10.0)
for x in frange_R(0, 10, 0.25):
print(x)
0.25
0.5
0.75
1.0
...
9.0
9.25
9.5
9.75
De plus, lorsque le paramètre skip
n'est pas divisible par la valeur stop
, il peut y avoir un écart béant compte tenu de ce dernier problème:
# Clearly we know that 10 - 9.43 is equal to 0.57
for x in frange_R(0, 10, 3/7):
print(x)
0.0
0.43
0.86
1.29
...
8.14
8.57
9.0
9.43
Il existe des moyens de résoudre ce problème, mais au bout du compte, la meilleure approche serait probablement d'utiliser simplement Numpy.
Aw, heck - je vais jeter dans une version simple sans bibliothèque. N'hésitez pas à l'améliorer [*]:
def frange(start=0, stop=1, jump=0.1):
nsteps = int((stop-start)/jump)
dy = stop-start
# f(i) goes from start to stop as i goes from 0 to nsteps
return [start + float(i)*dy/nsteps for i in range(nsteps)]
L'idée de base est que nsteps
est le nombre d'étapes pour vous rendre du début à la fin et que range(nsteps)
émet toujours des entiers afin d'éviter toute perte de précision. La dernière étape consiste à mapper [0..nsteps] linéairement sur [start..stop].
Si, comme alancalvitti , vous souhaitez que la série ait une représentation rationnelle exacte, vous pouvez toujours utiliser Fractions :
from fractions import Fraction
def rrange(start=0, stop=1, jump=0.1):
nsteps = int((stop-start)/jump)
return [Fraction(i, nsteps) for i in range(nsteps)]
[*] En particulier, frange()
renvoie une liste et non un générateur. Mais cela suffisait à mes besoins.
j'ai écrit une fonction qui retourne un tuple d'une plage de nombres en virgule flottante en double précision, sans décimales, au-delà des centièmes. il s'agissait simplement d'analyser les valeurs d'intervalle telles que les chaînes et de séparer les valeurs en excès. Je l'utilise pour afficher des plages à sélectionner dans une interface utilisateur. J'espère que quelqu'un d'autre le trouvera utile.
def drange(start,stop,step):
double_value_range = []
while start<stop:
a = str(start)
a.split('.')[1].split('0')[0]
start = float(str(a))
double_value_range.append(start)
start = start+step
double_value_range_Tuple = Tuple(double_value_range)
#print double_value_range_Tuple
return double_value_range_Tuple
Il y a ici plusieurs réponses qui ne gèrent pas les cas simples, comme les étapes négatives, le démarrage incorrect, l'arrêt, etc. Voici la version qui gère bon nombre de ces cas, donnant le même comportement que range()
:
def frange(start, stop=None, step=1):
if stop is None:
start, stop = 0, start
steps = int((stop-start)/step)
for i in range(steps):
yield start
start += step
Notez que ceci provoquerait une erreur step = 0, exactement comme range
en natif. Une différence est que la plage native retourne un objet indexable et réversible, alors que la précédente ne le fait pas.
Vous pouvez jouer avec ce code et les cas de test ici.