Esta es la tercera colección de consejos y programación de Python de mi
feed @pythonetc .
Selecciones anteriores:
Método de fábrica
Si crea nuevos objetos dentro de
__init__
, es más conveniente pasarlos listos como argumentos y usar el método de fábrica para crear el objeto. Esto separará la lógica empresarial de la implementación técnica de la creación de objetos.
En este ejemplo,
__init__
acepta
host
y
port
como argumentos para crear una conexión de base de datos:
class Query: def __init__(self, host, port): self._connection = Connection(host, port)
Opción de refactorización:
class Query: def __init__(self, connection): self._connection = connection @classmethod def create(cls, host, port): return cls(Connection(host, port))
Este enfoque tiene al menos las siguientes ventajas:
- La implementación es fácil. En las pruebas, puede hacer
Query(FakeConnection())
. - Una clase puede tener tantos métodos de fábrica como desee. Puede crear una conexión no solo usando el
host
y el port
, sino también clonando otra conexión, leyendo el archivo de configuración, usando la conexión predeterminada, etc. - Métodos de fábrica similares pueden convertirse en funciones asincrónicas, lo cual es absolutamente imposible de hacer con
__init__
.
super o siguiente
La función
super()
permite hacer referencia a la clase base. Esto puede ser muy útil en los casos en que la clase derivada quiera agregar algo a la implementación del método, en lugar de anularlo por completo.
class BaseTestCase(TestCase): def setUp(self): self._db = create_db() class UserTestCase(BaseTestCase): def setUp(self): super().setUp() self._user = create_user()
El nombre super no significa nada "super". En este contexto, significa "mayor jerarquía" (por ejemplo, como en la palabra "superintendente"). Al mismo tiempo,
super()
no siempre se refiere a la clase base; puede devolver fácilmente una clase secundaria. Por lo tanto, sería más correcto usar el nombre
next()
, ya que la siguiente clase se devuelve de acuerdo con el MRO.
class Top: def foo(self): return 'top' class Left(Top): def foo(self): return super().foo() class Right(Top): def foo(self): return 'right' class Bottom(Left, Right): pass
Recuerde que
super()
puede producir resultados diferentes dependiendo de dónde se llamó originalmente el método.
>>> Bottom().foo() 'right' >>> Left().foo() 'top'
Espacio de nombres personalizado para crear una clase
Una clase se crea en dos grandes pasos. Primero, se ejecuta el cuerpo de la clase, como el cuerpo de una función. En el segundo paso, la metaclase usa el espacio de nombres resultante (que es devuelto por
locals()
) (el valor predeterminado es
type
) para crear un objeto de la clase.
class Meta(type): def __new__(meta, name, bases, ns): print(ns) return super().__new__( meta, name, bases, ns ) class Foo(metaclass=Meta): B = 2
Este código muestra
{'__module__': '__main__', '__qualname__':'Foo', 'B': 3}
.
Obviamente, si introduce algo como
B = 2; B = 3
B = 2; B = 3
, entonces la metaclase verá solo
B = 3
, porque solo este valor está en
ns
. Esta limitación proviene del hecho de que la metaclase comienza a funcionar solo después de la ejecución del cuerpo.
Sin embargo, puede intervenir en el procedimiento de ejecución deslizando su propio espacio de nombres. Por defecto, se usa un diccionario simple, pero puede proporcionar su propio objeto, similar a un diccionario, si usa el método
__prepare__
de la metaclase.
class CustomNamespace(dict): def __setitem__(self, key, value): print(f'{key} -> {value}') return super().__setitem__(key, value) class Meta(type): def __new__(meta, name, bases, ns): return super().__new__( meta, name, bases, ns ) @classmethod def __prepare__(metacls, cls, bases): return CustomNamespace() class Foo(metaclass=Meta): B = 2 B = 3
Resultado de ejecución del código:
__module__ -> __main__ __qualname__ -> Foo B -> 2 B -> 3
Por
enum.Enum
tanto,
enum.Enum
protegido de la
duplicación .
matplotlib
matplotlib
es una biblioteca Python compleja y flexible para gráficos. Muchos productos lo admiten, incluidos Jupyter y Pycharm. Aquí hay un ejemplo de renderización de un fractal simple usando
matplotlib
:
https://repl.it/@VadimPushtaev/myplotlib (vea la imagen del título de esta publicación).
Soporte de zona horaria
Python proporciona una potente biblioteca de fecha y
datetime
para trabajar con fechas y horas. Es interesante que los objetos de
datetime
tengan una interfaz especial para admitir zonas horarias (es decir, el atributo
tzinfo
), pero este módulo tiene compatibilidad limitada para la interfaz mencionada, por lo que parte del trabajo se asigna a otros módulos.
El más popular de ellos es el
pytz
. Pero el hecho es que
pytz
no cumple totalmente con la interfaz
tzinfo
. Esto se afirma al comienzo de la documentación de
pytz
: "Esta biblioteca difiere de la API de Python documentada para implementaciones de tzinfo".
No puede usar objetos de
pytz
tzinfo
como
tzinfo
. Si intenta hacer esto, corre el riesgo de obtener un resultado completamente loco:
In : paris = pytz.timezone('Europe/Paris') In : str(datetime(2017, 1, 1, tzinfo=paris)) Out: '2017-01-01 00:00:00+00:09'
Tenga en cuenta el desplazamiento +00: 09. Pytz debe usarse así:
In : str(paris.localize(datetime(2017, 1, 1))) Out: '2017-01-01 00:00:00+01:00'
Además, después de cualquier operación aritmética, debe aplicar
normalize
a sus objetos de
datetime
para evitar cambios de desplazamiento (por ejemplo, en el borde del período DST).
In : new_time = time + timedelta(days=2) In : str(new_time) Out: '2018-03-27 00:00:00+01:00' In : str(paris.normalize(new_time)) Out: '2018-03-27 01:00:00+02:00'
Si tiene Python 3.6, la documentación recomienda usar
dateutil.tz
lugar de
pytz
. Esta biblioteca es totalmente compatible con
tzinfo
, se puede pasar como un atributo y no necesita usar
normalize
. Es cierto que funciona más lentamente.
Si desea saber por qué
pytz
no admite la API de
datetime
, o desea ver más ejemplos, lea
este artículo.
Magic StopIteration
Cada vez que
next(x)
llama a
next(x)
, devuelve un nuevo valor desde el iterador
x
hasta que se lanza una excepción. Si resulta ser
StopIteration
, entonces el iterador está agotado y ya no puede proporcionar valores. Si el generador está iterado, al final del cuerpo lanzará automáticamente
StopIteration
:
>>> def one_two(): ... yield 1 ... yield 2 ... >>> i = one_two() >>> next(i) 1 >>> next(i) 2 >>> next(i) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
StopIteration
puede manejarse automáticamente mediante herramientas que llaman a
next
:
>>> list(one_two()) [1, 2]
Pero el problema es que cualquier StopIteration obviamente no esperado que surgió en el cuerpo del generador se tomará en silencio como una señal del final del generador, y no como un error, como cualquier otra excepción:
def one_two(): yield 1 yield 2 def one_two_repeat(n): for _ in range(n): i = one_two() yield next(i) yield next(i) yield next(i) print(list(one_two_repeat(3)))
Aquí, el último
yield
es un error: una excepción
StopIteration
lanzada detiene la iteración de la
list(...)
. Obtenemos el resultado
[1, 2]
. Sin embargo, en Python 3.7 este comportamiento ha cambiado. Alien
StopIteration
reemplazado con
RuntimeError
:
Traceback (most recent call last): File "test.py", line 10, in one_two_repeat yield next(i) StopIteration The above exception was the direct cause of the following exception: Traceback (most recent call last): File "test.py", line 12, in <module> print(list(one_two_repeat(3))) RuntimeError: generator raised StopIteration
Puede usar
__future__ import generator_stop
para habilitar el mismo comportamiento desde Python 3.5.