
Es una nueva selección de consejos y trucos sobre Python y la programación de mi canal de Telegram @pythonetc.
←
Publicaciones anterioresSi una instancia de una clase no tiene un atributo con el nombre dado, intenta acceder al atributo de clase con el mismo nombre.
>>> class A: ... x = 2 ... >>> Ax 2 >>> A().x 2
Es bastante simple para una instancia tener un atributo que una clase no tiene o tener el atributo con un valor diferente:
>>> class A: ... x = 2 ... def __init__(self): ... self.x = 3 ... self.y = 4 ... >>> A().x 3 >>> Ax 2 >>> A().y 4 >>> Ay AttributeError: type object 'A' has no attribute 'y'
Sin embargo, si no es tan simple, si desea que una instancia se comporte como si no tuviera un atributo a pesar de que la clase la tenga. Para que esto suceda, debe crear un descriptor personalizado que no permita el acceso de acceso desde la instancia:
class ClassOnlyDescriptor: def __init__(self, value): self._value = value self._name = None
Vea también cómo funciona el decorador de
classonlymethod
Django:
https://github.com/django/django/blob/b709d701303b3877387020c1558a590713b09853/django/utils/decorators.py#L6Las funciones declaradas en un cuerpo de clase no pueden ver el alcance de la clase. Tiene sentido ya que el alcance de la clase solo existe durante la creación de la clase.
>>> class A: ... x = 2 ... def f(): ... print(x) ... f() ... [...] NameError: name 'x' is not defined
Eso generalmente no es un problema: los métodos se declaran dentro de una clase solo para convertirse en métodos y se los llamará más tarde:
>>> class A: ... x = 2 ... def f(self): ... print(self.x) ... >>> >>> >>> A().f() 2
Sorprendentemente, lo mismo es cierto para las comprensiones. Tienen sus propios ámbitos y tampoco pueden acceder al ámbito de la clase. Eso realmente tiene sentido para las comprensiones del generador: evalúan las expresiones una vez que la creación de la clase ya ha finalizado.
>>> class A: ... x = 2 ... y = [x for _ in range(5)] ... [...] NameError: name 'x' is not defined
Las comprensiones, sin embargo, no tienen acceso a
self
. La única forma de hacerlo funcionar es agregar un alcance más (sí, eso es feo):
>>> class A: ... x = 2 ... y = (lambda x=x: [x for _ in range(5)])() ... >>> Ay [2, 2, 2, 2, 2]
En Python,
None
es igual a
None
por lo que parece que puede verificar
None
con
==
:
ES_TAILS = ('s', 'x', 'z', 'ch', 'sh') def make_plural(word, exceptions=None): if exceptions == None:
Sin embargo, esto es algo incorrecto.
None
es igual a
None
, pero no es lo único que es. Los objetos personalizados también pueden ser iguales a
None
:
>>> class A: ... def __eq__(self, other): ... return True ... >>> A() == None True >>> A() is None False
La única forma adecuada de comparar con
None
es usar
is None
.
Los flotadores de Python pueden tener valores NaN. Puedes conseguir uno con
math.nan
.
nan
no es igual a nada, incluido a sí mismo:
>>> math.nan == math.nan False
Además, el objeto NaN no es único, puede tener varios objetos NaN diferentes de diferentes fuentes:
>>> float('nan') nan >>> float('nan') is float('nan') False
Eso significa que generalmente no puede usar NaN como clave de diccionario:
>>> d = {} >>> d[float('nan')] = 1 >>> d[float('nan')] = 2 >>> d {nan: 1, nan: 2}
typing
permite definir el tipo de generadores. También puede especificar qué tipo se produce, qué tipo se puede enviar a un generador y qué se devuelve.
Generator[int, None, bool]
es un generador que produce enteros, devuelve un valor booleano y no admite
g.send()
.
Aquí hay un ejemplo un poco más complicado.
chain_while
produce de otros generadores hasta que uno de ellos devuelve algo que es una señal para detenerse de acuerdo con la función de
condition
:
from typing import Generator, Callable, Iterable, TypeVar Y = TypeVar('Y') S = TypeVar('S') R = TypeVar('R') def chain_while( iterables: Iterable[Generator[Y, S, R]], condition: Callable[[R], bool], ) -> Generator[Y, S, None]: for it in iterables: result = yield from it if not condition(result): break def r(x: int) -> Generator[int, None, bool]: yield from range(x) return x % 2 == 1 print(list(chain_while( [ r(5), r(4), r(3), ], lambda x: x is True, )))
Anotar un método de fábrica no es tan simple como parece. La urgencia inmediata es usar algo como esto:
class A: @classmethod def create(cls) -> 'A': return cls()
Sin embargo, eso no es lo correcto. El problema es que
create
no devuelve
A
, devuelve una instancia de cls que es
A
o cualquiera de sus descendientes. Mira este código:
class A: @classmethod def create(cls) -> 'A': return cls() class B(A): @classmethod def create(cls) -> 'B': return super().create()
El resultado de la comprobación mypy es un
error: Incompatible return value type (got "A", expected "B")
. Una vez más, el problema es
super().create()
se anota para devolver
A
mientras que claramente devuelve
B
en este caso.
Puede arreglar eso anotando cls con TypeVar:
AType = TypeVar('AType') BType = TypeVar('BType') class A: @classmethod def create(cls: Type[AType]) -> AType: return cls() class B(A): @classmethod def create(cls: Type[BType]) -> BType: return super().create()
Ahora
create
devuelve la instancia de la clase
cls
. Sin embargo, estas anotaciones son demasiado flojas, perdimos la información de que
cls
es un subtipo de
A
:
AType = TypeVar('AType') class A: DATA = 42 @classmethod def create(cls: Type[AType]) -> AType: print(cls.DATA) return cls()
El error es
"Type[AType]" has no attribute "DATA"
.
Para solucionarlo, debe definir explícitamente
AType
como un subtipo de
A
con el argumento
bound
de
TypeVar
:
AType = TypeVar('AType', bound='A') BType = TypeVar('BType', bound='B') class A: DATA = 42 @classmethod def create(cls: Type[AType]) -> AType: print(cls.DATA) return cls() class B(A): @classmethod def create(cls: Type[BType]) -> BType: return super().create()