Conjuntos de objetos

Esta es una versión antigua del libro ahora conocido como Think Python. Quizá prefiera leer una versión más reciente.

Cómo pensar como un informático

Capítulo 15

15.1 Composición

A estas alturas, has visto varios ejemplos de composición.Uno de los primeros ejemplos fue el uso de una invocación a un método como parte de una anexión. Otro ejemplo es la estructura anidada de las sentencias; puede poner una sentencia if dentro de un bucle while, dentro de otra sentencia if, y así sucesivamente.

Habiendo visto este patrón, y habiendo aprendido sobre listas y objetos, no debería sorprenderse al saber que puede crear listas de objetos. También se pueden crear objetos que contengan listas (como atributos); se pueden crear listas que contengan listas; se pueden crear objetos que contengan objetos; y así sucesivamente.

En este capítulo y en el siguiente, veremos algunos ejemplos de estascombinaciones, utilizando los objetos Card como ejemplo.

15.2 Objetos carta

Si no estás familiarizado con los naipes comunes, ahora sería un buen momento para conseguir una baraja, o de lo contrario este capítulo podría no tener mucho sentido.Hay cincuenta y dos cartas en una baraja, cada una de las cuales pertenece a uno de los cuatro palos y a uno de los trece rangos. Los palos son Picas, Corazones, Diamantes y Palos (en orden descendente en el bridge). Los rangos son As, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jota, Reina y Rey. Dependiendo del juego al que se juegue, el rango del As puede ser mayor que el del Rey o menor que el 2.

Si queremos definir un nuevo objeto que represente una carta de juego, es obvio cuáles deben ser los atributos: rango y traje. No es tan obvio el tipo de atributos. Una posibilidad es utilizar cadenas que contengan palabras como «Pica» para los palos y «Reina» para los rangos. Un problema de esta implementación es que no sería fácil comparar las cartas para ver cuál tiene un rango o un palo más alto.

Una alternativa es utilizar números enteros para codificar los rangos y los palos.Por «codificar», no nos referimos a lo que algunos piensan, que es codificar o traducir en un código secreto. Lo que un informático entiende por «codificar» es «definir un mapeo entre una secuencia de números y los elementos que quiero representar». Por ejemplo:

Picas -> 3
Corazones -> 2
Diamantes -> 1
Clubes -> 0

Una característica obvia de este mapeo es que los palos mapean a enteros en orden, así que podemos comparar palos comparando enteros. El mapeo para los rangos es bastante obvio; cada uno de los rangos numéricos se asigna al número entero correspondiente, y para las cartas de la cara:

Jack -> 11
Queen -> 12
King -> 13

La razón por la que usamos notación matemática para estos mapeos es que no son parte del programa Python. Son parte del diseño del programa, pero nunca aparecen explícitamente en el código. La definición de la clase Card es la siguiente:

class Card:
def __init__(self, suit=0, rank=2):
self.suit = suit
self.rank = rank

Como es habitual, proporcionamos un método de inicialización que toma un parámetro opcional para cada atributo. El valor por defecto del palo es 0, que representa a los tréboles.

Para crear una carta, invocamos el constructor Card con el palo y el rango de la carta que queremos.

tresDeLosClubes = Card(3, 1)

En la siguiente sección averiguaremos qué carta acabamos de hacer.

15.3 Atributos de la clase y el método __str__

Para imprimir los objetos Card de una forma que la gente pueda leer fácilmente, queremos mapear los códigos enteros en palabras. Una forma natural de hacerlo es con listas de cadenas. Asignamos estas listas a los atributos de la clase en la parte superior de la definición de la clase:

class Card:
suitList =
rankList =
#init method omitted
def __str__(self):
return (self.rankList + » de » +
self.suitList)

Un atributo de clase se define fuera de cualquier método, y se puede acceder a él desde cualquiera de los métodos de la clase.

Dentro de __str__, podemos usar suitList y rankListpara mapear los valores numéricos de suit y rank a cadenas.Por ejemplo, la expresión self.suitList significa «usar el atributo suit del objeto self como un índice dentro del atributo de clase llamado suitList, y seleccionar la cadena apropiada.»

La razón del «narf» en el primer elemento de rankList es actuar como guardián del lugar para el elemento cero de la lista, que nunca debe ser utilizado. Los únicos rangos válidos son del 1 al 13. Este elemento desperdiciado no es del todo necesario. Podríamos haber empezado por el 0, como siempre, pero es menos confuso codificar el 2 como 2, el 3 como 3, y así sucesivamente.

Con los métodos que tenemos hasta ahora, podemos crear e imprimir tarjetas:

>>> tarjeta1 = Tarjeta(1, 11)
>>> imprimir tarjeta1
Jack of Diamonds

Los atributos de la clase como suitList son compartidos por todos los Cardobjects. La ventaja de esto es que podemos utilizar cualquier Cardobject para acceder a los atributos de la clase:

