[pyar] La función a la que se le mueve la estantería

Marcos M marcosmodenesi en gmail.com
Vie Jun 7 20:39:46 -03 2019


On Fri, Jun 7, 2019 at 4:56 PM Facundo Batista <facundobatista en gmail.com>
wrote:

> Hola!
>
> Este efecto es conocido:
>
> >>> funcs = [lambda n: n ** i for i in range(3)]
> >>> funcs[0](7)
> 49
>
> Sé como solucionarlo:
>
> >>> funcs = [lambda n, i=i: n ** i for i in range(3)]
> >>> funcs[0](7)
> 1
>
> Pero me interesa escarbar un poquito en por qué sucede. Alguien sabe?
> Idea de por dónde buscar?
>

Te propongo un código más simple:

funcs = [lambda: i for i in range(3)]

Con esa línea, tenés una lista con tres funciones que no toman argumentos y
podés hacer funcs[0](), funcs[1]() y funcs[2](). Nos gustaría pensar que
devuelven 0, 1 y 2, respectivamente, ya que la primera fue definida cuando
i valía 0, la segunda, cuando i valía 1, etc. Pero devuelven 2. Las tres
devuelven 2.

La razón es que cada función no se "acuerda" del valor que tiene que
devolver, se acuerda de la referencia (la variable "i") que tiene que
devolver. Estas funciones son clausuras que se "acuerdan" del contexto
donde fueron definidas. Y en ese contexto, i es una referencia. A la hora
de llamar a las funciones, la variable "i" en el contexto donde fueron
definidas, vale 2.

Un ejemplo más claro aún, sin list comprehensions ni lambdas, ni siquiera
un ciclo for:

n [1]: def outer():
   ...:     i = 0
   ...:     def f0():
   ...:         return i
   ...:     i = 1
   ...:     def f1():
   ...:         return i
   ...:     i = 2
   ...:     def f2():
   ...:         return i
   ...:     return f0, f1, f2
   ...:

In [2]: funcs = outer()
In [3]: funcs[0]()
Out[3]: 2
In [4]: funcs[1]()
Out[4]: 2
In [5]: funcs[2]()
Out[5]: 2

Si querés convencerte un poco más, las invocamos antes de que i alcance su
valor final:

In [6]: def outer():
   ...:     i = 0
   ...:     def f0():
   ...:         return i
   ...:     print('llamando a f0', f0())
   ...:
   ...:     i = 1
   ...:     def f1():
   ...:         return i
   ...:
   ...:     print('llamando a f0', f0())
   ...:
   ...:     i = 2
   ...:     def f2():
   ...:         return i
   ...:
   ...:     print('llamando a f0', f0())
   ...:     return f0, f1, f2
   ...:

In [7]: outer()
llamando a f0 0
llamando a f0 1
llamando a f0 2


Un ejemplo más extremo aún: pongamos la i en un lugar tan visible que
después podamos seguir modificándola:

In [11]: i = 0  # scope global
In [12]: def outer():
    ...:     global i
    ...:     def f0():
    ...:         return i
    ...:
    ...:     i =+ 1
    ...:     def f1():
    ...:         return i
    ...:
    ...:     i += 1
    ...:     def f2():
    ...:         return i
    ...:
    ...:     return f0, f1, f2
    ...:

In [14]: f0 = outer()[0]

In [15]: f0()
Out[15]: 2
In [16]: i = 'wat'
In [19]: f0()
Out[19]: 'wat'

Como ves, se acuerda de la i (es una referencia, un lugar en la memoria),
no del valor que tenía i en el momento en que fue definida como función.

Fijate si te sirve el modulo dis, para ver qué instrucciones ejecuta python
cuando llamás a f0:

import dis
dis.dis(f0)

Espero que sirva. Saludos!

Marcos Modenesi
------------ próxima parte ------------
Se ha borrado un adjunto en formato HTML...
URL: <http://listas.python.org.ar/pipermail/pyar/attachments/20190607/e1072de0/attachment.html>


Más información sobre la lista de distribución pyar