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