
Esta es la cuarta colección de consejos y programación de Python de mi feed @pythonetc .
Selecciones anteriores:
Anulación y sobrecarga
Hay dos conceptos que son fáciles de confundir: anulación y sobrecarga.
La anulación ocurre cuando una clase secundaria define un método ya proporcionado por las clases principales y, por lo tanto, lo reemplaza. En algunos idiomas, es necesario marcar explícitamente el método de anulación (en C # se usa el modificador de override
), y en algunos idiomas esto se hace como se desee (anotación de @Override
en Java). Python no requiere el uso de un modificador especial y no proporciona el marcado estándar de dichos métodos (en aras de la legibilidad, alguien usa el decorador personalizado @override
, que no hace nada).
La sobrecarga es otra historia. Este término se refiere a una situación en la que hay varias funciones con el mismo nombre, pero con firmas diferentes. La sobrecarga es posible en Java y C ++; a menudo se usa para proporcionar argumentos predeterminados:
class Foo { public static void main(String[] args) { System.out.println(Hello()); } public static String Hello() { return Hello("world"); } public static String Hello(String name) { return "Hello, " + name; } }
Python no admite la búsqueda de funciones por firma, solo por nombre. Por supuesto, puede escribir código que analice explícitamente los tipos y la cantidad de argumentos, pero esto se verá incómodo, y esta práctica es mejor evitarla:
def quadrilateral_area(*args): if len(args) == 4: quadrilateral = Quadrilateral(*args) elif len(args) == 1: quadrilateral = args[0] else: raise TypeError() return quadrilateral.area()
Si necesita sugerencias de typing
, use el módulo de typing
con el decorador @overload
:
from typing import overload @overload def quadrilateral_area( q: Quadrilateral ) -> float: ... @overload def quadrilateral_area( p1: Point, p2: Point, p3: Point, p4: Point ) -> float: ...
Vivificación automática
collections.defaultdict
permite crear un diccionario que devuelve un valor predeterminado si falta la clave solicitada (en lugar de arrojar un KeyError
). Para crear un defaultdict
predeterminado defaultdict
debe proporcionar no solo un valor predeterminado, sino una fábrica de dichos valores.
Por lo tanto, puede crear un diccionario con un número prácticamente infinito de diccionarios anidados, lo que le permite utilizar construcciones como d[a][b][c]...[z]
.
>>> def infinite_dict(): ... return defaultdict(infinite_dict) ... >>> d = infinite_dict() >>> d[1][2][3][4] = 10 >>> dict(d[1][2][3][5]) {}
Este comportamiento se llama "auto-vivificación", un término que proviene de Perl.
Instanciación
La instanciación de objetos implica dos pasos importantes. Primero, el método __new__
se llama desde la clase, que crea y devuelve un nuevo objeto. Luego, Python llama al método __init__
, que establece el estado inicial de este objeto.
Sin embargo, __init__
no se llamará si __new__
devuelve un objeto que no es una instancia de la clase original. En este caso, el objeto podría ser creado por otra clase, lo que significa que __init__
ya ha sido invocado en el objeto:
class Foo: def __new__(cls, x): return dict(x=x) def __init__(self, x): print(x)
También significa que no debe crear una instancia de la misma clase en __new__
usando el constructor regular ( Foo(...)
). Esto puede conducir a la ejecución repetida de __init__
, o incluso a una recursión infinita.
Recursión infinita:
class Foo: def __new__(cls, x): return Foo(-x)
Doble ejecución __init__
:
class Foo: def __new__(cls, x): if x < 0: return Foo(-x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
La forma correcta:
class Foo: def __new__(cls, x): if x < 0: return cls.__new__(cls, -x) return super().__new__(cls) def __init__(self, x): print(x) self._x = x
El operador [] y los cortes
En Python, puede anular el operador []
definiendo el método mágico __getitem__
. Entonces, por ejemplo, puede crear un objeto que virtualmente contenga un número infinito de elementos repetidos:
class Cycle: def __init__(self, lst): self._lst = lst def __getitem__(self, index): return self._lst[ index % len(self._lst) ] print(Cycle(['a', 'b', 'c'])[100])
Lo inusual aquí es que el operador []
admite una sintaxis única. Al usarlo puede obtener no solo [2]
, sino también [2:10]
, [2:10:2]
, [2::2]
e incluso [:]
. La semántica del operador es: [inicio: detener: paso], sin embargo, puede usarlo de cualquier otra manera para crear objetos personalizados.
Pero si llama a __getitem__
con esta sintaxis, ¿qué obtendrá como parámetro de índice? Esto es exactamente por qué existen los objetos de corte.
In : class Inspector: ...: def __getitem__(self, index): ...: print(index) ...: In : Inspector()[1] 1 In : Inspector()[1:2] slice(1, 2, None) In : Inspector()[1:2:3] slice(1, 2, 3) In : Inspector()[:] slice(None, None, None)
Incluso puede combinar la sintaxis de tuplas y sectores:
In : Inspector()[:, 0, :] (slice(None, None, None), 0, slice(None, None, None))
slice
no hace nada, solo almacena los atributos start
, stop
y step
.
In : s = slice(1, 2, 3) In : s.start Out: 1 In : s.stop Out: 2 In : s.step Out: 3
Interrupción de la rutina de Asyncio
Cualquier asyncio asyncio
ejecutable puede ser abortado usando el método cancel()
. En este caso, se enviará un CanceledError a la corutina, como resultado, esta y todas las corutinas asociadas con ella se interrumpirán hasta que el error sea detectado y eliminado.
CancelledError
es una subclase de Exception
, lo que significa que puede detectarse accidentalmente mediante una combinación de try ... except Exception
, diseñada para detectar "cualquier error". Para atrapar con seguridad un error de una rutina, debe hacer esto:
try: await action() except asyncio.CancelledError: raise except Exception: logging.exception('action failed')
Planificación de la ejecución
Para planificar la ejecución de algún código en un momento determinado, asyncio
generalmente crea una tarea que se ejecuta await asyncio.sleep(x)
:
import asyncio async def do(n=0): print(n) await asyncio.sleep(1) loop.create_task(do(n + 1)) loop.create_task(do(n + 1)) loop = asyncio.get_event_loop() loop.create_task(do()) loop.run_forever()
Pero crear una nueva tarea puede ser costoso, y no es necesario hacerlo si no planea realizar operaciones asincrónicas (como la función do
en mi ejemplo). En su lugar, puede usar las funciones loop.call_later
y loop.call_at
, que le permiten programar una llamada de devolución de llamada asíncrona:
import asyncio def do(n=0): print(n) loop = asyncio.get_event_loop() loop.call_later(1, do, n+1) loop.call_later(1, do, n+1) loop = asyncio.get_event_loop() do() loop.run_forever()