HomePage RecentChanges

Lesson18

Abstracção de dados

Até este momento todos os programas que escrevemos manipulam instâncias de tipos já existentes, nunca considerámos novos tipos de dados. É no entanto desejável que possamos representar e utilizar diferentes tipos de informação nos nossos programas e que não existem na linguagem que estamos a utilizar para os escrever.

A abstracção em programação

Uma abstracção é uma descrição simplificada de uma entidade com foco nas propriedades mais relevantes para o contexto em que estamos, deixando de parte propriedades não relevantes e pormenores.

Por exemplo, uma pessoa pode ser caracterizada pelo nome, morada, estado civil, género, data e local de nascimento, etc. É importante notar que algumas destas propriedades são já elas abstracções como o local de nascimento. Dependo da aplicação, estas propriedades podem não ser todas relevantes e/ou podem faltar outras. Por exemplo, numa aplicação para calcular o IRS, seria importante incluir o número de identificação fiscal, mas o género é irrelevante.

A abstracção é portanto um conceito fundamental em programação. Os programas são de facto construções abstractas que podem ser executadas por um computador.

Até este momento utilizámos abstracções procedimentais, o que nos permite abstrair o como é que uma função faz daquilo que faz, e que nos permite substituir funções por outras funções que fazem o mesmo ainda que o possam fazer forma diferente ou mais eficiente.

Quando falamos de estruturas de dados e tipos de informação estamos a falar de abstracções de dados, o equivalente às abstracções procedimentais mas para dados. O objectivo é novamente separar o modo como a estrutura de dados pode ser utilizada, e o que representa, da forma como é construída e representada a partir de outros tipos de dados e de outras estruturas de dados.

A título de exemplo podemos considerar as listas em Python. Ainda que saibamos o que representam e como fazer uso das mesmas, utilizando por exemplo operações pré-definidas, não sabemos como é que as listas em Python são representadas internamente. Por outro lado, desde que as listas continuem a representar a entidade que esperamos e que o modo de utilização seja o mesmo, a implementação interna no Python poderá mudar sem que tenhamos de modificar os nossos programas. Esta é uma propriedade fundamental dos tipos de dados abstractos.

Um tipo de dados abstracto é em geral caracterizado pelo conjunto de operações que suporta e pelo conjunto de instâncias ou entidades associadas. Este conjunto de instâncias denomina-se de domínio do tipo e cada instância no conjunto denomina-se elemento do tipo.

Exemplo: vectores

Consideremos um tipo de dados abstracto para representar vectores num espaço bidimensional. Operações a suportar:

Tal como veremos mais tarde, nesta especificação temos respectivamente um construtor, dois selectores, um reconhecedor, um teste e um transformador. Estas operações são essenciais na abstracção de dados e permitem-nos separar o modo como o tipo de dados pode ser utilizado da forma como o tipo de dados é representado e operado internamente. A representação retornada pela operação escreve_vector diz-se representação externa por oposição à representação interna utilizada para representar o tipo de dados internamente.

def cria_vector(x, y):
    if not (isinstance(x, (float, int)) and isinstance(y, (float, int))):
        raise ValueError('cria_vector: x and y must be numbers')
    return (x, y)

def vector_abcissa(v):
    if not e_vector(v):
        raise ValueError('vector_abcissa: v must be a vector')
    return v [0]

def vector_ordenada(v):
    if not e_vector(v):
        raise ValueError('vector_abcissa: v must be a vector')
    return v[1]

def e_vector(e):
    return isinstance(e, tuple) \
        and len(e) == 2 \
        and isinstance(e[0], (float, int)) \
        and isinstance(e[1], (float, int))

def vector_igual(u,v):
    if not (e_vector(v) and e_vector(u)):
        raise ValueError('vector_igual: u and v must be vectors')
    return v[0] == u[0] and v[1] == u[1]

def escreve_vector(v):
    if not e_vector(v):
        raise ValueError('vector_abcissa: v must be a vector')
    return str(v)

Exercício: Implemente uma função que dados dois vectores calcula o seu produto interno. Verifique se ao modificarmos a representação interna do vector é necessário modificar a implementação do cálculo do produto interno.

Exemplo: segundo projecto de 2014/2015

import random


# = = =  Constantes  = = = #

NLINHAS  = 4
NCOLUNAS = 4
DIRECOES = 'NSEW'


# = = = =  T.A.D. coordenada  = = = = #
#
# Um elemento do tipo coordenada e'
# representado como um tuplo de dois
# elementos cujo primeiro elemento
# corresponde a linha e cujo segundo
# elemento corresponde a coluna.
#
# = = = = = = = = = = = = = = = = = = #


