Capítulo 1. El modelo de datos de Python
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
El sentido que tiene Guido de la estética del diseño de lenguajes es asombroso. He conocido a muchos buenos diseñadores de lenguajes capaces de construir lenguajes teóricamente bellos que nadie utilizaría jamás, pero Guido es una de esas raras personas capaces de construir un lenguaje que es sólo un poco menos bello teóricamente, pero con el que da gusto escribir programas.
Jim Hugunin, creador de Jython, cocreador de AspectJ y arquitecto del DLR de .Net1
Una de las mejores cualidades de Python es su consistencia. Después de trabajar con Python durante un tiempo, eres capaz de empezar a hacer conjeturas informadas y correctas sobre características que son nuevas para ti.
Sin embargo, si aprendiste otro lenguaje orientado a objetos antes que Python, puede que te resulte extraño utilizar len(collection)
en lugar de collection.len()
. Esta aparente rareza es la punta de un iceberg que, bien entendido, es la clave de todo lo que llamamos Python. El iceberg se llama Modelo de Datos de Python, y es la API que utilizamos para que nuestros propios objetos funcionen bien con lascaracterísticas más idiomáticas del lenguaje.
Puedes pensar en el modelo de datos como una descripción de Python como marco de trabajo. Formaliza las interfaces de los bloques de construcción del propio lenguaje, como secuencias, funciones, iteradores, coroutines, clases, gestores de contexto, etc.
Cuando utilizamos un framework, pasamos mucho tiempo codificando métodos que son llamados por el framework. Lo mismo ocurre cuando aprovechamos el Modelo de Datos de Python para construir nuevas clases. El intérprete de Python invoca los métodos especiales para realizar operaciones básicas con objetos, a menudo provocadas por una sintaxis especial. Los nombres de los métodos especiales se escriben siempre con doble guión bajo inicial y final. Por ejemplo, la sintaxis obj[key]
está soportada por el método especial __getitem__
. Para evaluar my_collection[key]
, el intérprete llama a my_collection.__getitem__(key)
.
Implementamos métodos especiales cuando queremos que nuestros objetos admitan e interactúen con construcciones fundamentales del lenguaje, como:
-
Colecciones
-
Acceso a atributos
-
Iteración (incluida la iteración asíncrona mediante
async for
) -
Sobrecarga de operadores
-
Invocación de funciones y métodos
-
Representación y formateo de cadenas
-
Programación asíncrona con
await
-
Creación y destrucción de objetos
-
Contextos gestionados mediante las sentencias
with
oasync with
Magia y Dunder
El término método mágico es argot para método especial, pero ¿cómo hablamos de un método específico como __getitem__
? Aprendí a decir "dunder-getitem" del autor y profesor Steve Holden. "Dunder" es un atajo para "doble guión bajo antes y después". Por eso los métodos especiales también se conocen como métodos dunder. El capítulo "Análisis Léxico"de La Referencia del Lenguaje Python advierte que "Cualquier uso de los nombres __*__
, en cualquier contexto, que no siga el uso explícitamente documentado, está sujeto a rotura sin previo aviso."
Novedades de este capítulo
Este capítulo de ha sufrido pocos cambios respecto a la primera edición, ya que se trata de una introducción al Modelo de Datos de Python, que es bastante estable. Los cambios más significativos son:
-
Métodos especiales que soportan la programación asíncrona y otras novedades, añadidas a las tablas de "Visión general de los métodos especiales".
-
Figura 1-2 que muestra el uso de métodos especiales en la "API de colecciones", incluida la clase base abstracta
collections.abc.Collection
introducida en Python 3.6.
Además, aquí y en toda esta segunda edición he adoptado la sintaxis de cadena f introducida en Python 3.6, que es más legible y a menudo más cómoda que las antiguas notaciones de formato de cadena: el método str.format()
y el operador %
.
Consejo
Una razón para seguir utilizando my_fmt.format()
es cuando la definición de my_fmt
debe estar en un lugar del código distinto de donde debe producirse la operación de formateo. Por ejemplo, cuando my_fmt
tiene varias líneas y es mejor definirla en una constante, o cuando debe proceder de un archivo de configuración, o de la base de datos. Ésas son necesidades reales, pero no ocurren muy a menudo.
Una baraja pitónica
El ejemplo 1-1 es sencillo, pero demuestra la potencia de implementar sólo dos métodos especiales, __getitem__
y __len__
.
Ejemplo 1-1. Una baraja como secuencia de naipes
import
collections
Card
=
collections
.
namedtuple
(
'Card'
,
[
'rank'
,
'suit'
])
class
FrenchDeck
:
ranks
=
[
str
(
n
)
for
n
in
range
(
2
,
11
)]
+
list
(
'JQKA'
)
suits
=
'spades diamonds clubs hearts'
.
split
()
def
__init__
(
self
):
self
.
_cards
=
[
Card
(
rank
,
suit
)
for
suit
in
self
.
suits
for
rank
in
self
.
ranks
]
def
__len__
(
self
):
return
len
(
self
.
_cards
)
def
__getitem__
(
self
,
position
):
return
self
.
_cards
[
position
]
La primera cosa que hay que observar es el uso de collections.namedtuple
para construir una clase sencilla que represente cartas individuales. Utilizamos namedtuple
para construir clases de objetos que son sólo conjuntos de atributos sin métodos personalizados, como un registro de base de datos. En el ejemplo, lo utilizamos para proporcionar una bonita representación de las cartas de la baraja, como se muestra en la sesión de consola:
>>>
beer_card
=
Card
(
'7'
,
'diamonds'
)
>>>
beer_card
Card(rank='7', suit='diamonds')
Pero lo importante de este ejemplo es la clase FrenchDeck
. Es corta, pero tiene mucha fuerza. En primer lugar, como cualquier colección estándar de Python, una baraja responde a la función len()
devolviendo el número de cartas que contiene:
>>>
deck
=
FrenchDeck
()
>>>
len
(
deck
)
52
Leer cartas concretas de la baraja -por ejemplo, la primera o la última- es fácil, gracias al método __getitem__
:
>>>
deck
[
0
]
Card(rank='2', suit='spades')
>>>
deck
[
-
1
]
Card(rank='A', suit='hearts')
¿Deberíamos crear un método para elegir una carta al azar? No es necesario. Python ya tiene una función para obtener un elemento aleatorio de una secuencia: random.choice
. Podemos utilizarla en una instancia de baraja:
>>>
from
random
import
choice
>>>
choice
(
deck
)
Card(rank='3', suit='hearts')
>>>
choice
(
deck
)
Card(rank='K', suit='spades')
>>>
choice
(
deck
)
Card(rank='2', suit='clubs')
Acabamos de ver en dos ventajas de utilizar métodos especiales para aprovechar el Modelo de Datos de Python:
-
Los usuarios de tus clases no tienen que memorizar nombres arbitrarios de métodos para operaciones estándar. ("¿Cómo obtener el número de elementos? ¿Es
.size()
,.length()
, o qué?") -
Es más fácil beneficiarse de la rica biblioteca estándar de Python y evitar reinventar la rueda, como la función
random.choice
.
Pero la cosa mejora.
Como nuestro __getitem__
delega en el operador []
de self._cards
, nuestra baraja admite automáticamente el troceado. Así es como miramos las tres cartas superiores de una baraja nueva, y luego elegimos sólo los ases empezando en el índice 12 y saltándonos 13 cartas cada vez:
>>>
deck
[:
3
]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>>
deck
[
12
::
13
]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]
Sólo con implementar el método especial __getitem__
, nuestra baraja también es iterable:
>>>
for
card
in
deck
:
# doctest: +ELLIPSIS
...
(
card
)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
...
También podemos iterar sobre el mazo a la inversa:
>>>
for
card
in
reversed
(
deck
):
# doctest: +ELLIPSIS
...
(
card
)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
...
Elipsis en doctests
Siempre que fue posible , extraje los listados de la consola Python de este libro dedoctest
Cuando la salida era demasiado larga, la parte elidida se marcaba con una elipsis (...
), como en la última línea del código anterior. En esos casos, utilicé la directiva # doctest: +ELLIPSIS
para superar el doctest. Si pruebas estos ejemplos en la consola interactiva, puedes omitir los comentarios del doctest.
La iteración suele ser implícita. Si una colección no tiene un método __contains__
, el operador in
realiza un barrido secuencial. Un ejemplo: in
funciona con nuestra clase FrenchDeck
porque es iterable. Compruébalo:
>>>
Card
(
'Q'
,
'hearts'
)
in
deck
True
>>>
Card
(
'7'
,
'beasts'
)
in
deck
False
¿Qué tal la clasificación? Un sistema común de clasificación de cartas es por rango (siendo los ases los más altos), y luego por palo en el orden de picas (el más alto), corazones, diamantes y tréboles (el más bajo). Aquí tienes una función que clasifica las cartas según esa regla, devolviendo 0
para el 2 de tréboles y 51
para el as de picas:
suit_values
=
dict
(
spades
=
3
,
hearts
=
2
,
diamonds
=
1
,
clubs
=
0
)
def
spades_high
(
card
):
rank_value
=
FrenchDeck
.
ranks
.
index
(
card
.
rank
)
return
rank_value
*
len
(
suit_values
)
+
suit_values
[
card
.
suit
]
Teniendo en cuenta spades_high
, ahora podemos enumerar nuestra baraja en orden creciente:
>>>
for
card
in
sorted
(
deck
,
key
=
spades_high
):
# doctest: +ELLIPSIS
...
(
card
)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
...
(
46
cards
omitted
)
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')
Aunque FrenchDeck
hereda implícitamente de la clase object
, la mayor parte de su funcionalidad no se hereda, sino que proviene de aprovechar el modelo de datos y la composición. Al implementar los métodos especiales __len__
y __getitem__
, nuestra FrenchDeck
se comporta como una secuencia estándar de Python, lo que le permite beneficiarse de las características principales del lenguaje (por ejemplo, la iteración y el troceado) y de la biblioteca estándar, como muestran los ejemplos que utilizan random.choice
,reversed
sorted
Gracias a la composición, las implementaciones de __len__
y __getitem__
pueden delegar todo el trabajo en un objeto list
,self._cards
.
¿Qué te parece barajar?
Tal y como se ha implementado hasta ahora, un FrenchDeck
no se puede barajar porque es inmutable: las cartas y sus posiciones no se pueden cambiar, salvo violando la encapsulación y manejando directamente el atributo _cards
. En el Capítulo 13, lo arreglaremos añadiendo un método __setitem__
de una sola línea.
Cómo se utilizan los métodos especiales
La primera cosa que debes saber sobre los métodos especiales es que están pensados para ser llamados por el intérprete de Python, y no por ti. No escribes my_object.__len__()
. Escribes len(my_object)
y, si my_object
es una instancia de una clase definida por el usuario, entonces Python llama al método __len__
que has implementado.
Pero el intérprete toma un atajo cuando trata con tipos incorporados como list
, str
, bytearray
, o extensiones como las matrices de NumPy. Las colecciones de tamaño variable de Python escritas en C incluyen una estructura2
llamado PyVarObject
, que tiene un campo ob_size
que contiene el número de elementos de la colección. Por tanto, si my_object
es una instancia de uno de esos constructores, entonces len(my_object)
recupera el valor del campo ob_size
, y esto es mucho más rápido que llamar a un método.
La mayoría de las veces, la llamada al método especial es implícita. Por ejemplo, la sentencia for i in x:
en realidad provoca la invocación de iter(x)
, que a su vez puede llamar a x.__iter__()
si está disponible, o utilizar x.__getitem__()
, como en el ejemplo FrenchDeck
.
Normalmente, tu código no debería tener muchas llamadas directas a métodos especiales. A menos que estés haciendo mucha metaprogramación, deberías implementar métodos especiales más a menudo que invocarlos explícitamente. El único método especial al que el código de usuario llama directamente con frecuencia es __init__
para invocar el inicializador de la superclase en tu propia implementación __init__
.
Si necesitas invocar un método especial, suele ser mejor llamar a la función incorporada correspondiente (por ejemplo, len
, iter
, str
, etc.). Estas funciones incorporadas llaman al método especial correspondiente, pero a menudo proporcionan otros servicios y -para los tipos incorporados- son más rápidas que las llamadas a métodos. Consulta, por ejemplo, "Utilizar iter con un Callable" en el capítulo 17.
En los próximos apartados, veremos algunos de los usos más importantes de los métodos especiales:
-
Emular tipos numéricos
-
Representación en cadena de los objetos
-
Valor booleano de un objeto
-
Implantar colecciones
Emular tipos numéricos
Varios métodos especiales de permiten que los objetos de usuario respondan a operadores como +
. Lo veremos con más detalle en el capítulo 16, pero aquí nuestro objetivo es ilustrar mejor el uso de los métodos especiales mediante otro ejemplo sencillo.
En implementaremos una clase para representar vectores bidimensionales, es decir, vectores euclidianos como los que se utilizan en matemáticas y física (véase la Figura 1-1).
Consejo
El tipo incorporado complex
puede utilizarse para representar vectores bidimensionales, pero nuestra clase puede ampliarse para representar vectores n-dimensionales. Lo haremos en el Capítulo 17.
Empezaremos a diseñar la API de una clase de este tipo escribiendo una sesión de consola simulada que podremos utilizar más adelante como doctest. El siguiente fragmento de código prueba la suma de vectores que se muestra en la Figura 1-1:
>>>
v1
=
Vector
(
2
,
4
)
>>>
v2
=
Vector
(
2
,
1
)
>>>
v1
+
v2
Vector(4, 5)
Observa en cómo el operador +
da como resultado un nuevo Vector
, que se muestra en un formato amigable en la consola.
La función incorporada abs
devuelve el valor absoluto de enteros y flotantes, y la magnitud de complex
números, por lo que, para ser coherentes, nuestra API también utiliza abs
para calcular la magnitud de un vector:
>>>
v
=
Vector
(
3
,
4
)
>>>
abs
(
v
)
5.0
Nosotros también podemos implementar el operador *
para realizar multiplicaciones escalares (es decir, multiplicar un vector por un número para obtener un nuevo vector con la misma dirección y una magnitudmultiplicada ):
>>>
v
*
3
Vector(9, 12)
>>>
abs
(
v
*
3
)
15.0
El ejemplo 1-2 es una clase Vector
que implementa las operaciones que acabamos de describir, mediante el uso de los métodos especiales __repr__
, __abs__
, __add__
, y __mul__
.
Ejemplo 1-2. Una clase vectorial bidimensional simple
"""
vector2d.py: a simplistic class demonstrating some special methods
It is simplistic for didactic reasons. It lacks proper error handling,
especially in the ``__add__`` and ``__mul__`` methods.
This example is greatly expanded later in the book.
Addition::
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)
Absolute value::
>>> v = Vector(3, 4)
>>> abs(v)
5.0
Scalar multiplication::
>>> v * 3
Vector(9, 12)
>>> abs(v * 3)
15.0
"""
import
math
class
Vector
:
def
__init__
(
self
,
x
=
0
,
y
=
0
):
self
.
x
=
x
self
.
y
=
y
def
__repr__
(
self
):
return
f
'Vector(
{self.x!r}
,
{self.y!r}
)'
def
__abs__
(
self
):
return
math
.
hypot
(
self
.
x
,
self
.
y
)
def
__bool__
(
self
):
return
bool
(
abs
(
self
))
def
__add__
(
self
,
other
):
x
=
self
.
x
+
other
.
x
y
=
self
.
y
+
other
.
y
return
Vector
(
x
,
y
)
def
__mul__
(
self
,
scalar
):
return
Vector
(
self
.
x
*
scalar
,
self
.
y
*
scalar
)
Hemos implementado cinco métodos especiales, además del conocido __init__
. Observa que ninguno de ellos se llama directamente dentro de la clase o en el uso típico de la clase ilustrado por los doctests. Como ya hemos dicho, el intérprete de Python es el único llamador frecuente de la mayoría de los métodos especiales.
El Ejemplo 1-2 implementa dos operadores: +
y *
, para mostrar el uso básico de __add__
y __mul__
. En ambos casos, los métodos crean y devuelven una nueva instancia de Vector
, y no modifican ninguno de los operandos -self
o other
se limitan a leerlos. Éste es el comportamiento esperado de los operadores infijos: crear nuevos objetos y no tocar sus operandos. Tendré mucho más que decir al respecto en el Capítulo 16.
Advertencia
Tal como está implementado, el Ejemplo 1-2 permite multiplicar un Vector
por un número, pero no un número por un Vector
, lo que viola la propiedad conmutativa de la multiplicación escalar. Lo arreglaremos con el método especial __rmul__
en el Capítulo 16.
En los apartados siguientes, hablaremos de otros métodos especiales en Vector
.
Representación de cadenas
El método especial __repr__
es llamado por el repr
incorporado para obtener la representación en cadena del objeto para su inspección. Sin un __repr__
personalizado, la consola de Python mostraría una instancia Vector
<Vector object at 0x10e100070>
.
La consola interactiva y el depurador llaman a repr
sobre los resultados de las expresiones evaluadas, al igual que el marcador de posición %r
en el formateo clásico con el operador %
, y el !r
campo de conversión en la nueva sintaxis de cadena de formato utilizada en f-cadenas el método str.format
.
Observa que la cadena f de nuestro __repr__
utiliza !r
para obtener la representación estándar de los atributos que se van a mostrar. Esto es una buena práctica, porque muestra la diferencia crucial entre Vector(1, 2)
y Vector('1', '2')
-este último no funcionaría en el contexto de este ejemplo, porque los argumentos del constructor deben ser números, no str
.
La cadena devuelta por __repr__
debe ser inequívoca y, a ser posible, coincidir con el código fuente necesario para volver a crear el objeto representado. Por eso, nuestra representación Vector
se parece a llamar al constructor de la clase (por ejemplo, Vector(3, 4)
).
En el contraste , __str__
es llamado por el incorporado str()
y utilizado implícitamente por la función print
. Debe devolver una cadena adecuada para mostrarla a los usuarios finales.
A veces, la misma cadena devuelta por __repr__
es fácil de usar, y no necesitas codificar __str__
porque la implementación heredada de la clase object
llama a __repr__
como alternativa.El ejemplo 5-2 es uno de los varios ejemplos de este libro con un __str__
personalizado.
Consejo
Los programadores con experiencia previa en lenguajes con un método toString
tienden a implementar __str__
y no __repr__
. Si sólo implementas uno de estos métodos especiales en Python, elige __repr__
.
"¿Cuál es la diferencia entre __str__
y __repr__
en Python?" es una pregunta de Stack Overflow con excelentes contribuciones de los pythonistas Alex Martelli y Martijn Pieters.
Valor booleano de un tipo personalizado
Aunque Python tiene un tipo bool
, acepta cualquier objeto en un contexto booleano, como la expresión que controla una sentencia if
o while
, o como operandos de and
, or
y not
. Para determinar si un valor x
es verdadero o falso, Python aplica bool(x)
, que devuelve True
o False
.
Por defecto, las instancias de las clases definidas por el usuario se consideran verdaderas, a menos que __bool__
o __len__
estén implementadas. Básicamente, bool(x)
llama a x.__bool__()
y utiliza el resultado. Si __bool__
no está implementada, Python intenta invocar a x.__len__()
, y si eso devuelve cero, bool
devuelve False
. De lo contrario bool
devuelve True
.
Nuestra implementación de __bool__
es conceptualmente sencilla: devuelve False
si la magnitud del vector es cero, True
en caso contrario. Convertimos la magnitud a un booleano utilizando bool(abs(self))
porque se espera que __bool__
devuelva un booleano. Fuera de los métodos de __bool__
, rara vez es necesario llamar explícitamente a bool()
, porque cualquier objeto puede utilizarse en un contexto booleano.
Observa cómo el método especial __bool__
permite que tus objetos sigan las reglas de comprobación del valor de verdad definidas enel capítulo "Tipos incorporados"de la documentación La biblioteca estándar de Python.
Nota
Una aplicación más rápida de Vector.__bool__
es ésta:
def
__bool__
(
self
):
return
bool
(
self
.
x
or
self
.
y
)
Esto es más difícil de leer, pero evita el viaje a través de abs
, __abs__
, los cuadrados y la raíz cuadrada. La conversión explícita a bool
es necesaria porque __bool__
debe devolver un booleano, yor
devuelve cualquiera de los operandos tal cual: x or y
evalúa a x
si es verdadero, de lo contrario el resultado es y
, sea lo que sea.
API de recogida
La Figura 1-2 documenta en las interfaces de los tipos de colección esenciales del lenguaje. Todas las clases del diagrama sonclases base abstractas (ABC). Las ABC y el módulo collections.abc
se tratan en el Capítulo 13. El objetivo de esta breve sección es ofrecer una visión panorámica de las interfaces de colección más importantes de Python, mostrando cómo se construyen a partir de métodos especiales.
El ABC Collection
(nuevo en Python 3.6) unifica las tres interfaces esenciales que toda colección debeimplementar:
-
Iterable
para que admitafor
, desempaquetado y otras formas deiteración
Python no exige que las clases concretas hereden realmente de ninguno de estos ABC. Cualquier clase que implemente __len__
satisface la interfaz Sized
.
Tres especializaciones muy importantes de Collection
son:
-
Sequence
formalizando la interfaz de componentes comolist
ystr
-
Mapping
implementado pordict
,collections.defaultdict
, etc. -
Set
la interfaz de los tipos incorporadosset
yfrozenset
Sólo Sequence
es Reversible
, porque las secuencias admiten el orden arbitrario de sus contenidos, mientras que las correspondencias y los conjuntos no.
Nota
Desde Python 3.7, el tipo dict
es oficialmente "ordenado", pero eso sólo significa que se conserva el orden de inserción de las claves. No puedesreordenar las claves de un dict
como quieras.
Todos los métodos especiales del ABC Set
implementan operadores infijos. Por ejemplo,a & b
calcula la intersección de los conjuntos a
y b
, y se implementa en el método especial __and__
.
Los dos capítulos siguientes tratarán en detalle las secuencias, asignaciones y conjuntos de la biblioteca estándar.
Consideremos ahora las principales categorías de métodos especiales definidos en el Modelo de Datos de Python.
Visión general de los métodos especiales
En el capítulo "Modelo de datos" de La Referencia del Lenguaje Python se enumeran más de 80 nombres de métodos especiales. Más de la mitad de ellos implementan operadores aritméticos, bit a bit y de comparación. Como resumen de lo que hay disponible, consulta las tablas siguientes.
La Tabla 1-1muestra los nombres de los métodos especiales, excluyendo los utilizados para implementar operadores infijos o funciones matemáticas básicas como abs
. La mayoría de estos métodos se tratarán a lo largo del libro, incluidas las incorporaciones más recientes: métodos especiales asíncronos como __anext__
(añadido en Python 3.5), y el gancho de personalización de clases, __init_subclass__
(de Python 3.6).
Categoría | Nombres de los métodos |
---|---|
Representación cadena/bytes |
|
Conversión a número |
|
Emular colecciones |
|
Iteración |
|
Ejecución invocable o coroutine |
|
Gestión del contexto |
|
Creación y destrucción de instancias |
|
Gestión de atributos |
|
Descriptores de atributos |
|
Clases base abstractas |
|
Metaprogramación de clases |
|
Los operadores infijos y numéricos están soportados por los métodos especiales enumerados enla Tabla 1-2. Aquí los nombres más recientes son __matmul__
, __rmatmul__
, y __imatmul__
, añadidos en Python 3.5 para soportar el uso de @
como operador infijo para la multiplicación de matrices, como veremos en el Capítulo 16.
Categoría de operador | Símbolos | Nombres de los métodos |
---|---|---|
Numérico unario |
|
|
Rica comparación |
|
|
Aritmética |
|
|
Aritmética inversa |
(operadores aritméticos con operandos intercambiados) |
|
Aritmética de asignación aumentada |
|
|
Bitwise |
|
|
Bit a bit invertido |
(operadores bit a bit con operandos intercambiados) |
|
Asignación aumentada bit a bit |
|
|
Nota
Python llama a un método especial de operador inverso en el segundo operando cuando no se puede utilizar el método especial correspondiente en el primer operando. Las asignaciones aumentadas son atajos que combinan un operador infijo con una asignación de variable, por ejemplo, a += b
.
El capítulo 16 explica detalladamente los operadores inversos y la asignación aumentada.
Por qué el len no es un método
Yo hice esta pregunta al desarrollador del núcleo Raymond Hettinger en 2013, y la clave de su respuesta fue una cita de "El Zen de Python":"En "Cómo se utilizan los métodos especiales", describí cómo len(x)
se ejecuta muy rápido cuando x
es una instancia de un tipo incorporado. No se llama a ningún método para los objetos incorporados de CPython: la longitud simplemente se lee de un campo de una estructura C. Obtener el número de elementos de una colección es una operación común y debe funcionar eficientemente para tipos tan básicos y diversos como str
, list
, memoryview
, etc.
En otras palabras, len
no se llama como método porque recibe un tratamiento especial como parte del Modelo de Datos de Python, al igual que abs
. Pero gracias al método especial __len__
, también puedes hacer que len
funcione con tus propios objetos personalizados. Se trata de un compromiso justo entre la necesidad de objetos incorporados eficientes y la coherencia del lenguaje. También de "El Zen de Python": "Los casos especiales no son tan especiales como para romper las reglas".
Nota
Si piensas en abs
y len
como operadores unarios, puede que te sientas más inclinado a perdonar su aspecto funcional, en contraposición a la sintaxis de llamada a métodos que cabría esperar en un lenguaje orientado a objetos.
De hecho, el lenguaje ABC -un antepasado directo de Python que fue pionero en muchas de sus características- tenía un operador #
que era el equivalente a len
(escribirías #s
). Cuando se utilizaba como operador infijo, escrito x#s
, contaba las apariciones de x
en s
, que en Python obtienes como s.count(x)
, para cualquier secuencia s
.
Resumen del capítulo
Mediante la implementación de métodos especiales en, tus objetos pueden comportarse como los tipos incorporados, permitiendo el estilo de codificación expresivo que la comunidad considera pitónico.
Un requisito básico para un objeto Python es proporcionar representaciones de cadena utilizables de sí mismo, una utilizada para la depuración y el registro, y otra para la presentación a los usuarios finales. Por eso existen los métodos especiales __repr__
y __str__
en el modelo de datos.
Emular secuencias, como se muestra con el ejemplo de FrenchDeck
, es uno de los usos más comunes de los métodos especiales. Por ejemplo, las bibliotecas de bases de datos suelen devolver resultados de consultas envueltos en colecciones similares a secuencias. Aprovechar al máximo los tipos de secuencias existentes es el tema del Capítulo 2. Implementar tus propias secuencias se tratará en el Capítulo 12, cuando creemos una extensión multidimensional de la clase Vector
.
Gracias a la sobrecarga de operadores, Python ofrece una rica selección de tipos numéricos, desde los incorporados a decimal.Decimal
y fractions.Fraction
, todos ellos compatibles con operadores aritméticos infijos. Las bibliotecas de ciencia de datos NumPy admiten operadores infijoscon matrices y tensores. La implementación de operadores -incluidos los operadores inversos y la asignación aumentada- se mostrará en el capítulo 16 mediante mejoras del ejemploVector
ejemplo.
El uso y la implementación de la mayoría de los restantes métodos especiales del Modelo de Datos de Python se tratan a lo largo de este libro.
Otras lecturas
El capítulo "Modelo de datos" de La Referencia del Lenguaje Python es la fuente canónica para el tema de este capítulo y de gran parte de este libro.
Python in a Nutshell, 3ª ed. de Alex Martelli, Anna Ravenscroft y Steve Holden (O'Reilly) tiene una excelente cobertura del modelo de datos. Su descripción de la mecánica del acceso a atributos es la más fidedigna que he visto aparte del códigofuente en C de CPython. Martelli es también un prolífico colaborador de Stack Overflow,con más de 6.200 respuestas publicadas. Consulta su perfil de usuario en Stack Overflow.
David Beazley tiene dos libros que tratan en detalle el modelo de datos en el contexto de Python 3: Python Essential Reference, 4ª ed. (Addison-Wesley), y Python Cookbook, 3ª ed. (O'Reilly), en coautoría con Brian K. Jones.
El arte del protocolode meta objetos (MIT Press), de Gregor Kiczales, Jim des Rivieres y Daniel G. Bobrow, explica el concepto de protocolo de metaobjetos, del que el Modelo de Datos de Python es un ejemplo.
1 "Historia de Jython", escrito como prólogo a Jython Essentials por Samuele Pedroni y Noel Rappin (O'Reilly).
2 Una estructura C es un tipo de registro con campos con nombre.
Get Python fluido, 2ª edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.