jueves, 23 de abril de 2015

defaultdict vs setdefault


El método setdefault() se utiliza con un diccionario común para recuperar el valor de una clave y, si ésta no existe, establecer un valor predeterminado:

# Establecer el valor por defecto de 'y' con 5:

diccionario = {'x':2, 'y':5}
print(diccionario.setdefault('y', 1))  # 5

# Mostrar los datos del diccionario: 
# 'y' existe con valor 5 y ese es el valor mostrado 

print(diccionario)  # {'x': 2, 'y': 5}

# Establecer el valor por defecto de 'z' en 3:

print(diccionario.setdefault('z', 3))

# Mostrar el valor de 'z': 
# como no existía aparece su valor por defecto:

print(diccionario['z'])  # 3

# Mostrar los datos del diccionario:

print(diccionario)  # {'z': 3, 'y': 5, 'x': 2}

Sin embargo, defaultdict, del módulo collections, permite asignar un valor predeterminado por adelantado, justo en el momento de inicializar el contenedor:

import collections

def fvalpredeter():
    return 1

ddiccionario = collections.defaultdict(fvalpredeter, x=2, z=3)
print('ddiccionario:', ddiccionario)
print('x: ', ddiccionario['x']) # 2
print('y: ', ddiccionario['y']) # 1
print('z: ', ddiccionario['z']) # 3
print('w: ', ddiccionario['w']) # 1
print(dict(ddiccionario))  # {'z': 3, 'x': 2, 'w': 1, 'y': 1}

# La función fvalpredeter se podía haber expresado con 
# una función lambda:

ddiccionario = collections.defaultdict(lambda: 1, x=2, z=3)

(Ver información sobre las funciones lambda).

La aplicación del ejemplo anterior es útil cuando todas las claves deben tener el mismo valor por defecto.

No obstante, el uso de defaultdict puede resultar más interesante cuando el valor por defecto se establece como del tipo list, set o int. La razón está en que estos tipos permiten añadir, contar y acumular datos:

# En el ejenplo siguiente defaultdict se utiliza con list
# para agrupar valores de claves que se repiten:

import collections
peces1 = [('dorada', 1), ('merluza', 2), 
          ('dorada', 3), ('bacalao', 4), 
          ('merluza', 1)]
ddiccionario1 = collections.defaultdict(list)
print(list(ddiccionario1))
for clave, valor in peces1:
    ddiccionario1[clave].append(valor)

list(ddiccionario1.items())  
# [('dorada', [1, 3]), ('merluza', [2, 1]), ('bacalao', [4])]

# En el ejemplo siguiente defaultdict se utiliza con int
# para contar elementos repetidos:

peces2 = ['dorada', 'merluza', 'dorada', 
          'merluza', 'merluza', 'bacalao']
cont_peces2 = collections.defaultdict(int)
for pez in peces2:
    cont_peces2[pez] += 1

print(dict(cont_peces2))  
# {'merluza': 3, 'bacalao': 1, 'dorada': 2}

# En el siguiente ejemplo a partir de una cadena se 
# obtiene una lista de tuplas ordenadas alfabéticamente
# con el número de veces que se repite cada carácter:

poppins = 'supercalifragilisticoexpialidoso'
ddiccionario = collections.defaultdict(int)
for clave in poppins:
    ddiccionario[clave] += 1

print(sorted(list(ddiccionario.items())))  
# [('a', 3), ('c', 2), ('d', 1), ('e', 2), ('f', 1), 
#  ('g', 1), ('i', 6), ('l', 3), ('o', 3), ('p', 2), 
#  ('r', 2), ('s', 3), ('t', 1), ('u', 1), ('x', 1)]

# A continuación, defaultdict se utiliza con set 
# para obtener una lista de elementos con los valores
# no repetidos:

peces3 = [('dorada', 1), ('merluza', 2), 
          ('merluza', 2), ('dorada', 3)]
dict_peces3 = collections.defaultdict(set)

for clave, valor in peces3:
    dict_peces3[clave].add(valor)

print(list(dict_peces3.items()))  
# [('dorada', {1, 3}), ('merluza', {2})]

# En el ejemplo siguiente defaultdict se utiliza con set
# para agrupar las posiciones que ocupan los elementos
# en una lista (el primer elemento se encuentra en 
# la posición 0):