>>> card2 = Card(1, 3)
>>> print card2
3 of Diamonds
>>> print card2.suitList
Diamantes

El inconveniente es que si modificamos un atributo de la clase, afecta a todas las instancias de la misma. Por ejemplo, si decidimos que «Jota de Diamantes» debería llamarse realmente «Jota de Ballenas Revoloteantes», podríamos hacer lo siguiente:

>>> card1.suitList = «Swirly Whales»
>>> print card1
Jack of Swirly Whales

El problema es que todos los Diamantes acaban de convertirse enSwirly Whales:

>>>imprimir la tarjeta2
3 de Swirly Whales

No suele ser buena idea modificar los atributos de las clases.

15.4 Comparación de tarjetas

Para los tipos primitivos, existen operadores condicionales (<, >, ==, etc.) que comparan valores y determinan cuándo uno es mayor, menor o igual que otro. Para los tipos definidos por el usuario, podemos anular el comportamiento de los operadores incorporados proporcionando un método llamado __cmp__. Por convención, __cmp__ tiene dos parámetros, self y other, y devuelve1 si el primer objeto es mayor, -1 si el segundo objeto es mayor, y 0 si son iguales entre sí.

Algunos tipos están completamente ordenados, lo que significa que se pueden comparar dos elementos cualesquiera y decir cuál es mayor. Por ejemplo, los enteros y los números de punto flotante están completamente ordenados. Algunos conjuntos están desordenados, lo que significa que no hay una forma significativa de decir que un elemento es mayor que otro. Por ejemplo, las frutas están desordenadas, por lo que no se pueden comparar manzanas y naranjas.

El conjunto de naipes está parcialmente ordenado, lo que significa que a veces se pueden comparar cartas y a veces no. Por ejemplo, sabes que el 3 de tréboles es más alto que el 2 de tréboles, y el 3 de diamantes es más alto que el 3 de tréboles. Pero, ¿cuál es mejor, el 3 de Tréboles o el 2 de Diamantes? Uno tiene un rango más alto, pero el otro tiene un palo más alto.

Para que las cartas sean comparables, hay que decidir qué es más importante, el rango o el palo. A decir verdad, la elección es arbitraria. Para elegir, diremos que el palo es más importante, porque una nueva baraja viene ordenada con todos los Tréboles juntos, seguidos de todos los Diamantes, y así sucesivamente.

Decidido esto, podemos escribir __cmp__:

def __cmp__(self, other):
# comprueba los palos
si self.suit > other.suit: return 1
si self.suit < other.suit: return -1
# los palos son iguales… comprueba los rangos
si self.rank > other.rank: return 1
if self.rank < other.rank: return -1
# ranks are the same… it’s a tie
return 0

En este ordenamiento, los Ases aparecen por debajo de los Dos.

Como ejercicio, modifica __cmp__ para que los Ases aparezcan más arriba que los Reyes.

15.5 Mazos

Ahora que tenemos objetos para representar las Cartas, el siguiente paso lógico es definir una clase para representar un Mazo. Por supuesto, un mazo se compone de cartas, por lo que cada objeto Deck contendrá una lista de cartas como un atributo.

Lo siguiente es una definición de clase para la clase Deck. El método de inicialización crea el atributo cartas y genera el conjunto estándar de cincuenta y dos cartas:

clase Baraja:
def __init__(self):
self.cards =
for suit in range(4):
for rank in range(1, 14):
self.cards.append(Card(suit, rank))

La forma más sencilla de rellenar la baraja es con un bucle anidado. El bucle exterior enumera los palos de 0 a 3. El bucle interior enumera los rangos de 1 a 13. Como el bucle exterior itera cuatro veces y el interior trece, el número total de veces que se ejecuta el cuerpo es de cincuenta y dos (trece por cuatro). Cada iteración crea una nueva instancia de Carta con el palo y el rango actuales, y añade esa carta a la lista de cartas.

El método append funciona con listas pero no, por supuesto, con tuplas.

15.6 Impresión de la baraja

Como es habitual, cuando definimos un nuevo tipo de objeto queremos un método que imprima el contenido de un objeto.Para imprimir un mazo, recorremos la lista e imprimimos cada carta:

clase Mazo:

def printDeck(self):
for card in self.cards:
print card

Aquí, y a partir de ahora, la elipsis (…) indica que hemos omitido los demás métodos de la clase.

Como alternativa a printDeck, podríamos escribir un método __str__ para la clase Deck. La ventaja de __str__ es que es más flexible. En lugar de imprimir el contenido del objeto, genera una representación de cadena que otras partes del programa pueden manipular antes de imprimir, o almacenar para su uso posterior.

Aquí hay una versión de __str__ que devuelve una representación de cadena de un Deck.Para añadir un poco de dinamismo, organiza las cartas en una cascadaen la que cada carta está sangrada un espacio más que la anterior:

class Deck:

def __str__(self):
s = «»
for i in range(len(self.cards)):
s = s + «»*i + str(self.cards) + «\n»
return s

