Por que não há implementação de intervalo de ponto flutuante na biblioteca padrão?
Como ficou claro por todas as postagens aqui, não há versão de ponto flutuante range(). Dito isso, a omissão faz sentido se considerarmos que a range()função é frequentemente usada como um gerador de índice (e, claro, isso significa um acessador ). Portanto, quando chamamos range(0,40), estamos dizendo que queremos 40 valores começando em 0, até 40, mas não inclusivos em 40.
Quando consideramos que a geração de índice é tanto sobre o número de índices quanto sobre seus valores, o uso de uma implementação de float range()na biblioteca padrão faz menos sentido. Por exemplo, se chamarmos a função frange(0, 10, 0.25), esperaríamos que 0 e 10 fossem incluídos, mas isso renderia um vetor com 41 valores.
Assim, uma frange()função que depende de seu uso sempre exibirá um comportamento contra-intuitivo; possui valores demais percebidos da perspectiva da indexação ou não inclui um número que razoavelmente deve ser retornado da perspectiva matemática.
O caso de uso matemático
Com isso dito, conforme discutido, numpy.linspace()executa a geração com a perspectiva matemática bem:
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.
])
O caso de uso de indexação
E para a perspectiva da indexação, escrevi uma abordagem um pouco diferente com algumas mágicas complicadas de string que nos permitem especificar o número de casas decimais.
# 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))
Da mesma forma, também podemos usar a roundfunção interna e especificar o número de casas decimais:
# 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)
Uma rápida comparação e desempenho
Obviamente, dada a discussão acima, essas funções têm um caso de uso bastante limitado. No entanto, aqui está uma comparação rápida:
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)
Os resultados são idênticos para cada um:
-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
E alguns horários:
>>> 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
Parece que o método de formatação de strings vence por um fio no meu sistema.
As limitações
E, finalmente, uma demonstração do ponto da discussão acima e uma última limitação:
# "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
Além disso, quando o skipparâmetro não é divisível pelo stopvalor, pode haver uma lacuna de bocejo devido ao último problema:
# 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
Existem maneiras de resolver esse problema, mas no final do dia, a melhor abordagem provavelmente seria usar o Numpy.