peces4 = ['dorada', 'merluza', 'dorada', 
          'merluza', 'merluza', 'bacalao']
cont_peces4 = collections.defaultdict(set)
for indice, clave in enumerate(peces4): 
    cont_peces4[clave].add(indice)

print(dict(cont_peces4))  
# {'dorada': {0, 2}, 'bacalao': {5}, 'merluza': {1, 3, 4}}

cont_peces4['dorada']  # {0, 2}

dorada = list(cont_peces4['dorada'])
print(dorada)  # [0, 2]

martes, 21 de abril de 2015

Combinar diccionarios con ChainMap


La clase ChainMap del módulo collections permite crear objetos como resultado de combinar varios diccionarios. En el objeto que se obtiene se hace un mapeo lógico de todas las claves existentes en todos los diccionarios, permitiéndose realizar ciertas operaciones, como la búsqueda de valores o comprobar la existencia de dichas claves. ChainMap está disponible desde Python 3.3.

import collections
diccionario1 = {'cafés':12, 'zumos':9}
diccionario2 = {'zumos':6, 'infusiones':8}

# Combinar dos diccionarios y obtener un objeto ChainMap:

diccionario3 = collections.ChainMap(diccionario1, 
                                    diccionario2)

# Mostrar el objeto ChainMap:

print(diccionario3)  
# ChainMap({'cafés': 12, 'zumos': 9}, 
#          {'infusiones': 8, 'zumos': 6})

# Accediendo al valor de varias claves:

print(diccionario3['cafés'])  # 12, del diccionario1
print(diccionario3['zumos'])  # 9, del diccionario1
print(diccionario3['infusiones'])  # 8, del diccionario2

Como puede observarse si se accede al valor de una clave que existe en varios diccionarios, se obtiene el valor que tenga en el primer diccionario donde se encuentre dicha clave.

De lo anterior se deduce que cuando se combinan varios diccionarios realmente no se produce una fusión de todas las asignaciones existentes, en el sentido más estricto del término. En realidad un objeto ChainMap mantiene todas las asignaciones vinculadas con su diccionario de origen y redefine las operaciones que se aplican a un diccionario común para poder explorar los elementos combinados. Además, aquellas operaciones que se aplican a un diccionario común funcionarán con un objeto ChainMap.

# Añadir nuevos pares a un objeto ChainMap:

diccionario3['cacaos'] = 23
diccionario3['granizados'] = 4

# Mostrar el objeto ChainMap:

print(diccionario3)  
# ChainMap({'cafés': 12, 'granizados': 4,'zumos': 9,
#           'cacaos': 23}, {'infusiones': 8, 'zumos': 6})

# Mostrar el número de pares en el objeto ChainMap:

print(len(diccionario3))  # Objeto ChainMap tiene 5 pares

# Mostrar el número de pares del diccionario diccionario1:

print(len(diccionario1))  # diccionario1 tiene 4 pares

# Borrar un par clave/valor si existe en
# el objeto ChainMap:

if "granizados" in diccionario3:
    del diccionario3['granizados'] # Si existe es borrado

if "granizados" not in diccionario3: 
    print('No existe')  # Si no existe muestra mensaje

# Mostrar el objeto ChainMap:

print(diccionario3)  
# ChainMap({'cafés': 12, 'zumos': 9, 'cacaos': 23}, 
#          {'infusiones': 8, 'zumos': 6})

# Obtener una lista actualizada con todos los 
# diccionarios del ChainMap:

lista_diccionarios = diccionario3.maps
print(lista_diccionarios)

Por otro lado, un objeto ChainMap es particularmente útil cuando se ha construido a partir de varios diccionarios hijos que comparten pares con claves repetidas porque éstas pueden ser recorridas por descartes sucesivos:

import collections

# Crear un objeto ChainMap vacío:

pizzas = collections.ChainMap()
pizzas['tonno'] = 5

# Añadir un nuevo diccionario hijo:

pizzas = pizzas.new_child()
pizzas['tonno'] = 10

# Agregar un nuevo diccionario hijo:

pizzas = pizzas.new_child()
pizzas['tonno'] = 15

# Mostrar objeto ChainMap:

print(pizzas)  
# ChainMap({'tonno': 15}, 
#          {'tonno': 10}, 
#          {'tonno': 5})

