range()函数是否适用于浮点数?

214

在Python中是否有适用于浮点数的range()等效函数?

>>> 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

2
这些不是分数,而是浮点数。浮点数可能会产生与您预期不同的结果。 - user395760
8
一个快速的解决方法是将整数视为小数,例如:range(5, 50, 5),然后只需将每个数字除以10。 - NullUserException
@delnan - 已更新。为了拥有浮点范围的便利,我愿意接受微小的不准确性。 - Jonathan Livni
2
可能是Python decimal range() step value的重复问题。 - Jonathan Livni
你可以从Python decimal range() step value问题以及它所涉及的frange(), a range function with float increments (ActiveState Code)中找到更多关于这个问题的好坏答案的深入见解。 - nealmcb
显示剩余4条评论
24个回答

156

您可以使用以下方法之一:

[x / 10.0 for x in range(5, 50, 15)]

或者使用 lambda / map:

map(lambda x: x/10.0, range(5, 50, 15))

1
并且数组(range(5,50,15)) / 10.0作为numpy数组具有处理除法、乘法等运算符。 - edvaldig
3
@edvaldig:你说得对,我不知道这个……尽管如此,我认为arange(0.5, 5, 1.5)更易读。 - Grzegorz Rożniecki
2
我更喜欢这个答案而不是被接受的那个,因为前两个解决方案是基于迭代整数并从整数中推导出最终浮点数。这更加健壮。如果您直接使用浮点数进行操作,则由于浮点数在内部的表示方式,可能会出现奇怪的一次性错误。例如,如果您尝试 list(frange(0, 1, 0.5)),它可以正常工作并且1被排除在外,但是如果您尝试 list(frange(0, 1, 0.1)),则您得到的最后一个值接近于1.0,这可能不是您想要的结果。这里提供的解决方案没有这个问题。 - blubberdiblub
5
永远不要使用numpy.arange(即使numpy文档本身也不建议这样做)。使用wim推荐的numpy.linspace,或者在此答案中提供的其他建议之一。 - benrg

112
我不知道有没有内置函数可以用,但是像[这样](https://dev59.com/oHRB5IYBdhLWcg3w4bKv#477610)编写一个函数应该不会太复杂。
def frange(x, y, jump):
  while x < y:
    yield x
    x += jump

正如评论所提到的,这可能会产生不可预测的结果,例如:

>>> list(frange(0, 100, 0.1))[-1]
99.9999999999986
为了得到预期的结果,您可以使用本问题中的其他答案之一,或者如@Tadhg所提到的,您可以将decimal.Decimal作为jump参数使用。请确保用字符串而不是浮点数进行初始化。
>>> import decimal
>>> list(frange(0, 100, decimal.Decimal('0.1')))[-1]
Decimal('99.9')

甚至可以这样:

import decimal

def drange(x, y, jump):
  while x < y:
    yield float(x)
    x += decimal.Decimal(jump)

然后:

>>> list(drange(0, 100, '0.1'))[-1]
99.9

[编辑说明:如果您只使用正的jump和整数xy开始和结束,那么这个方法可以正常工作。如需更通用的解决方案,请参见此处。]


38
Python的格言实际上是“做一件事应该有一种——最好只有一种——显而易见的方法”(http://www.python.org/dev/peps/pep-0020/)。但Python仍然非常棒 :) - Jonathan Livni
3
>>> print list(frange(0,100,0.1))[-1]==100.0 will be False - Volodimir Kopey
1
frange 可能会出现意外情况。由于浮点数算术的诅咒,例如 frange(0.0, 1.0, 0.1) 会产生11个值,其中最后一个值是 0.9999999999999999。一个实用的改进方法是 while x + sys.float_info.epsilon < y:,尽管即使这种方法在处理大数时也可能失败 - Akseli Palén
15
请不要在可能影响我的生命的软件中使用此代码。无法保证其可靠性。也请不要使用Akseli Palén的答案。请使用Xaerxess或wim的答案(但忽略有关arange部分的内容)。 - benrg
4
如果你使用 decimal.Decimal 作为步长,而不是浮点数,那么这个方法会非常有效。 - Tadhg McDonald-Jensen
显示剩余8条评论

108

我曾经使用numpy.arange,但由于浮点运算误差,控制其返回元素数量时出现了一些问题。因此现在我使用linspace,例如:

>>> import numpy
>>> numpy.linspace(0, 10, num=4)
array([  0.        ,   3.33333333,   6.66666667,  10.        ])

尽管没有使用decimal,仍然存在浮点误差,例如:np.linspace(-.1,10,num=5050)[0] - TNT
2
@TNT 不,那不是错误。你会发现 np.linspace(-.1,10,num=5050)[0] == -.1 是真的。只是 repr(np.float64('-0.1')) 显示了更多的数字。 - wim
2
虽然这个例子没有显示出过多的舍入误差,但是还是存在失败的情况。例如,当理想结果应该是 1.0 时,print(numpy.linspace(0, 3, 148)[49]) 输出了 0.9999999999999999。相比于 arangelinspace 做得更好,但不能保证产生最小可能的舍入误差。 - user2357112
它保证执行正确的端点处理,并始终生成所请求的确切数量的元素。 - user2357112

41

Pylab有frange(实际上是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. ])

6
自matplotlib版本2.2起,Frange已被弃用。应使用numpy.arange。 - kuzavas

15

渴望地评估(2.x range):

[x * .5 for x in range(10)]

延迟计算(2.x 中的 xrange,3.x 中的 range):

itertools.imap(lambda x: x * .5, xrange(10)) # or range(10) as appropriate

或者:

itertools.islice(itertools.imap(lambda x: x * .5, itertools.count()), 10)
# without applying the `islice`, we get an infinite stream of half-integers.

5
为什么不使用(x * .5 for x in range(10))作为生成器表达式进行惰性求值呢?+1 - Tim Pietzcker
2
因为那样太容易了,我猜呢? :) - Karl Knechtel

