martes, 31 de marzo de 2020

Enumeraciones



Una enumeración es una agrupación de constantes que guardan alguna relación y que comparten un mismo espacio de nombres. Las enumeraciones dan consistencia a los programas porque evitan que las constantes sean objetos aislados a los que cueste identificar con el paso del tiempo. También, proporcionan una metodología para abordar con un mismo criterio de estructuración del código implementaciones que se dan con frecuencia.

Las enumeraciones se pueden utilizar para agrupar los diferentes estados de un proceso; las distintas respuestas que pueden darse en los condicionantes que determinan el flujo de un programa; las constantes matemáticas o físicas necesarias para realizar determinados cálculos en un desarrollo y los valores o características de objetos que son del mismo tipo, entre otros.


Creación de una enumeración


Para declarar una enumeración utilizaremos la clase Enum del módulo enum, un viejo conocido en Python como módulo independiente que pasó a formar parte de la librería estándar a partir de Python 3.4 y que ha sufrido alguna ampliación de sus funcionalidades en Python 3.6. A continuación, algunos ejemplos de declaración de enumeraciones:

from enum import Enum


class Respuesta(Enum):
    SI = 1
    NO = 2


class Estado(Enum):
    DETENIDO = 0
    ENESPERA = 0
    INICIADO = 1


class Andalucia(Enum):
    AL = (715993, 8775)  
    CA = (1240020, 7436)
    CO = (782516, 13771)
    GR = (914428, 12647)
    HU = (521428, 10128)
    JA = (633120, 13496)
    MA = (1660693, 7308)
    SE = (1941804, 14036)


Semana = Enum(
    value='Semana',
    names=('LU MA MI JU VI SA DO'),
)


Color = Enum(
    value='Color',
    names=[
        ('ROJO', '#FF0000'),
        ('AMARILLO', '#FFFF00'),
        ('VERDE', '#008000'),
    ],
)    

Las enumeraciones de los ejemplos muestran que existen dos modos de declararlas: como una clase un tanto especial o con el método Enum() en una simple asignación. También, puede observarse que las constantes de una misma enumeración pueden tener o no distintos valores y de diferentes tipos (int, str, tuple, dict y otros).

En la enumeración Respuesta las constantes SI y NO tienen distintos valores. Sin embargo, en Estado tanto la constante DETENIDO como ENESPERA tienen el mismo valor, 0. Esto no es ningún inconveniente, lo que no se admite es tener dos constantes con la misma denominación en una enumeración. Más adelante mostraremos también cómo forzar que las constantes tengan valores únicos.

En la enumeración Andalucia cada constante tiene asignada una tupla con dos valores que hacen referencia al número de habitantes (en 2019) y a la superficie en kilómetros cuadrados de cada provincia andaluza.

En la enumeración Semana se obtienen las constantes que son abreviaturas de los días de la semana del argumento names. Además, como no se indican valores se les asignarán de forma automática los enteros del 1 al 7.

Finalmente, en la enumeración Color las constantes se obtienen también del argumento names aunque en este caso se corresponde con una lista de tuplas con dos cadenas que hacen referencia al nombre de la constante y a su valor, un código de color en hexadecimal representado como una cadena.

La sintaxis utilizada para expresar cualquier constante facilita la legibilidad del código. Simplemente, hay que indicar en primer lugar el nombre de la enumeración, seguido de un punto y el nombre de la constante:

NombreEnumeración.CONSTANTE

Basándonos en esta notación podemos referirnos a alguna de las constantes declaradas en los ejemplos anteriores del siguiente modo:

Respuesta.SI
Semana.LU
Color.ROJO

Pero conozcamos más de cerca estos objetos y sus atributos:

# Obtener la clase de una enumeración:

type(Respuesta)  # 'enum.EnumMeta'
type(Estado)  # 'enum.EnumMeta'

# Obtener el tipo de objeto de una constante:

type(Respuesta.SI)  # enum 'Respuesta'
type(Semana.JU)  # enum 'Semana'

# Obtener el nombre de una constante

respnom = Respuesta.SI.name  # 'SI'
estnom = Estado.DETENIDO.name  # 'DETENIDO'

# Obtener el valor de una constante

respval = Respuesta.SI.value  # 1
hab, sup = Andalucia.AL.value  # hab=715993; sup=8775
diaval = Semana.DO.value  # 7
color = Color.VERDE.value  # '#008000'

# Obtener una constante por su nombre

objnom1 = Respuesta['SI']  # Respuesta.SI: 1
objnom2 = Semana['LU']  # Semana.LU: 1

# Obtener la primera constante que tiene un valor determinado

objconst1 = Respuesta(2)  # Respuesta.NO: 2
objconst2 = Estado(0)  # Estado.DETENIDO: 0