# Acceder al valor de una clave:

print(pizzas['tonno']) # 15

# Descartar el primer mapeo del objeto ChainMap
# y mostrar valor de clave

pizzas = pizzas.parents 
print(pizzas['tonno']) # 10

# Descarta el siguiente mapeo del objeto ChainMap
# y mostrar valor de clave

pizzas = pizzas.parents  
print(pizzas['tonno']) # 5

# Mostrar objeto ChainMap

print(pizzas)  # ChainMap({'tonno': 5})


jueves, 16 de abril de 2015

Tuplas a medida con namedtuple


namedtuple del módulo collections se utiliza para definir subclases de la clase tuple, que permiten crear tuplas a medida a los programadores. El nombre del nuevo tipo de tupla será el que se indique en el primer argumento y constará de los campos que se incluyan en la lista del segundo argumento:

namedtuple(typename, field_names, verbose=False, rename=False)


Si el argumento rename es True los nombres de campos que no sean válidos serán sustituidos, automáticamente, por nombres posicionales ('_1', '_2', ...). También, si el argumento verbose es True la definición de la clase será impresa después de que se construya. Esta última opción que tiende a desaparecer se obtiene también mediante el atributo _source.

Los nombres de los campos pueden contener caracteres alfabéticos, numéricos y guiones bajos; y no pueden comenzar con caracteres numéricos o guiones bajos, ni pueden ser, lógicamente, palabras reservadas del lenguaje Python.

import collections

# Crear un nuevo tipo de tupla para contener datos
# de elementos químicos

elemento = collections.namedtuple('elemento', 
                                  ['nombre', 'simbolo', 'numato'])

# Crear un nuevo objeto del tipo elemento, 
# con datos del Azufre

elem = elemento('Azufre', 'S', 16)

# Mostrar información del objeto anterior

print(elem.nombre, elem.simbolo, elem.numato)  # Azufre S 16
print(elem[0], elem[1], elem[2])  # Azufre S 16
print(elem)  # elemento(nombre='Azufre', simbolo='S', numato=16)

# Conocer de qué tipo es el objeto 'elem' 

print(type(elem))  # 

# Mostrar la definición de la subclase 'elem'

print(elem._source)

# Mostrar los nombres de campos de la subclase 'elem'

print(elem._fields)  
# nombres de campos: ('nombre', 'simbolo', 'numato')

# Sustituir los valores actuales

elem = elem._replace(nombre='Carbono', simbolo='C', numato=6)

# Mostrar información

print(elem.nombre)  # 'Carbono'

# Mostrar tipo de diccionario con los campos ordenados
# como en una tupla

print(elem._asdict())

# Obtener el valor del campo indicado

print(getattr(elem, 'simbolo'))  # 'C'

# Crear otro tipo nuevo para contener coordenadas de
# un plano y color

coordenada = collections.namedtuple('coordenada', 
                                    ['x', 'y', 'color'])
coord = coordenada(12, 53, 'blue')

# Mostrar valores 

print('x=', coord.x)
print('y=', coord.y)
print('color=', coord.color)

x, y, color = coord
print(x, y, color)


Ir al índice del tutorial de Python

miércoles, 15 de abril de 2015

Con OrderedDict el orden ha llegado


Un diccionario en Python es un objeto de la clase dict bastante peculiar porque sus pares de claves/valor cuando son recorridos no tienen porque aparecer en el mismo orden en que fueron agregados. Es más, si recorremos un diccionario varias veces y mostramos los pares existentes es muy probable que aparezcan cada vez en distinto orden.

Después de ejecutar tres veces el siguiente ejemplo:
dicc = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6, 'g':7}

for clave in dicc.keys():
    print(clave, "->" , dicc[clave], end="|")

El resultado obtenido ha sido:

1) a -> 1|f -> 6|g -> 7|e -> 5|c -> 3|b -> 2|d -> 4|
2) f -> 6|d -> 4|a -> 1|b -> 2|g -> 7|c -> 3|e -> 5|
3) f -> 6|b -> 2|c -> 3|g -> 7|a -> 1|d -> 4|e -> 5|


