Desenvolupament modern en Python
Python ja no és el que era. Ja fa un temps que el desenvolupament modern amb Python involucra tot d'eines que ens ajuden a augmentar la qualitat del codi.
Ja de temps enrere, projectes de linting com pylint
, i més recentment ruff
, analitzen el codi de forma estàtica per a trobar-ne problemes.
Més recentment mypy
ens ajuda a comprovar que el codi sigui correcte a traves de una declaració explícita dels tipus.
I per a reblar el clau, pre-commit
ens dóna la forma d'automatitzar aquestes eines per a que s'executin de forma regular.
Totes aquestes eines resulten ser essencials en el dia a dia de l'equip de desenvolupament.
El problema
Si teniu experiencia amb mypy
, segur que us haureu trobat amb situacions on no es comporta com esperàveu. La temptació de afegi un # ignore: typing
és gran, o també de fer servir typing.cast
per a treure l'error. Al cap i a la fi, si no aconseguim acontentar a mypy
no ens passarà el pre-commit
.
import typing
class A:
def a():
print('a')
class B(A):
def b():
print('b')
things: dict[str,A] = {'a':A(), 'b':B()}
b = things['b']
# Sabem que b és instancia de B, però mypy no
b.b() # Això no li agrada a mypy
typing.cast(B, b).b() # mypy accepta el canvi
Tot i que això acontenta a mypy, és molt perillós. I si la part del codi canvia i b
acaba sent de la classe A
? És possible que ens provoqui errors en temps d'execució, però en llocs poc evidents. Si en lloc de cridar directament b.b()
ho passéssim a una altra funció, podria passar desapercebut durant molt de temps:
from queue import Queue
cua: Queue[B] = Queue()
def fes_algo_mes_tard(b: B) -> None:
cua.put(b)
b_b = typing.cast(B, b) # mypy accepta el canvi
fes_algo_mes_tard(b_b)
Llavors no veurem l'error fins que l'element sigui consumit de la cua.
Alternatives
L'alternativa més senzilla a typing.cast
és assert isinstance
:
b = things['b']
# Sabem que b és instancia de B, però mypy no
assert isinstance(b,B) # ara mypy sí que ho sap
b.b() # Cap problema
Si us hi fixeu, el codi llegeix: "Jo, programador, us asseguro que sé que b
és instància de B
, i si no tinc raó, que salti una excepció!". Això ens impedeix que ens passi com en l'exemple anterior on l'error quedava sense detectar durant temps.
A més, en entorn de producció es poden desactivar els assert
s, eliminant qualsevol penalització en l'eficiència.
Així i tot, això no funciona per tipus com dict[str,B]
. Per a solucionar-ho podem usar pydantic
i crear una comanda de substitució:
from typing import cast
from pydantic import parse_obj_as
def assert_cast(expected_type: type[T], value: Any) -> T:
"""Drop-in replacement for typing.cast, but with runtime checks.
If assert is disabled, this function does nothing.
Checks that value is of type expected_type and returns it as such.
this also works with arbitrary types such as Union, Pydanitc models, etc.
Because of how Pydantic works, we can't prevent the check to use type coercion.
This should be solved in Pydantic V2, in the future"""
# If asserts are disabled, we don't want to do anything
if not __debug__:
return cast(T, value)
try:
parse_obj_as(expected_type, value)
except ValueError as exc:
raise AssertionError(
f"Expected value of type {expected_type} but got {type(value)} instead"
) from exc
return cast(T, value)
I ara substituïu cast
per assert_cast
, i així tindreu tots els beneficis de cast i assert alhora.
assert_cast(dict[str,B],b_dict)
Nota: Recordeu que també podeu demanar a ChatGPT que generi tests unitaris per la funció. I recordeu-vos de revisar-los!
Implantació
Un cop tenim identificat el problema en la nostra base de codi, cal trobar una forma perquè no torni a passar. Evidentment, se'ls pot explicar als membres de l'equip, però no podem confiar només en la memòria; tornarà a passar.
Caldria trobar la forma de detectar typing.cast
en el pre-commit per a evitar que se'ns passi, però com? Doncs fem un plugin de pylint
.
El problema és que fer un plugin de pylint
té pinta de ser una tasca llarga i avorrida. I, pitjor de tot, ens convertiríem en experts en plugins de pylint
i ens n'encarregarien constantment. Ningú ho vol, això.
Per tant, la millor alternativa és encarregar-ho a ChatGPT.
ChatGPT
Perquè ChatGPT ens faci un plugin de pylint
li ho hem d'encarregar:
Please write a pylint plugin that warns about using
typing.cast
function anywhere in the code.
Per a tenir uns bons resultats, millor usem GPT-4
, GPT-3.5
al·lucina massa.
Al primer intent, ens generarà codi correcte, però que es deixa alguns casos. Per exemple, al principi només ens detectava quan es cridava typing.cast
però no quan es cridava cast
després de fer from typing import cast
. La tècnica és anar encarregant-li al ChatGPT que afegeixi aquests casos.
Ajuda molt tenir diversos fitxers canaris on afegim deliberadament typing.cast
de diferents formes, per a comprovar que efectivament, el plugin els reconeix.
Un cop funcionant, ajuda molt fer un document en el mateix repositori on explica tot el raonament de per què es prohibeix l'ús de typing.cast
i referir-s'hi en el missatge del warning. D'aquesta forma tothom podrà entendre de què va el tema quan li salti l'error.
Recuperant el pre-commit
Un cop afegit el plugin, el pre-commit
ens fallarà, trobant totes les vegades que hi ha typing.cast
en el codi.
Ens trobem en la situació que hauríem de solucionar tots els errors de pylint abans de poder fer push de la regla, i això pot ser una tasca massa gran, si n'hi ha masses.
En aquest cas, l'estratègia que vam triar va ser la de afegir la regla de pylint com a warning i no com a error, de forma que, al llarg del temps, podem fer el canvi i anar substituint els casos de typing.cast
de forma progressiva.
Conclusions
pylint
i pre-commit
són fantàstics per aquest cas d'ús: reforçar una norma que prohibeix l'ús d'una funció. D'aquesta manera no cal vigilar manualment que la gent no se la salti.
Un descobriment és com d'efectiu és ChatGPT per a aquesta tasca.
Si no es pot deixar el pre-commit en verd d'una sola passada, caldrà organitzar sessions per acabar d'eliminar la funció prohibida.
Top comments (0)