# Comprobar si una constante pertenece a una enumeración

isinstance(Respuesta.SI, Respuesta)  # True
isinstance(Semana.DO, Semana)  # True

# Imprimir el objeto (No se obtiene el valor)

print(Respuesta.SI)  #  Respuesta.SI
print(Color.AMARILLO)  # Color.AMARILLO

# Imprimir la cadena de representación de una constante

print(repr(Respuesta.SI))  # Respuesta.SI: 1
print(repr(Estado.DETENIDO))  # Estado.DETENIDO: 0

# Comparar constantes con is o is not 

Respuesta.SI is not Respuesta.NO  # True
Estado.DETENIDO is Estado.ENESPERA  # True

# Comparar constates con == o !=

Respuesta.SI != Respuesta.NO  # True
Estado.DETENIDO == Estado.ENESPERA  # True


Comparaciones. La clase IntEnum


Algo que no agrada a muchos es no poder comparar una constante con un valor entero determinado; pero esto es normal porque la referencia a una constante devuelve el objeto enumeración al que pertenece no un valor entero, una cadena u otro tipo de objeto. Mucho cuidado, cualquier comparación que se haga de este tipo siempre devolverá False:

Estado.DETENIDO == 0  # False
Respuesta.SI == 1  # False

Para este tipo de comparaciones podemos utilizar el atributo value que ya ha aparecido antes en los ejemplos:

Estado.DETENIDO.value == 0  # True
Respuesta.SI.value == 1  # True

No obstante, la clase IntEnum nació para permitir hacer este tipo comparaciones de forma más abreviada y directa, sin obligar a referenciar el atributo value. Para ello, tan solo tendremos que declarar las enumeraciones con valores basados en enteros del siguiente modo:

from enum import IntEnum


class Respuesta(IntEnum):
    SI = 1
    NO = 2


class Estado(IntEnum):
    DETENIDO = 0
    ENESPERA = 0
    INICIADO = 1


# Ahora es posible evaluar de forma más óptima
# el valor (entero) de una constante.

Estado.DETENIDO == 0  # True
Respuesta.SI == 1  # True


Iteraciones


Otra característica que tienen las enumeraciones, además de permitir comparaciones, consiste en que son objetos iterables. Esto significa que se pueden recorrer, una a una, todas sus constantes en el orden en que fueron declaradas:

for dia in Semana:
    print(dia.name, '->', dia.value)


# LU -> 1
# MA -> 2
# MI -> 3
# JU -> 4
# VI -> 5
# SA -> 6
# DO -> 7


Enumeraciones con constantes con valores únicos


Como se ha comentado antes, la enumeración Estado cuenta con las constantes DETENIDO y ENESPERA que tienen el mismo valor, 0. Si queremos exigir que todas las constantes tengan valores únicos tendremos que agregar el decorador @unique a la declaración de la enumeración. Si agregamos el decorador y existen valores repetidos se producirá una excepción del tipo ValueError:

from enum import Enum, unique

@unique
class Estado(Enum):
    DETENIDO = 0
    ENESPERA = 0
    INICIADO = 1


# ValueError: duplicate values found in : ENESPERA -> DETENIDO


Enumeraciones con métodos


Las enumeraciones son clases de Python y como tales permiten incorporar métodos, algunos especiales, que amplían sobremanera el potencial de este tipo de objetos. En el siguiente ejemplo se declara la enumeración Andalucia con varios métodos para obtener los valores de habitantes y superficie de las constantes en una tupla, como valores independientes o bien devolviendo el cálculo de la densidad de población.

from enum import Enum


class Andalucia(Enum):
    AL = (715993, 8775)  
    CA = (1240020, 7436)
    CO = (782516, 13771)
    GR = (914428, 12647)
    HU = (521428, 10128)
    JA = (633120, 13496)
    MA = (1660693, 7308)
    SE = (1941804, 14036)

    def __init__(self, hab, sup):
        self.hab = hab  # Núm. habitantes
        self.sup = sup  # En Km cuadrados
 
    @property       
    def habitantes(self):
        return self.hab
 
    @property       
    def superficie(self):
        return self.sup

    @property
    def densidad(self):
        return self.hab / self.sup


# Obtener información de la provincia de Córdoba (CO):

Andalucia.CO.value  # (782516, 13771)
Andalucia.CO.habitantes  # 782516
Andalucia.CO.superficie  # 13771
Andalucia.CO.densidad  # 56.823469610050104


# Obtener información de la provincia de Sevilla (SE):

Andalucia.SE.value   # (1941804, 14036)
Andalucia.SE.habitantes  # 1941804
Andalucia.SE.superficie  # 14036
Andalucia.SE.densidad  # 138.3445426047307


Relacionado: