El company Pedro està aprenent a fer anar Scrapy i em comenta espontàniament:
una cosa meravellosa d’emacs és que està tot molt ben documentat, i de seguida estàs mirant el codi font còmodament (tant sigui emacs lisp que c); hi ha algun equivalent a python? he intentat això:
(ara m’he posat amb el scrapy tutorial)
12345678910[13:49:28] $ ipython3
Python 3.9.2 (default, Feb 28 2021, 17:03:44)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.20.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: help(yield)
File "<ipython-input-1-b8899ae5635b>", line 1
help(yield)
^
SyntaxError: invalid syntax
El poc que sé de Scrapy és que és una eina molt potent i que paga la pena aprendre (però encara no hi he tingut ocasió!) i, encara més important: que està escrita en Python, fent servir la llibreria Twisted.
Amb aquesta informació i havent après Twisted fa més anys dels que
voldria admetre, entenc immediatament que el dubte darrere la pregunta, no va
de yield
únicament, sinó de com funciona la programació asíncrona en
Twisted.
Anem a fer-hi una ullada, amb l’explicació que a mi m’hagués agradat tenir quan ho aprenia i de pas mirem els generadors!
help(yield)
?Comencem per respondre la pregunta concreta:
“com puc obtenir informació sobre yield
des de Python?”
Doncs no anàvem malament amb help(yield)
!
Aturem-nos però a pensar què passa quan fem alguna consulta que sí funciona,
com ara help("")
:
>>> help("")
Help on class str in module builtins:
class str(object)
[...]
help
?Des de la terminal interactiva podem esbrinar-ho:
>>> type(help)
<class '_sitebuiltins._Helper'>
És a dir, que és una instància de la classe _sitebuiltins._Helper
,
un objecte!
Podem fer-hi coses curioses?
>>> a = help
>>> a("")
Help on class str in module builtins:
class str(object)
[...]
>>> dir(help)
['__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__',
'__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__',
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__',
'__sizeof__', '__str__', '__subclasshook__', '__weakref__']
Interessant! Veiem que implementa __call__
, és a dir, és un objecte
callable
:
callable(obj, /)
Return whether the object is callable (i.e., some kind of function).
Note that classes are callable, as are instances of classes with a
__call__() method.
help(yield)
?Ara que sabem que help
és un objecte, i que és un callable
, entenem que
help(yield)
no fa res més que cridar help
amb yield
com a argument…
Però yield
és una paraula reservada! D’aquí que tinguem un error de sintaxi.
help("yield")
Per evitar aquest error de sintaxi, podem posar la paraula yield
entre
cometes, d’aquesta manera és una sintaxi vàlida, i help
comprovarà si
correspon a un concepte conegut.
Aquesta estratègia no només ens és útil aquí, si volem saber com funciona |
o +
podem fer el mateix: help("|")
o help("+")
.
>>> help("yield")
The "yield" statement
*********************
yield_stmt ::= yield_expression
A "yield" statement is semantically equivalent to a yield expression.
The yield statement can be used to omit the parentheses that would
otherwise be required in the equivalent yield expression statement.
For example, the yield statements
yield <expr>
yield from <expr>
are equivalent to the yield expression statements
(yield <expr>)
(yield from <expr>)
Yield expressions and statements are only used when defining a
*generator* function, and are only used in the body of the generator
function. Using yield in a function definition is sufficient to cause
that definition to create a generator function instead of a normal
function.
For full details of "yield" semantics, refer to the Yield expressions
section.
yield
en PythonEl text de help("yield")
ens explica que es fa servir en funcions
generadores, però no hi entra molt en detall.
Tot i que podríem seguir amb la documentació amb help
, ja és útil canviar
a explicacions més casolanes!
def fib():
"""Generador de nombres de Fibonacci"""
a = 1
b = 1
while True:
yield a
(a, b) = (b, a + b)
La faríem servir així:
>>> a = fib()
>>> type(a)
<class 'generator'>
>>> next(a)
1
>>> next(a)
1
>>> next(a)
2
>>> next(a)
3
>>> next(a)
5
>>> next(a)
8
>>> next(a)
13
Si ens hi fixem, la funció fib
conté un bucle infinit (while True
)!
Però a = fib()
retorna de seguida!
No només això, a
acaba essent de tipus generator
.
La màgia és que aquest generator
és iterable, en particular podem anar-hi
avançant amb next
per obtenir el següent nombre de Fibonacci.
Només es generen exactament aquells nombres de Fibonacci que necessitem.
Aquest concepte és força útil i en Python modern es fa servir moltíssim.
Hi ha maneres molt, i molt interessants de fer-los servir :-) però no era
el tema, el tema era per què cal yield
amb Scrapy, i amb
Twisted.
yield
amb Scrapy/Twisted?Doncs… Perquè a Python no hi havia async
i await
fa 20 anys!
La primera versió de Twisted va sortir al 2002. Aleshores el concepte de programació asíncrona no era tan comú com ho és ara, i per descomptat Python no ho suportava.
Ja que hem mencionat el concepte un parell de vegades, caldrà il·lustrar-ho breument:
Tradicionalment un programa s’executa de forma seqüencial, és a dir en l’ordre en què estan escrites les instruccions.
En el món real però, especialment en el món de les xarxes, fer-ho d’aquesta manera és altament ineficient.
Ja que parlem de Scrapy, pensem en la tasca “descarregar la primera plana de 1000 pàgines web”.
Si ho fem de manera seqüencial, trigarem la suma de tots els temps de
descàrrega, és a dir, si es triga 1 segon
per cada pàgina, trigarem
1000 segons
.
Si ho fem de manera asíncrona, es facilita paral·lelitzar aquesta tasca, el
que fem és començar la descàrrega d’una pàgina web i rebre una
notificació / “callback” quan aquesta descàrrega finalitza.
Així, si podem fer 10 descàrregues paral·leles i triguem 1 segon
per
descàrrega, la mateixa tasca trigarà 100 segons
(100 blocs de 1 segon
amb 10 descàrregues per bloc).
En Python modern es pot fer programació asíncrona sense necessitar altres llibreries, aquesta implementació va estar molt influenciada per les maneres de fer de Twisted i els 10 anys d’experiència mantenint aquest tipus de codi en Python.
Molt per sobre, per definir una funció asíncrona:
async def f():
# Pausa l'execució fins que `g` retorni
b = await g()
# Segueix fent altres coses
return b**2
Podem fer servir async def
per marcar la funció f
com a asíncrona, i
en el cos d’aquesta funció podem fer servir await
per “esperar” a que altres
funcions asíncrones acabin, i així poder escriure codi que sembli seqüencial.
En executar f()
, no obtenim directament el resultat, sinó que obtenim una
coroutine
(segons com una Promise
).
Aquests són els noms que més es fan servir avui dia per aquests conceptes, en part perquè es van popularitzar amb Node.js al 2009.
No és molt diferent! Però per aconseguir-ho va caldre abusar conceptes que ja estaven disponibles en Python aleshores, com ara els generators!
from twisted.internet import defer
@defer.inlineCallbacks
def f():
# Pausa l'execució fins que `g` retorni
b = yield g()
# Segueix fent altres coses
defer.returnValue(b**2)
Podem fer servir el decorador defer.inlineCallbacks
per
“marcar la funció f
com a asíncrona”, i
en el cos d’aquesta funció podem fer servir yield
per “esperar” a que altres
funcions asíncrones acabin, i així poder escriure codi que sembli seqüencial.
En executar f()
, no obtenim directament el resultat, sinó que obtenim un
twisted.internet.defer.Deferred
.
Conceptualment molt semblant, però força diferent a la vista. Per què?
Doncs en no tenir els conceptes dintre del llenguatge, desenvolupadors com
Glyph Lefkowitz van tenir idees molt bones; si ens hi fixem, fer
servir yield
al cos de la funció, vol dir que f
retorna un generator
i no
pas un resultat.
De fet, no tenim ben bé una manera de marcar “el que f
retorna”, perquè cada
operació que fem de manera asíncrona necessitarà un yield
.
D’aquí que necessitem ambdós: defer.inlineCallbacks
i defer.returnValue
.
Amb el decorador defer.inlineCallbacks
, el que fem és
convertir aquesta funció, en quelcom que ens retorna un Deferred
, i amb
defer.returnValue
el que fem és especificar quin serà el valor de retorn
d’aquest Deferred
.
Els Deferreds
, com les Promises
, en el fons són molt
equivalents, són un objecte de Python, que ens representa un procés o càlcul
que encara no ha finalitzat.
A aquests objectes els podem afegir manualment callbacks
, per tal d’encadenar
processos o podem fer servir yield
dintre de funcions marcades com a
asíncrones amb defer.inlineCallbacks
per tal d’esperar el seu resultat.
La sintaxi “nova” de Python per programació asíncrona és molt bonica!
I Twisted, en ser una llibreria tan innovadora, encara no se’n
beneficia al 100%. En part és perquè el canvi de Python 2 a Python 3 va trigar
força, perquè la base de codi és molt extensa (hi ha molts protocols
implementats), en part també perquè l’ecosistema async
a Python va
trigar en estabilitzar-se.
La realitat és que a dia d’avui es pot escriure codi que faci servir Twisted amb la sintaxi nova! Tot i això, molt sovint és útil saber d’on venim :-) perquè la compatibilitat no és perfecta, i a vegades ens trobem amb la necessitat d’escriure codi amb la sintaxi antiga; a més a més, entenent això podrem entendre els tutorials i el codi de les llibreries que necessitem, com Scrapy!
Per cert, que quedi clar que Twisted encara és un projecte rellevant! De fet, avui mateix s’ha anunciat la pre-release d’una nova versió: 22.8.0.