Como irse quitando 💉 de Python2
David García Garzón
y no migración?
2020: Acaba soporte a Python 2
Gisce no tendrá el ERP migrado en enero
Aún estaremos con Python 2 por un tiempo
Cuando se pongan las pilas, no tendremos
capacidad de seguirles el ritmo con nuestro código.
Hay que ir haciéndolo ya
Migrar el código para que funcione en ambos
Manual: Idiomas de compatibilidad
Se proponen diversos idiomas para cada problema
Escoger el idioma más parecido al Py3 final
Intento concretar propuestas, no escritas en piedra Unifiquemos criterios con diálogo
Importante: Cobertura previa de tests
Código aguantado con tests.
¿No hay tests? Montamos B2B de campaña.
Objetivo: que no dejen de pasar en Py2 mientras avanzamos con Py3.
A falta de CI, los que podamos en Travis
(tests que no usen datos personales, ni erp)
Ejemplos travis multiversion: intercoop, generation…
2to3
: Convierte a Py3, pero no portable.
Aplica una bateria de fixers (arregladores) (Lista)
Algunos son portables, los aplicaremos tal cual.
Otros no, los aplicamos a parte para detectar incompatibilidades.
Parte de la libreria future
futurize
: Genera Py3 backward compatible Py2
pasteurize
: Genera Py2 forward compatible Py3
Aún así hay que revisar el código.
Pasamos el 2to3
con los fixes portables
$ 2to3 -w -f raise -f except -f asserts -f paren \
-f raw_input -f reduce -f import
Test, commit y, el resto, sin sobreescribir:
$ 2to3 -nw --add-suffix=3 src/ # Genera ficheros py3
$ futurize -nw --add-suffix=23 src/ # py23, Py3 backward compatible
Analizamos los cambios propuestos caso a caso
$ find -name \*.py3 |
while read a; do vimdiff $a ${a%3} ${a%3}23; done
print
x
)range
, (iter
)items
except Exception as e
keys
/iterkeys
, values
/itervalues
, items
/iteritems
, range
/xrange
, readlines
/xreadlines
, map
/imap
, zip
/izip
, filter
/ifilter
, …
En Py2 retornan listas, la alternativa retorna generadores.
En Py3 los primeros retornan generadores, los segundos no existen.
2to3 convierte:
d.iteritems()
→ d.items()
d.items()
→ list(d.items())
Propuesta: usar items()
sin list
casi siempre
En Py2, listas grandes intermedias son lentas. Usar future.utils.iteritems
en esos casos.
En Py3, los generadores se gastan. Si hay manipulaciones o más una sola iteración, convertir a lista o volver a llamarlo.
for k,v in d.items() # Original
for k,v in list(d.items()) # 2to3
for k,v in d.items() # Nuestra propuesta
# Peligro 1: Si la lista es grande y en Py2 ralentiza
from future.builtins import iteritems
for k in iteritems(d)
# Peligro 2: Lista guardada en Py2, rompe Py3
items = d.items()
for k in (items): ....
for k in sorted(items): ... # generador ya consumido en Py3
# Solución: Llamar la función cada vez
# Si se vuelve costoso en Py2, ver Peligro 1
Solo en caso que Py2 sea muy ineficiente usaremos:
Estrategia: Dejar a 2to3 que haga lo suyo
Ejemplo de fix totalmente portable.
Funciona perfectamente en Py2
y es el definitivo para Py3
raise from
Interesante para encadenar contexto
# Python 3 only
class FileDatabase:
def __init__(self, filename):
try:
self.file = open(filename)
except IOError as exception:
raise DatabaseError('failed to open') from exception
# Python 2 and 3:
from future.utils import raise_from
class FileDatabase:
def __init__(self, filename):
try:
self.file = open(filename)
except IOError as exception:
raise_from(DatabaseError('failed to open'), exception)
# Testing the above:
try:
fd = FileDatabase('non_existent_file.txt')
except Exception as e:
assert isinstance(e.__cause__, IOError) # FileNotFoundError on Py3.3+ inherits from IOError
Algunas excepciones han cambiado:
TODO
print
En Py2 el print
era una cláusula como el return
.
Ahora es una función built-in como len()
.
# Solo una cosa: Entre parentesis.
print('Hola mundo') # Portable
# Varias cosas: Usamos format
print('Hola', name) # imprime la tupla en Py2!!
print('Hola {}'.format(name)) # Portable
# Standard error: usamos consolemsg
print sys.stderr << "La has cagado", name # Py2 only
print("Fastidiate {}".format(name)", file=sys.stderr) # Py3 only
from consolemsg import error, out # Errores en colorines
error("Fastidiate {}", name) # No hace falta format
# Tambien tenemos el substituto para stdout
out("Hola {}", name) # Funciona con unicodes y pipes
Hemos evitado hacer from __future__ import print_function
Pero ¿y los prints acabados en coma que no hacen salto de linia?
Usar el write
y, si cal, flush
.
En Py2 los imports eran implicitamente relativos.
En Py3 solo se puede hacer de forma explícita.
Como la forma explícita es común, usaremos esa en ambos.
Fixer portable de 2to3
: import
.
Se han reestructurado las librerias:
itertools, collections, queue, socketserver
http(lib), url(lib). xmlrpc(lib)
Se resuelve:
Usando el import de Py3
Antes de hacer el import, poner:
Atención: Falla silenciosamente. ¡Bugs latentes!
Py2: a/b
es división entera con operadores enteros
En muchos sitios, float(a)/b
para evitarlo.
Py3: a/b
siempre es división real
3 / 2 # Retorna 1 en Py2, 1.5 en Py3
# Si queremos entero, usamos operador explicito
3 // 2 # Retorna 1 en Py2 y Py3
# No queremos comportamiento antiguo de / asi que:
from __future__ import division
3 / 2 # Retorna 1.5 en Py2 y Py3
long
El sufijo L
para los long es implicito en Py2, ilegal en Py3, lo quitamos
El tipo long
de Py2 es el int
de Py3, si necesitas discriminarlos:
Atención: oculta bugs latentes
0644
solo funciona en Py2, 0o644
en ambos.
Usar la segunda forma siempre.
Estrategia, pasar el fix numliterals
de 2to3
, y revisar que realmente queriamos una constante octal donde proponga cambios.
__
mágicos__
🎩Atención: ¡Fallo silencioso!
Nuestro código no los suele redefinir.
Si lo hemos hecho, cuidado con estos:
__nonzero__
y __bool__
y
next
y __next__
__unicode__
y __str__
__nonzero__
__next__
# Old Py2 only code
class myiterable:
def __iter__(self): return self
def next(self): ... # bad
it.next() # explicit use, Py2 only
# Portable code, looking almost like Py3
from builtins import object # the only added code for portability
class customiterator(object):
def __iter__(self): return self # same
def __next__(self): ... # same as next
next(it) # explicit use portable
__str__
y __unicode__
Unicode ya es complicado con una sola versión.
Un código que funcione en las dos… es posible.
A veces cuesta hacer portable porque no lo estamos gestionando bien en Py2 o en Py3
Aprovechemos para aclarar conceptos y establecer premisas que podamos seguir en ambas.
unicode
(str
en Py3): texto multilenguaje abstracto, sin ninguna codificación explícita.
bytes
(str
en Py2): bytes que lo representan usando una codificación concreta, ASCII, UTF-8…
o qualquier tira de bytes aleatoria que no sea texto.
¡Ojo! Aunque tienen la misma semántica, bytes
en Py3 tiene una API muy diferente a str
en Py2.
str
→ bytes
unicode
→ str
'text'
o b'text'
→ b'text'
u'text'
→ 'text'
o u'text'
basestring
→ removed
(str
y bytes
ya no comparten API en Py3)
Los prefijos explícitos mantienen la misma semántica
No disponibles hasta 2.7.1 (b
) y 3.3 (u
)
type(u'')
es unicode
en Py2, str
en Py3
type(b'')
es str
en Py2, bytes
en Py3
type('')
es str
en ambos pero semanticamente diferentes
Nosotros manejamos todo el texto como unicode.
Si nos llegan cosas como bytes: decode('utf8')
Si nos piden cosas como bytes: encode('utf8')
Todos los bytes codificados en UTF-8, por defecto.
Solo se aplica decode
a bytes
Solo se aplica encode
a unicodes
Py2 permite aplicarlos indistintamente
¡No lo hagamos!
Si aplicamos encode o decode al tipo que no toca,
Py2 convierte al tipo que toca usando ASCII.
Si contiene no-ASCII, peta 🙇
u'Castaña'.decode('utf8')
# En Py3 no hay decode para unicodes
# En Py2 sería equivalente a:
u'Castaña'.encode('ascii').decode('utf8')
# Como no puede encodear a ascii peta
# Analogamente:
b'Castaña'.encode('utf8')
# En Py3 no hay encode para bytes
# En Py2 sería equivalente a
b'Castaña'.decode('ascii').encode('utf8')
Bytes: Nunca se manipulan, suman, formatean… al menos que estemos hackeando hardware.
Minimizaremos o aislaremos las fuentes y destinos de bytes para no tener que hacer encodes y decodes en todos sitios
Para cuando no sabemos qué nos llega
(aunque deberíamos saber qué nos llega)
def u(text):
if type(text) == type(u''): return text
if type(text) == type(b''): return text.decode('utf8')
return type(u'')(text) # unicode formating
def b(text):
if type(text) == type(b''): return text
if type(text) == type(u''): return text.encode('utf8')
return type(u'')(text).encode('utf8') # format and encode
En la última versión de consolemsg
😄
El código fuente se guarda como bytes
¿Qué codificación?
En Py3 se supone UTF8
En Py2 se supone ASCII (again!)
Por Py2, siempre en la cabecera:
# -*- encoding: utf8 -*-
Y configurar editor con encoding UTF8 defecto.
Ante la duda explicitar prefijo u
siempre.
Como mínimo, si el literal:
format
La comunidad desaconseja usar
un aparentemente conveniente
from __future__ import unicode_literals
¿Por qué explicitar u
en las no ASCII?
Cuando Py2 combina unicode con otra cosa,
decode
implicito ASCII ¡Aggh!Si las partes no ASCII ya están en unicode, lo irá promocionando bien.
¿Por qué explicitar u
en templates de format
?
En Py2 el format
mantiene el tipo del template.
Segun toque codifica o decodifica los argumentos usando ASCII (again!)
name1 = u'María' # prefijo por la norma de no-ascii
name2 = 'Pedro' # podriamos poner el prefijo, no se requiere
# Py2 implícitamente codifica name1 en ASCII
'Hola {} y {}'.format(name1,name2) # Peta, hay no ASCII
# Py2 implícitamente decodifica name2 de ASCII
u'Hola {} y {}'.format(name1,name2) # Funciona
unicode_literals
Podríamos ahorrarnos las u
’s con:
from __future__ import unicode_literals
En la práctica dicen que da problemas y todo el mundo lo descaconseja.
No entiendo porqué (ver notas)
open
Cuando abrimos en modo texto:
Py2 lee y escribe bytes
.
Si le pasamos unicode
, codifica a ASCII implicito
Hemos de codificar explícitamente, si no va bien.
En modo texto, lee y escribe unicode
Especificamos a open
el parámetro encoding
Ojo que por defecto es ascii
codecs.open
En Py2 cuando queríamos serializar unicode
usabamos codecs
.
Overkill en Py3, builtin open
ya lo hace.
io.open
Misma interfaz que el open
builtin en Py3.
Cuando caiga Py2, simplemente quitar el import.
print
Cuando metemos nuestro script en pipes de shell el locale de stdin/stdout pasa a ser C (ASCII)
Py2: funciona en consola pero en pipe peta
La solución en todo caso es definir un environment.
PYTHONIOENCODING='utf8'
O usar exclusivamente las funciones de la última versión de consolemsg
que gestionan esto ellas solas sin requerir que el usuario defina el environ.
Nos hemos centrado en código portable Py2/Py3.
¿Qué podremos hacer cuando dejemos caer Py2?
Dejar de usar los prefijos u''
Eliminar todos los futures y six
Substituyen a format
, similar microlenguage
Prefijo f''
, usa las variables locales
Se pueden meter expresiones Python
Mucho más rápido (increiblemente)
No puede haber claves duplicadas pero si valores
@enum.unique
prohibe valores duplicados
EnumInt
deriva de int
, se puede usar como tal
Enum.auto()
asigna los valores automágicamente
Podemos definir métodos en la clase
Enums combinables con & | ~ ^
class Colors(Flag): # O FlagInt
BLACK = 0 # Cero explicito, auto empieza en 1
RED = auto() # Potencias de 2
GREEN = auto()
BLUE = auto()
YELLOW = RED | GREEN # Combinacion nombrada
CYAN = GREEN | BLUE
MAGENTA = RED | BLUE
WHITE = RED | GREEN | BLUE
Colors.RED | Colors.BLUE # Colors.MAGENTA
Colors.WHITE & ~Colors.BLUE # Colors.YELLOW
bool(Colors.CYAN & Colors.RED) # False
bool(Colors.CYAN & Colors.BLUE) # True
Bye os.path.join
and co
PurePath
: ops que no acceden al sistema
Path
: le añade ops que acceden al sistema
En Linux:
[Pure]Path
es un alias de Posix[Pure]Path
Se puede usar WindowsPurePath
pero no WindowsPath
workingDir = Path('.') # Also Path.cwd()
home = Path.home() # '/home/vokimon' in my linux
p = Path('/tmp/vokimon/myzip.tar.gz')
p.parent # '/tmp/vokimon'
p.name # 'myzip.tar.gz'
p.stem # 'myzip.tar'
p.suffix # '.gz'
p.suffixes # ['.tar', '.gz']
p.relative_to('/tmp') # 'vokimon/myzip.tar.gz'
p.with_suffix('.bz2') # '/tmp/vokimon/myzip.tar.bz2'
p.with_name('foo.zip') # '/tmp/vokimon/foo.zip'
p.anchor # p.drive + p.root
str(p) # returns the path string
bytes(p) # uses os.fsencode()
p.as_uri() # file://....
p.as_posix() # forwards back slashes
p.is_absolute() # true
p.is_reserved() # pe. true for 'null' in windows
p.match("*.gz") # true
Path("/home/vokimon/../sammy/").resolve() # /home/sammy
Path('~/.config').expanduser() # /home/vokimon/.config
subdirs = [ x for x in workingDir.iterdir() if x.is_dir() ]
p.exists(), p.is_file(), p.is_dir(), p.is_link(), p.is_socket()
p.is_fifo(), p.is_block_device(), p.is_char_device()
p.glob("**/*.py") # globbing recursivo
with p.open(encoding='utf8') as f: lines = f.readlines()
content = p.read_text(encoding='utf8') # Or read_bytes
p.write_text(content, encoding='utf8') # Or write_bytes(bcontent)
p.owner
p.group
p.stat().st_size # st_mtime, st_size...
p.mkdir(parents=True, exists_ok=True) # mkdir -p
p.chmod(0o777)
p.rmdir() # must be empty
p.symlink_to(target) # ln -s
p.(target) # mv
p.rename(target) # mv? (behaves different Win/Posix)
p.replace(target) # mv? (portable)
p.touch() # touch
p.unlink() # rm
p.samefile(other) # Points to same inode
Python solo lo guarda en myfunction.__annotations__
y prou.
Static type checking ( mypy, pytype )
Editores para dar pistas inteligentes.
Para documentar, implementar sobrecarga…
pytypes
define decorators @typechecked
, @override
…
funciones para activarlos a nivel de modulo o global
enforce.py
el decorator es @runtime_validation
pydantic
Named tuples by class (ya lo hace Python 3.8)
Anotaciones con tipos genéricos
from typing import TypeVar, Sequence
X = TypeVar('X')
def choose(elements: Sequence[X], n: int) -> X:
...
DuckTypes (como los Concepts de C++)
Any
, Union
, List
, Tuple
, Callable
, Type
, TypeVar
, Optional
…
Facilita el uso de desempaquetado con tamaños variables.
a
solo posicional/
indica anteriores solo posicionalesb
posicional o clave*
indica siguientes solo clavec
solo clave:=
(asignación/expresión) (2.8)<=>
(comparacion 1,0,-1) (2.9)@
, __matmult__
functools.lru_cache
ipaddress
faulthandler
:
@coroutine
raise excep from oldexcept
yield from generator()
for x in generator(): yield x
nonlocal
en las closures para modificar