El concepto de orden en un diccionario está fuera de lugar. Sin embargo, el objeto OrderedDict del módulo collections pertenece a una subclase de la clase dict que sí permite mantener las claves de un diccionario en el mismo orden en que fueron insertadas. Además, si una nueva entrada sobrescribe una existente la posición de inserción original se mantiene; pero si previamente es eliminada y después se vuelve a agregar pasará a ocupar la última posición.

import collections

escritores = {'Borges':'Argentina', 
              'Coehlo':'Brasil',
              'Lorca':'España', 
              'Márquez':'Colombia',
              'Neruda':'Chile',
              'Rulfo':'México',
              'Saramago':'Portugal'}

# Crear un objeto OrderedDict:
odescritores = collections.OrderedDict(escritores)

# Suprimir par clave/valor por el final y 
# asignar tupla con datos:
escritor_ultimo = odescritores.popitem()

# Muestra tupla:
print(escritor_ultimo)  # ('Saramago', 'Portugal')

# Muestra elementos manteniendo orden original
# sin elemento borrado
print(odescritores)  

# Suprimir par clave/valor por el principio y
# asignar tupla con datos:
escritor_primero = odescritores.popitem(last=False)

# Muestra tupla 
print(escritor_primero) # ('Borges', 'Argentina')

# Muestra elementos manteniendo orden original
# sin elemento borrado
print(odescritores)  

# Suprimir par clave/valor por el final y 
# asignar datos a dos variables:
escritor, pais = odescritores.popitem()

print(escritor, pais)  # Rulfo México

# Muestra elementos manteniendo orden original
# sin elemento borrado 
print(odescritores)

# OrderedDict([('Lorca', 'España'), 
#              ('Coehlo', 'Brasil'),
#              ('Neruda', 'Chile'), 
#              ('Saramago', 'Portugal')])

# Obtener un diccionario a partir de objeto OrderedDict:
diccionario_desordenado = dict(odescritores)

# Los elementos aparecerán a su libre albedrío:
print(diccionario_desordenado)
# {'Lorca': 'España', 'Coehlo': 'Brasil', 
#  'Saramago': 'Portugal', 'Neruda': 'Chile'}

# Mover clave/valor al final del objeto OrderedDict:
# Mueve al final (si existe) el par de clave 'Coehlo'
odescritores.move_to_end('Coehlo')

print(odescritores)
# OrderedDict([('Lorca', 'España'), ('Neruda', 'Chile'),
#              ('Saramago', 'Portugal'), ('Coehlo', 'Brasil')])

# Mover un par clave/valor al principio del objeto OrderedDict:
# Mueve al principio (si existe) el par
odescritores.move_to_end('Coehlo', last=False)

# Muestra datos con par de Coehlo' al comienzo
print(odescritores)

# Añadir nuevos pares al final del objeto OrderedDict:
odescritores['Martí'] = 'Cuba'   # Añadir nuevo par
odescritores['Benedetti'] = 'Uruguay' # Añadir nuevo par

print(odescritores)   
# Muestra: OrderedDict([('Coehlo', 'Brasil'), 
#                       ('Lorca', 'España'), 
#                       ('Neruda', 'Chile'), 
#                       ('Saramago', 'Portugal'), 
#                       ('Martí', 'Cuba'), 
#                       ('Benedetti', 'Uruguay')])

# Obtener lista ordenada de tuplas del objeto OrderedDict:
lista_ordenada = sorted(odescritores.items())

print(lista_ordenada)  
# [('Benedetti', 'Uruguay'), ('Coehlo', 'Brasil'), 
# ('Lorca', 'España'), ('Martí', 'Cuba'), 
# ('Neruda', 'Chile'), ('Saramago', 'Portugal')]

# Obtener lista ordenada de claves del objeto OrderedDict:
lista_claves = sorted(odescritores.keys())
print(lista_claves)  
# ['Benedetti', 'Coehlo', 'Lorca', 
#  'Martí', 'Neruda', 'Saramago']

# Mostrar todos los pares del objeto OrderedDict 
# ordenados por las claves:
for clave in sorted(odescritores.keys()):
    print(clave, odescritores[clave])

# Obtener lista ordenada de valores del objeto OrderedDict:
lista_valores = sorted(odescritores.values())
print(lista_valores)  
# ['Brasil', 'Chile', 'Cuba', 
#  'España', 'Portugal', 'Uruguay']

