Gerador Python eficiente da sequência de Fibonacci
Encontrei essa pergunta ao tentar obter a geração Pythonic mais curta dessa sequência (depois percebendo que havia visto uma semelhante em uma Proposta de aprimoramento do Python ) e não notei mais ninguém surgindo com minha solução específica (embora a resposta principal fica perto, mas ainda menos elegante), então aqui está, com comentários descrevendo a primeira iteração, porque acho que isso pode ajudar os leitores a entender:
def fib():
a, b = 0, 1
while True: # First iteration:
yield a # yield 0 to start with and then
a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1)
e uso:
for index, fibonacci_number in zip(range(10), fib()):
print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number))
impressões:
0: 0
1: 1
2: 1
3: 2
4: 3
5: 5
6: 8
7: 13
8: 21
9: 34
10: 55
(Para fins de atribuição, observei recentemente uma implementação semelhante na documentação do Python sobre módulos, mesmo usando as variáveis a
e b
, que agora me lembro de ter visto antes de escrever esta resposta. Mas acho que essa resposta demonstra melhor uso da linguagem.)
Implementação definida recursivamente
A Enciclopédia Online de Sequências Inteiras define a Sequência de Fibonacci recursivamente como
F (n) = F (n-1) + F (n-2) com F (0) = 0 e F (1) = 1
Definir sucintamente isso recursivamente em Python pode ser feito da seguinte maneira:
def rec_fib(n):
'''inefficient recursive function as defined, returns Fibonacci number'''
if n > 1:
return rec_fib(n-1) + rec_fib(n-2)
return n
Mas essa representação exata da definição matemática é incrivelmente ineficiente para números muito maiores que 30, porque cada número calculado também deve ser calculado para cada número abaixo dela. Você pode demonstrar como é lento usando o seguinte:
for i in range(40):
print(i, rec_fib(i))
Recursão memorizada para eficiência
Ele pode ser memorizado para melhorar a velocidade (este exemplo aproveita o fato de que um argumento de palavra-chave padrão é o mesmo objeto toda vez que a função é chamada, mas normalmente você não usaria um argumento padrão mutável exatamente por esse motivo):
def mem_fib(n, _cache={}):
'''efficiently memoized recursive function, returns a Fibonacci number'''
if n in _cache:
return _cache[n]
elif n > 1:
return _cache.setdefault(n, mem_fib(n-1) + mem_fib(n-2))
return n
Você verá que a versão memorizada é muito mais rápida e rapidamente excederá sua profundidade máxima de recursão antes que você possa pensar em se levantar para tomar um café. Você pode ver o quanto mais rápido é visualmente, fazendo o seguinte:
for i in range(40):
print(i, mem_fib(i))
(Pode parecer que podemos fazer o que está abaixo, mas na verdade não nos permite tirar proveito do cache, porque ele se chama antes que o setdefault seja chamado.)
def mem_fib(n, _cache={}):
'''don't do this'''
if n > 1:
return _cache.setdefault(n, mem_fib(n-1) + mem_fib(n-2))
return n
Gerador definido recursivamente:
Enquanto eu aprendia Haskell, me deparei com essa implementação em Haskell:
fib@(0:tfib) = 0:1: zipWith (+) fib tfib
O mais próximo que eu acho que posso chegar disso em Python no momento é:
from itertools import tee
def fib():
yield 0
yield 1
# tee required, else with two fib()'s algorithm becomes quadratic
f, tf = tee(fib())
next(tf)
for a, b in zip(f, tf):
yield a + b
Isso demonstra:
[f for _, f in zip(range(999), fib())]
Só pode ir até o limite de recursão. Geralmente, 1000, enquanto a versão Haskell pode chegar a centenas de milhões, embora use todos os 8 GB de memória do meu laptop para fazer isso:
> length $ take 100000000 fib
100000000
Consumindo o iterador para obter o enésimo número de fibonacci
Um comentarista pergunta:
Pergunta para a função Fib () que é baseada no iterador: e se você quiser obter o enésimo, por exemplo, o décimo número do fib?
A documentação do itertools tem uma receita para isso:
from itertools import islice
def nth(iterable, n, default=None):
"Returns the nth item or a default value"
return next(islice(iterable, n, None), default)
e agora:
>>> nth(fib(), 10)
55