# Construtor:

def cria_coordenada(l, c):
    ''' cria_coordenada : int x int -> coordenada
        cria_coordenada(l, c) cria uma coordenada correspondente ao par 
        (l, c). '''
        
    if l < 1 or l > NLINHAS or c < 1 or c > NCOLUNAS:
        raise ValueError ('cria_coordenada: argumentos invalidos')
    
    return (l, c)

# -- Fim: cria_coordenada -- #


# Selectores:

def coordenada_linha(c):
    ''' coordenada_linha : coordenada -> int
        coordenada_linha(c) devolve a linha correspondente a coordenada c.'''
    
    return c[0]

# -- Fim: coordenada_linha -- #


def coordenada_coluna(c):
    ''' coordenada_coluna : coordenada -> int
        coordenada_coluna(c) devolve a coluna correspondente a coordenada 
        c.'''

    return c[1]

# -- Fim: coordenada_coluna -- #


# Reconhecedor:

def e_coordenada(arg):
    ''' e_coordenada : universal -> logico
        e_coordenada(arg) devolve True caso arg seja do tipo coordenada e 
        False em caso contrario.'''
    
    return isinstance(arg, tuple) and \
           len(arg) == 2 and \
           isinstance(arg[0], int) and \
           isinstance(arg[1], int) and \
           1 <= arg[0] <= NLINHAS and \
           1 <= arg[1] <= NCOLUNAS

# -- Fim: e_coordenada -- #


# Teste:

def coordenadas_iguais(c1, c2):
    ''' coordenadas_iguais : coordenada x coordenada -> logico
        coordenadas_iguais(c1, c2) devolve True caso c1 seja igual a c2 e
        False em caso contrario.'''
    
    return c1 == c2

# -- Fim: coordenadas_iguais -- #


# = = = =  T.A.D.  tabuleiro  = = = = #
#
# Um tabuleiro e' representado como 
# uma lista com 5 elementos, os quatro
# primeiros correspondendo 'as linhas 
# do tabuleiro de 2048, e o ultimo 'a
# respectiva pontuacao.
#
# = = = = = = = = = = = = = = = = = = #


# Construtor:

def cria_tabuleiro():
    ''' cria_tabuleiro : {} -> tabuleiro
        cria_tabuleiro() devolve um novo tabuleiro vazio com score inicial
        0.'''
    
    return [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], 0]

# -- Fim: cria_tabuleiro -- #


# Selectores:

def tabuleiro_posicao(t, c):
    ''' tabuleiro_posicao : tabuleiro x coordenada -> int
        tabuleiro_posicao(t, c) devolve o valor da posicao do tabuleiro
        correspondente 'a coordenada c.'''
    
    if not e_coordenada(c):
        raise ValueError ('tabuleiro_posicao: argumentos invalidos')
    
    return t[coordenada_linha(c) - 1][coordenada_coluna(c) - 1]

# -- Fim: tabuleiro_posicao -- #


def tabuleiro_pontuacao(t):
    ''' tabuleiro_pontuacao : tabuleiro -> int
        tabuleiro_pontuacao(t) devolve o valor da pontuacao associado ao 
        tabuleiro t.'''
    
    return t[-1]

# -- Fim: tabuleiro_pontuacao -- #

def tabuleiro_posicoes_vazias(t):
    ''' tabuleiro_posicoes_vazias : tabuleiro -> lista
        tabuleiro_posicoes_vazias(t) devolve uma lista contendo as coordenadas
        de todas as posicoes vazias do tabuleiro t.'''
    
    lista = []
    
    for l in range(NLINHAS):
        for c in range(NCOLUNAS):
            if t[l][c] == 0:
                lista = lista + [cria_coordenada(l + 1, c + 1)]
                
    return lista 

# -- Fim: tabuleiro_posicoes_vazias -- #


# Modificadores: 

def tabuleiro_preenche_posicao(t, c, v):
    ''' tabuleiro_preenche_posicao : tabuleiro x coordenada x int -> tabuleiro
        tabuleiro(t, c, v) coloca o valor v na posicao correspondente 'a 
        coordenada c do tabuleiro t.'''
    
    if not e_coordenada(c):
        raise ValueError ('tabuleiro_preenche_posicao: argumentos invalidos')
    
    if not isinstance(v, int):
        raise ValueError ('tabuleiro_preenche_posicao: argumentos invalidos')
        
    t[coordenada_linha(c) - 1][coordenada_coluna(c) - 1] = v
    
    return t