# Obtener lista ordenada inversa de valores:
lista_valores = sorted(odescritores.values(), reverse=True)
print(lista_valores)  
# ['Uruguay', 'Portugal', 'España', 
# 'Cuba', 'Chile', 'Brasil']

# Agregar a un objeto OrderedDict los elementos de
# otro objeto OrdereDict:
odescritoras = collections.OrderedDict()
odescritoras['Allende'] = 'Chile'
odescritoras['Laforet'] = 'España'
odescritoras['Pizarnik'] = 'Argentina'
odescritoras['Mastretta'] = 'México'
odescritores.update(odescritoras)

Comparando diccionarios y objetos OrderedDict


Si se comparan dos diccionarios el resultado será True cuando contengan los mismos pares de claves/valores sin importar la posición que ocupen:
dicc1 = {'a':1, 'b':2}
dicc2 = {'b':2, 'a':1}
print(dicc1 == dicc2)  # True

En cambio, si se comparan dos objetos OrderedDict el resultado será True cuando contengan los mismos pares de claves/valores y además ocupen la misma posición:

odicc1 = collections.OrderedDict()
odicc1['a'] = 1
odicc1['b'] = 2
odicc2 = collections.OrderedDict()
odicc2['b'] = 2
odicc2['a'] = 1
print(odicc1 == odicc2)  # False


Importante: A partir de Python 3.6 la implementación de los diccionarios ha cambiado, manteniéndose en todo momento el orden en que fueron agregados los elementos a un diccionario cuando son recorridos o consultados. 

Ir al índice del tutorial de Python

martes, 14 de abril de 2015

Objeto deque, más que una lista


Un objeto deque es un contenedor de datos del módulo collections similar a una lista o una cola que permite añadir o suprimir elementos por sus dos extremos.

Se construye a partir de objetos iterables (que permiten recorrer sus elementos) como una secuencia de caracteres, una lista o una tupla. En el caso de los diccionarios el objeto deque se construirá con las claves existentes en el mismo.

El argumento opcional maxlen se utiliza para limitar el número de elementos que puede contener el objeto deque:

deque([iterable[, maxlen]])

A continuación, se muestra cómo crear un objeto deque a partir de una lista y se utilizan varios métodos propios de listas para consultar la longitud del objeto; añadir y borrar elementos.

import collections
documentos = ['doc1', 'doc3', 'doc4', 'doc5']
docs = collections.deque(documentos)
print('Deque:', docs)
print('Longitud:', len(docs))  # mostrar número de elementos deque
print('Elemento de más a la izquierda:', docs[0])
print('Elemento de más a la derecha:', docs[-1])
docs.remove('doc3')  # borrar el elemento indicado
print('remove(doc3):', docs)
docs.append('doc6')  # añadir un elemento por la derecha
print(docs)  # deque(['doc1', 'doc4', 'doc5', 'doc6'])
listadocs = list(docs)  # Obtener lista con todos

La ventaja principal de este tipo de contenedor está en que permite agregar o suprimir elementos por la izquierda:

# Para añadir un elemento por la izquierda:
docs.appendleft('doc0')

print(docs)  
# deque(['doc0', 'doc1', 'doc4', 'doc5', 'doc6'])

# Para extender la lista por la izquierda con nuevos
# elementos:
docs.extendleft(['libro1', 'libro2'])

print(docs)  
# deque(['libro2', 'libro1', 'doc0', 
         'doc1', 'doc4', 'doc5', 'doc6'])

# Para borrar elementos por cualquiera de los extremos:
docs.pop()  # borra elemento de más a derecha: 'doc6'

print(docs)  
# deque(['libro2', 'libro1', 'doc0', 
#        'doc1', 'doc4', 'doc5'])

docs.popleft()  # borra elemento de más a izquierda: 'libro2'

print(docs)  
# deque(['libro1', 'doc0', 'doc1', 'doc4', 'doc5'])

# En caso de no existir elementos para borrar se producirá
# la siguiente excepción: 
# IndexError: pop from an empty deque

# Para contar el número de veces que aparece el elemento
# indicado (desde Python 3.2):
docs.count('doc4')  # 1

# Para invertir el orden de los elementos (desde Python 3.2):
docs.reverse()
print(docs)  
# deque(['doc5', 'doc4', 'doc1', 'doc0', 'libro1'])

