Date Lang en

Há algumas semanas tive que implementar alguns algoritmos de inteligência artificial para um trabalho da faculdade. Acabei fazendo alguns códigos que talvez sejam úteis para outras pessoas, então decidi compartilhá-los.

Obs: Todos os códigos deste artigo foram testados em Python 3.4.

Um range de float's

Uma das (sub)tarefas era gerar todos os números num dado intervalo com um dado passo. Isto é:

  • [0, 5) com passo 1 deveria resultar em: [0, 1, 2, 3, 4]
  • [0, 0.5) com passo 0.1 deveria resultar em: [0, 0.1, 0.3, 0.4]

A maioria das pessoas com um pouco de familiaridade com python diria que tal problema pode ser resolvido com a função range. Realmente, esta função extremamente conveniente resolve este tipo de problema. Então tentei resolver o problema usando range.

>>> list(range(0, 5, 1))
[0, 1, 2, 3, 4]
>>>list(range(0, 0.5, 0.1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'float' object cannot be interpreted as an integer

range claramente resolve o primeiro caso do problema mas, por que não resolve o segundo? Achei que estava fazendo alguma coisa errada, como colocar os argumentos em ordem errada, então fui procurar a documentação. Fiquei um pouco decepcionado ao descobrir que os argumentos não podem ser do tipo float. Como precisava realmente disto decidi fazer meu próprio float_range.

Minha primeira tentativa foi:

def float_range(start, stop=None, step=1):
    if stop is None:
        stop, start = start, 0

    while(start < stop):
        yield start
        start += step

Me senti bastante esperto com esta solução até encontrar um problema (que acredito ter sido o motivo para não usarem float em range).

>>> list(float_range(0, 5, 1))
[0, 1, 2, 3, 4]
>>> list(float_range(5))
[0, 1, 2, 3, 4]
>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.30000000000000004, 0.4]

Pois é, estava indo tudo bem demais para ser verdade. Este problema de aritmética de ponto flutuante é bastante conhecido e está relacionado a dificuldade de representar números "quebrados" em potências de base 2 (binário). Para mais informação veja este texto.

Felizmente existe uma função (built-in) no python que me ajudou a resolver este problema. A função round, como sugere o nome, arredonda o número para o inteiro mais próximo. Apesar desta funcionalidade ser bastante mágica, não é o mais incrível desta função. Existe um segundo argumento que determina quantos dígitos devem ser considerados ao arredondar.

Com esta nova descoberta fiz uma segunda tentativa:

def float_range(start, stop=None, step=1):
    if stop is None:
        stop, start = start, 0

    while(start < stop):
        yield round(start, 1)
        start += step

Mas esta solução falhava caso a precisão necessária fosse maior que 1.

>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.3, 0.4]
>>> list(float_range(0, 0.05, 0.01))
[0, 0.0, 0.0, 0.0, 0.0]

Este problema claramente se repetiria caso mantivesse a precisão constante. Decidi que a melhor solução seria determinar a precisão do passo e usar aquela precisão ao arredondar. Fiz então uma pequena função auxiliar que determina a precisão.

def precision(number):
    try:
        res = len(str(number).split('.')[1])
    except IndexError:
        res = 1

    return res

Para isto transformamos o número numa string, a "quebramos" no caracter "." e a precisão será o tamanho da segunda string resultante da quebra. Se a quebra resultar apenas em uma string, trata-se de um int e a precisão é 1.

Com esta função podemos melhorar a solução anterior.

def float_range(start, stop=None, step=1):
    if stop is None:
        stop, start = start, 0

    while(start < stop):
        yield round(start, precision(step))
        start += step

Esta, finalmente, funciona como o necessário.

>>> list(float_range(0, 5, 1))
[0, 1, 2, 3, 4]
>>> list(float_range(0, 0.5, 0.1))
[0, 0.1, 0.2, 0.3, 0.4]
>>> list(float_range(0, 0.5, 0.01))
[0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18, 0.19, 0.2, 0.21, 0.22, 0.23, 0.24, 0.25, 0.26, 0.27, 0.28, 0.29, 0.3, 0.31, 0.32, 0.33, 0.34, 0.35, 0.36, 0.37, 0.38, 0.39, 0.4, 0.41, 0.42, 0.43, 0.44, 0.45, 0.46, 0.47, 0.48, 0.49]