# -- Fim: tabuleiro_preenche_posicao -- #


def tabuleiro_actualiza_pontuacao(t, pont):
    ''' tabuleiro_actualiza_pontuacao : tabuleiro x int -> tabuleiro
        tabuleiro_actualiza_pontuacao(t, p) actualiza a pontuacao do tabuleiro
        t em p unidades.'''
    
    if not isinstance(pont, int) or pont < 0 or pont % 4 != 0:
        raise ValueError ('tabuleiro_actualiza_pontuacao: argumentos invalidos')
    
    t[-1] = t[-1] + pont
    
    return t

# -- Fim: tabuleiro_actualiza_pontuacao -- #


def tabuleiro_reduz(t, direcao):
    ''' tabuleiro_reduz : tabuleiro x string -> tabuleiro
        tabuleiro(t, d) reduz o tabuleiro t na direcao d.'''
    
    def trata_linha(linha):
        ''' trata_linha : lista -> lista
            trata_linha(l) reduz a linha l de acordo com as regras do 2048, 
            correspondente a uma jogada com direcao do inicio para o fim da
            lista. A funcao e' recursiva e resolve a linha do fim para o 
            inicio. Actualiza (nao localmente) o score do tabuleiro.'''
        
        if len(linha) < 2:
            return linha
    
        # Combina dois numeros iguais no final da linha
        
        elif linha[-2] == linha[-1] != 0:
            linha[-2]  = 0
            linha[-1]  = 2 * linha[-1]
            linha[:-1] = trata_linha(linha[:-1])
            tabuleiro_actualiza_pontuacao(t, linha[-1])
            
        # Elimina espaco no final da linha    
    
        elif linha[-1] == 0:
            linha[1:]  = trata_linha(linha[:-1])
            linha[0]   = 0
            
        # Elimina espaco na penultima casa da linha
    
        elif linha[-2] == 0:
            linha[1:-1] = linha[:-2]
            linha[1:]   = trata_linha(linha[1:])
            linha[0]    = 0
            
        # Ultima posicao fixa, trata resto da linha
    
        else:
            linha[:-1] = trata_linha(linha[:-1])
            
        return linha
    
    # -- Fim: trata_linha -- #
    
    def inverte(t):
        ''' inverte : tabuleiro -> tabuleiro
            inverte(t) inverte a ordem dos elementos em cada linha de t.'''
        
        for l in range(NLINHAS):
            for c in range(NCOLUNAS//2):
                t[l][c], t[l][NCOLUNAS - c - 1] = t[l][NCOLUNAS - c - 1], t[l][c]
                
        return t
    
    # -- Fim: inverte -- #
        
    def transpoe(t):
        ''' transpoe : tabuleiro -> tabuleiro
            transpoe(t) transpoe as linhas/colunas de t.'''
        
        for l in range(NLINHAS):
            for c in range(l):
                t[l][c], t[c][l] = t[c][l], t[l][c]
                
        return t
    
    # -- Fim: transpoe -- #
    
    def reduz_horizontal(t):
        ''' reduz : tabuleiro -> tabuleiro
            reduz(t) efecuta a reducao das linhas do tabuleiro t 
            correspondente a uma jogada na direcao "E".'''
        
        for i in range(NLINHAS):
            trata_linha(t[i])
            
        return t
    
    # -- Fim: reduz_horizontal -- #


    # Corpo principal da funcao reduz
    
    TRANSFORMA = {'N' : lambda t : inverte(transpoe(t)),
                  'S' : transpoe,
                  'E' : lambda t : t,
                  'W' : inverte}
    
    REVERTE    = {'N' : lambda t : transpoe(inverte(t)),
                  'S' : transpoe,
                  'E' : lambda t : t,
                  'W' : inverte}

    
    if direcao not in DIRECOES:
        raise ValueError ('tabuleiro_reduz: argumentos invalidos')
    
    REVERTE[direcao](reduz_horizontal(TRANSFORMA[direcao](t)))
    
    return t
    

# -- Fim: reduz -- #


# Reconhecedores:

def e_tabuleiro(arg):
    ''' e_tabuleiro : universal -> logico
        e_tabuleiro(arg) devolve True se o seu argumento for do tipo 
        tabuleiro e False em caso contrario.'''
    
    if not isinstance(arg, list) or len(arg) != NLINHAS + 1:
        return False
    
    if not isinstance(arg[-1], int) or arg[-1] < 0 or arg[-1] % 4 != 0:
        return False
    
    for i in range(len(arg) - 1):
        if not isinstance(arg[i], list) or len(arg[i]) != NCOLUNAS:
            return False
        else:
            for el in arg[i]:
                if not isinstance(el, int):
                    return False
                
    return True

# -- Fim: e_tabuleiro -- #

    
def tabuleiro_terminado(t):
    ''' tabuleiro_terminado : tabuleiro -> logico
        tabuleiro_terminado(t) devolve True caso o tabuleiro t corresponda
        a um jogo terminado, e False em caso contrario.'''
    
    for l in range(NLINHAS):
        for c in range(NCOLUNAS):            
            if t[l][c] == 0:
                return False
            elif l > 0 and t[l][c] == t[l-1][c]:
                return False
            elif c > 0 and t[l][c] == t[l][c-1]:
                return False
            
    return True

# -- Fim: tabuleiro_terminado -- #


# Teste: 

def tabuleiros_iguais(t1, t2):
    ''' tabuleiros_iguais : tabuleiro x tabuleiro -> logico
        tabuleiros_iguais(t1, t2) devolve True caso t1 seja igual a t2 e
        False em caso contrario.'''
    
    return t1 == t2

# -- Fim: tabuleiros_iguais -- #


# Transformador de saida:

def escreve_tabuleiro(t):
    ''' escreve_tabuleiro : tabuleiro -> {}
        escreve_tabuleiro(t) escreve para o ecra o tabuleiro de 2048 t.'''
    
    if not e_tabuleiro(t):
        raise ValueError ('escreve_tabuleiro: argumentos invalidos')
    
    for l in range(NLINHAS):
        for c in range(NCOLUNAS):
            print('[', tabuleiro_posicao(t, cria_coordenada(l + 1, c + 1)), ']', end=' ')
        print()
        
    print('Pontuacao:', tabuleiro_pontuacao(t))
            
# Funcoes auxiliares:

def copia_tabuleiro(t):
    ''' copia_tabuleiro : tabuleiro -> tabuleiro
        copia_tabuleiro(t) devolve um novo tabuleiro com a mesma configuracao
        que o tabuleiro t.'''
    
    if not e_tabuleiro(t):
        raise ValueError ('copia_tabuleiro: argumentos invalidos')

    tnovo = cria_tabuleiro()
    
    for l in range(NLINHAS):
        for c in range(NCOLUNAS):
            coord = cria_coordenada(l + 1, c + 1)
            tabuleiro_preenche_posicao(tnovo, coord, tabuleiro_posicao(t, coord))
            
    tabuleiro_actualiza_pontuacao(tnovo, tabuleiro_pontuacao(t))
            
    return tnovo

# -- Fim: copia_tabuleiro -- #

def preenche_posicao_aleatoria(t):
    ''' preenche_posicao_aleatoria : tabuleiro -> tabuleiro
        preenche_posicao_aleatoria(t) modifica o tabuleiro t, preenchendo
        uma posicao aleatoria com o valor 2, com probabilidade 0.8, ou 4,
        com probabilidade 0.2.'''
    
    valor = 2 if random.random() < 0.8 else 4    
    coord = random.choice(tabuleiro_posicoes_vazias(t))
    tabuleiro_preenche_posicao(t, coord, valor)
    
    return t

# -- Fim: preenche_posicao_aleatoria -- #

def pede_jogada():
    ''' pede_jogada : {} -> string
        pede_jogada() pede ao utilizador para introduzir uma jogada.'''

    jogada = input('Introduza uma jogada (N, S, E, W): ')    
    
    while jogada == '' or jogada not in DIRECOES:
        jogada = input('Jogada invalida.\nIntroduza uma jogada (N, S, E, W): ')
            
    return jogada

# -- Fim: pede_jogada -- #

def joga_2048():
    ''' joga_2048 : {} -> {}
        joga_2048() permite jogar um jogo de 2048.'''

    t = cria_tabuleiro()
    preenche_posicao_aleatoria(t)
    preenche_posicao_aleatoria(t)
    
    while not tabuleiro_terminado(t):
        escreve_tabuleiro(t)
        jogada = pede_jogada()
        if copia_tabuleiro(t) != tabuleiro_reduz(t, jogada):
            preenche_posicao_aleatoria(t)

    print('Jogo terminado.')
    
if __name__ == "__main__":
    joga_2048()

Exercício: Identifique onde é que são violadas as barreiras de abstracção neste programa.