# Para rotar o pasar un número de elementos desde la derecha
# a la izquierda, o viceversa:

docs.rotate(2)  # rota 2 elementos desde derecha a izquierda
print(docs)
# deque(['doc0', 'libro1', 'doc5', 'doc4', 'doc1'])

docs.rotate(-1)  # rota 1 elemento desde izquierda a derecha
print(docs)
# deque(['libro1', 'doc5', 'doc4', 'doc1', 'doc0'])

# Para borrar todos los elementos: 

docs.clear()
print(docs)  # deque([])


Ir al índice del tutorial de Python

lunes, 13 de abril de 2015

Counter, el contador de Python



El módulo collections provee nuevos tipos de datos mejorados que derivan, según el caso, de una lista, una tupla, un diccionario o conjunto de datos (set). En este artículo vamos a estudiar el objeto Counter.

Un objeto Counter es un contenedor del módulo collections que se utiliza para contar las veces que aparece un valor en una secuencia de caracteres, en una lista, un diccionario o una lista de nombres de argumentos con asignaciones. Este tipo de objeto es de una subclase que está basada en la clase dict (diccionario).

A continuación, se muestran distintas formas de construir objetos Counter y se realizan algunas operaciones que demuestran su afinidad con la clase dict.

import collections
lista1 = ['Granada', 'Huelva', 'Sevilla', 'Granada', 
          'Granada', 'Sevilla', 'Sevilla']

# Crear un objeto Counter a partir de la lista lista1
cuenta1 = collections.Counter(lista1)

# En el objeto Counter las claves son los 
# elementos (sin repetir) de la lista y los valores el
# número de veces que aparece cada elemento en la lista.
print(cuenta1)
# Counter({'Granada': 3, 'Sevilla': 3, 'Huelva': 1})

lista1.append('Huelva')  # Añade un nuevo elemento
cuenta1 = collections.Counter(lista1)
print(cuenta1)  
# Counter({'Granada': 3, 'Sevilla': 3, 'Huelva': 2})

# Creación de un objeto Counter a partir de un diccionario.

dicc1 = {'Granada': 3, 'Sevilla': 3, 'Huelva': 1}
cuenta2 = collections.Counter(dicc1)
cuenta3 = collections.Counter(Granada=3, Sevilla=3, Huelva=1)

# Un objeto contador se puede construir vacío y después 
# actualizar con el método update().

cuenta4 = collections.Counter()
print(cuenta4)
cuenta4.update('xxxyxzzyxyx')
print(cuenta4)  
# Counter({'x': 6, 'y': 3, 'z': 2})

cuenta4.update({'m':1, 'n':2})
print(cuenta4)  
# Counter({'x': 6, 'y': 3, 'n': 2, 'z': 2, 'm': 1})

# La forma de acceder al valor de algún elemento del Contador
# es igual que en un diccionario:
print(cuenta4['x'])  # 6
print(cuenta4['a'])  # 0

# Para obtener una lista con los elementos de un objeto Counter:
lista4 = list(cuenta4.elements())
print(lista4)  
# ['m', 'y', 'y', 'y', 'n', 'n', 'x', 
#  'x', 'x', 'x', 'x', 'x', 'z', 'z']

# Los elementos de la lista no estarán necesariamente ordenados. 
# Para ordenar una lista utilizaremos el método sort().
lista4.sort()
print(lista4)  
# ['m', 'n', 'n', 'x', 'x', 'x', 'x', 
#  'x', 'x', 'y', 'y', 'y', 'z', 'z']

# Para obtener una lista de tuplas con los elementos más
# comunes de un Contador.
print(cuenta4.most_common(1))
# Obtiene el elemento que más se repite: [('x', 6)]

print(cuenta4.most_common(3))
# 3 elementos más repetidos: [('x', 6), ('y', 3), ('n', 2)]

Para contar las palabras de una frase podemos crear una lista utilizando el método split() para dividir la frase en tantos trozos como palabras existan. Por defecto, split() utiliza el espacio en blanco como separador:

sagan = 'La ausencia de prueba no es prueba de ausencia'
lista5 = sagan.split()
cuenta5 = collections.Counter(lista5)

for clave, valor in cuenta5.items():
    print(clave,':',valor) 
    # no : 1, ausencia : 2, La : 1, de : 2, es : 1, prueba : 2