12

使用 itertools: 惰性计算的浮点数范围:

>>> 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]

5
使用itertools.takewhile很好。但是,itertools.count(start, step)存在累积的浮点误差问题。(例如,计算takewhile(lambda x: x < 100, count(0, 0.1))。)相反,我会编写takewhile(lambda x: x < stop, (start + i * step for i in count()))。请注意不要改变原始含义,并尽量使翻译更易于理解。 - musiphil

7
我帮助添加了函数 numeric_range 到包 more-itertools 中。 more_itertools.numeric_range(start, stop, step) 与内置函数 range 类似,但可以处理浮点数、Decimal 和 Fraction 类型。
>>> from more_itertools import numeric_range
>>> tuple(numeric_range(.1, 5, 1))
(0.1, 1.1, 2.1, 3.1, 4.1)

5
为什么标准库中没有浮点数范围实现?
正如所有帖子所清楚地表明的那样,没有浮点版本的range()。话虽如此,如果我们考虑到range()函数通常用作一个索引(当然,这意味着一个访问器)生成器,那么忽略这一点是有意义的。因此,当我们调用range(0,40)时,我们实际上是要求从0开始,共有40个值,最大不超过40本身。
当我们考虑索引生成与其值一样多的指数数量时,标准库中使用浮点实现的range()函数就不太合理了。例如,如果我们调用函数frange(0,10,0.25),我们期望包括0和10,但这会产生一个有41个值的生成器,而不是像10/0.25产生的40个值。
因此,根据其用途,frange()函数总是会表现出反直觉的行为;从索引的角度来看,它要么具有太多的值,要么对于一个合理应该从数学角度返回的数字不是包容性的。换句话说,很容易看出这样一个函数会混淆两种非常不同的用例——命名意味着索引使用用例;行为则意味着数学用例。
数学用例
话虽如此,正如其他帖子中讨论的那样,numpy.linspace()很好地执行了来自数学角度的生成:
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.
])

索引用例

就索引而言,我用了一种略微不同的方法,利用了一些巧妙的字符串技巧,使我们能够指定小数点后的位数。

# 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))

同样地,我们也可以使用内置的round函数并指定小数位数:

# 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)

快速比较和性能

当然,根据上面的讨论,这些函数有相当有限的用途。尽管如此,以下是一个快速比较:

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)

结果对于每个人都是一样的:
-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

还有一些时间:

>>> 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

在我的系统上,似乎字符串格式化方法稍微占优势。

限制

最后,展示一下上面讨论的观点并指出另一个限制:

# "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

此外,当skip参数不能被stop数值整除时,可能会出现一个巨大的间隔问题:
# 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

有多种方法可以解决这个问题,但归根结底,最好的方法可能是仅使用Numpy。


2
这是一个相当扭曲的论点。range()应该被视为迭代生成器,它是在for循环中使用还是用于索引某些内容应该由调用者决定。人们已经在for循环中使用浮点数了几千年,上述理由是荒谬的。Python委员会在这里犯了大错,好的论点可能被一些扭曲的理由淹没了。就是这么简单。现在有太多像上面那样的决策被奉为Python语言的基础。 - Shital Shah
如果有任何要返回的点,则函数的第一个值应该是起始点;list(frange_S(2,3,4)) is [] but should be [2.0] - smichr

5

由kichik提供了一种不依赖numpy等库的解决方案,但由于浮点运算, 它经常表现出意外的行为。正如我自己blubberdiblub所指出的那样,额外的元素很容易潜入结果中。例如naive_frange(0.0, 1.0, 0.1)会产生0.999...作为其最后一个值,因此总共产生11个值。

这里提供了一个稍微健壮的版本:

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
        if x < y:
          yield x

由于乘法,舍入误差不会累积。使用 epsilon 可以处理乘法可能出现的舍入误差,尽管在非常小和非常大的情况下可能会出现问题。现在,正如预期的那样:
> a = list(frange(0.0, 1.0, 0.1))
> a[-1]
0.9
> len(a)
10

并且使用稍大的数字:

> b = list(frange(0.0, 1000000.0, 0.1))
> b[-1]
999999.9
> len(b)
10000000

该代码也可作为 GitHub Gist获得。


这个在使用frange(2.0, 17.0/6.0, 1.0/6.0)时会失败。它永远无法变得健壮。 - benrg
@benrg 谢谢你指出这个问题!这让我意识到 epsilon 应该依赖于跳跃值,所以我重新审查了算法并修复了这个问题。这个新版本更加健壮,不是吗? - Akseli Palén
这对于 frange(0.026, 0.619, 0.078) 失败。 - smichr
1
@smichr 谢谢你的注意。我已经修复了这个问题。不知何故,我没有考虑到y - x不是跳数的倍数的情况。我怎么会这么糊涂呢?好吧,修复方法是在yield之前确保x小于或等于y。现在frange(0.026,0.619,0.078)得到的结果是0.026,0.104,0.182,0.26,0.338,0.416,0.494,0.572,就像应该的一样。 - Akseli Palén
我在代码中没有看到条件。此外,请检查frange(.071,.493,.001)这种情况,它不应以0.493结束。但是,如果您认为是因为当x <= y时发出信号,则可以将其更改为x <y,但是那么frange(0.569,0.799,0.23)将会失败,因为它会发出大于0.569的信号。我正在针对我提供的版本测试代码。 - smichr
@smichr 哦,我明白了。感谢您的好眼力。我将 <= 改为 <。我想进一步改进已经没有希望了。基本问题仍然存在:在 Python 中,0.569 + 0.23 > 0.799 是一个正确的语句,尽管对于我们习惯于十进制的眼睛来看,它看起来像是错误的。使用 decimal.Decimal 可能是唯一正确的方法,就像 被接受的答案 中所介绍的那样。 - Akseli Palén

5

正如 kichik 所写,这并不应该太复杂。但是,这段代码:

def frange(x, y, jump):
  while x < y:
    yield x
    x += jump

浮点数在处理时由于错误的累计效应而不合适。这就是为什么你会收到类似以下的东西:

>>>list(frange(0, 100, 0.1))[-1]
99.9999999999986

预期的行为应该是:

>>>list(frange(0, 100, 0.1))[-1]
99.9

解决方案1

可以通过使用索引变量来简单减少累积误差。以下是示例:

from math import ceil

    def frange2(start, stop, step):
        n_items = int(ceil((stop - start) / step))
        return (start + i*step for i in range(n_items))

解决方案2

不使用嵌套函数。只需使用while循环和计数器变量:

此示例按预期工作。

def frange3(start, stop, step):
    res, n = start, 1

    while res < stop:
        yield res
        res = start + n * step
        n += 1

这个函数通常也能正常工作,但在需要翻转区间的情况下不能如此。例如:

>>>list(frange3(1, 0, -.1))
[]

在这种情况下,解决方案1可以正常工作。为使该函数在此类情况下正常工作,您必须应用以下类似的 hack:
from operator import gt, lt

def frange3(start, stop, step):
    res, n = start, 0.
    predicate = lt if start < stop else gt
    while predicate(res, stop):
        yield res
        res = start + n * step
        n += 1

使用这个技巧,您可以使用负步长(即倒序)来调用这些函数:

>>>list(frange3(1, 0, -.1))
[1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.3999999999999999, 0.29999999999999993, 0.19999999999999996, 0.09999999999999998]

解决方案 3

您可以使用纯标准库进一步组合数字类型的范围函数:

from itertools import count
from itertools import takewhile

def any_range(start, stop, step):
    start = type(start + step)(start)
    return takewhile(lambda n: n < stop, count(start, step))

这个生成器源代码来自于《Fluent Python》一书(第14章 “可迭代对象、迭代器和生成器”)。它不能用于递减的范围。你必须像前一个解决方案中那样应用黑科技。

例如,您可以按以下方式使用此生成器:

>>>list(any_range(Fraction(2, 1), Fraction(100, 1), Fraction(1, 3)))[-1]
299/3
>>>list(any_range(Decimal('2.'), Decimal('4.'), Decimal('.3')))
[Decimal('2'), Decimal('2.3'), Decimal('2.6'), Decimal('2.9'), Decimal('3.2'), Decimal('3.5'), Decimal('3.8')]

当然,你也可以将其与浮点数和整数一起使用。

注意事项

如果您想使用这些函数进行负步长的操作,则应添加一个步长符号的检查,例如:

no_proceed = (start < stop and step < 0) or (start > stop and step > 0)
if no_proceed: raise StopIteration

如果您想要模仿 range 函数接口,最好的选择是提出 StopIteration 异常。

模仿 range

如果您想要模仿 range 函数接口,您可以提供一些参数检查:

def any_range2(*args):
    if len(args) == 1:
        start, stop, step = 0, args[0], 1.
    elif len(args) == 2:
        start, stop, step = args[0], args[1], 1.
    elif len(args) == 3:
        start, stop, step = args
    else:
        raise TypeError('any_range2() requires 1-3 numeric arguments')

    # here you can check for isinstance numbers.Real or use more specific ABC or whatever ...

    start = type(start + step)(start)
    return takewhile(lambda n: n < stop, count(start, step))

我觉得你明白了。

你可以使用这些函数中的任意一个(除了第一个函数),而且你所需要的一切都是Python标准库。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接