Este ejemplo demuestra varias características. En primer lugar, en lugar de recorrer self.cards y asignar cada tarjeta a una variable, utilizamos i como variable del bucle y como índice de la lista de tarjetas.

En segundo lugar, utilizamos el operador de multiplicación de cadenas para indentar cada tarjeta con un espacio más que la última. La expresión»»*i produce un número de espacios igual al valor actual de i.

En tercer lugar, en lugar de utilizar el comando print para imprimir las tarjetas, utilizamos la función str. Pasar un objeto como argumento a str equivale a invocar el método __str__ sobre el objeto.

Por último, utilizamos la variable s como acumulador.Inicialmente, s es la cadena vacía. Cada vez que se pasa por el bucle, se genera una nueva cadena y se concatena con el valor anterior de sto para obtener el nuevo valor. Cuando el bucle termina, s contiene la representación completa de la cadena del mazo, que se parece a esto:

>>>mazo = Mazo()
>>>imprimir mazo
Ace de tréboles
2 de tréboles
4 de tréboles
5 de tréboles
6 de Tréboles
7 de Tréboles
8 de Tréboles
9 de Tréboles
10 de Tréboles
Jack de Tréboles
Rey de Tréboles
Ace de Diamantes

Y así sucesivamente. Aunque el resultado aparece en 52 líneas, es una cadena larga que contiene nuevas líneas.

15.7 Barajar el mazo

Si un mazo está perfectamente barajado, cualquier carta tiene la misma probabilidad de aparecer en cualquier lugar del mazo, y cualquier lugar del mazo tiene la misma probabilidad de contener cualquier carta.

Para barajar el mazo, utilizaremos la función randrange del módulo random. Con dos argumentos enteros, a y b, randrange elige un entero aleatorio en el rango a <= x < b. Como el límite superior es estrictamente menor que b, podemos utilizar la longitud de una lista como segundo argumento, y se garantiza que obtendremos un índice legal.Por ejemplo, esta expresión elige el índice de una carta al azar en una baraja:

random.randrange(0, len(self.cards))

Una forma fácil de barajar la baraja es recorrer las cartas e intercambiar cada carta con una elegida al azar. Es posible que la carta se intercambie con ella misma, pero eso está bien. De hecho, si excluyéramos esa posibilidad, el orden de las cartas sería menos que completamente aleatorio:

clase Baraja:

def barajar(self):
importar random
nCartas = len(self.cards)
for i in range(nCards):
j = random.randrange(i, nCards)
self.cards, self.cartas = self.cards, self.cards

En lugar de suponer que hay cincuenta y dos cartas en la baraja, obtenemos la longitud real de la lista y la almacenamos en nCards.

Por cada carta de la baraja, elegimos una carta al azar de entre las cartas que aún no se han barajado. Luego intercambiamos la carta actual (i) con la carta seleccionada (j). Para intercambiar las cartas utilizamos una asignación de tupla, como en la sección 9.2:

self.cards, self.cards = self.cards, self.cards

Como ejercicio, reescriba esta línea de codossin utilizar una asignación de secuencia.

15.8 Eliminar y repartir cartas

Otro método que sería útil para la clase Deck es removeCard, que toma una carta como argumento, la elimina y devuelve True si la carta estaba en el mazo y False en caso contrario:

class Deck:

def removeCard(self, card):
if card in self.cards:
self.cards.remove(card)
return True
else:
devuelve False

El operador in devuelve true si el primer operando está en thesecond, que debe ser una lista o una tupla. Si el primer operando es unobjeto, Python utiliza el método __cmp__ del objeto para determinar la igualdad con los elementos de la lista. Como el __cmp__ de la claseCard comprueba la igualdad profunda, el método removeCard comprueba la igualdad profunda.

Para repartir cartas, queremos eliminar y devolver la carta superior.El método pop de la lista proporciona una forma conveniente de hacerlo:

clase Baraja:

def popCard(self):
return self.cards.pop()

En realidad, pop elimina la última carta de la lista, por lo que estamos repartiendo sin efecto desde el fondo de la baraja.

Una operación más que probablemente queramos es la función booleanaisEmpty, que devuelve true si la baraja no contiene cartas:

class Baraja:

def isEmpty(self):
return (len(self.cards) == 0)

15.9 Glosario

codificar Representar un conjunto de valores utilizando otro conjunto de valores construyendo un mapeo entre ellos. class attribute Variable que se define dentro de una definición de clase pero fuera de cualquier método. Los atributos de clase son accesibles desde cualquier método de la clase y son compartidos por todas las instancias de la clase. acumulador Una variable utilizada en un bucle para acumular una serie de valores, por ejemplo, concatenándolos en una cadena o añadiéndolos a una suma en curso.

Esta es una versión antigua del libro ahora conocido como Think Python. Quizá prefiera leer una versión más reciente.

Cómo pensar como un informático

Deja una respuesta

Tu dirección de correo electrónico no será publicada.