bits
Transcrição
bits
Computadores e Programação 2003-2004 José António Paixão (T), F4 Fernando Nogueira (P1), D19.D António José Silva (P2), D19.D Página Web da disciplina: http://argus.fis.uc.pt/cp Ver também: http://www.fis.uc.pt link INSITU Programa 2003-2004 • O modelo de von Neumann do computador digital. Arquitectura de um computador moderno. • Representação digital de dados. Códigos binários para representação de inteiros (código de valor absoluto e sinal e código de complementos de 2), reais (vı́rgula flutuante), caracteres (ASCII, unicode), imagem (RGB, JPEG) e som (CD-AUDIO). • Operações numéricas sobre dados binários. Problemas ligados à imprecisão da representação dos números reais em vı́rgula flutuante. • Processadores. Funcionamento de um CPU. Representação binária do código executável de um programa. • Assembladores, compiladores e interpretadores. Linguagens de programação de alto nı́vel e de muito alto nı́vel (VHLL). Sistemas operativos. • Introdução à linguagem de programação Python. • A instrução de atribuição. Aliasing. Noção de ponteiro. • Tipos numéricos: inteiros, inteiros longos, números em vı́rgula flutuante e complexos. • Sequências (listas, tuplas e sequências de caracteres). Iteração sobre sequências e operações de fatiagem (slicing). Abrangências. Dicionários. • Instruções de controlo de fluxo: if..elif..else, while..else, for..else. • Funções. Espaço dos nomes e regras de alcance. Mecanismo de passagem de argumentos e devolução de valores. • Programação funcional e imperativa. Funções puras. As ferramentas de programação funcional lambda, map, filter e reduce. Exemplos de pequenos programas em estilo funcional. • Módulos. Ferramentas de introspecção e metaprogramação. • Ficheiros. Formatação. Redirecção dos canais de fluxo de entrada e saı́da. • Programação orientada por objectos. Noção de classe e instâncias de classe. Atributos e métodos. Herança, encapsulamento e polimorfismo. Sobrecarga de operadores. Objectos persistentes: módulos pickle e shelve. • Excepções. As instruções raise e try..except..finally. • Recursão. Iteradores e geradores. • Aplicações em Análise Numérica e a problemas de Fı́sica. – Resolução de equações não-lineares: método da bissecção, iteradores de ponto fixo, Regula-Falsi e Newton-Raphson. – Interpolação de Lagrange. – Ajuste de curvas a dados experimentais pelo método dos mı́nimos quadrados. – Derivação e integração numérica: método do trapézio, regra de Simpson. Mı́nimos de curvas de energia potencial intramoleculares, momentos de inércia, etc. – Resolução de equações diferenciais ordinárias: método de Euler, Heun (preditor-corrector), Runge-Kutta e Runge-Kutta-Fehlberg (de passo adaptado). Equações do decaimento radioactivo, osciladores, projécteis, movimento planetário, etc. – Monte-Carlo. Avaliação A avaliação da disciplina é feita por frequência (opcional) e por exame. Uma classificação de frequência igual ou superior a 10 valores concede dispensa de exame. Os exames e a frequência têm ambos uma parte teórica e uma parte prática com a cotação de 12 e 8 valores, respectivamente. A parte teórica contém uma série de 20 questões de escolha múltipla e 2 ou 3 questões de desenvolvimento. A parte prática consiste na resolução de problemas (algoritmo e codificação do programa frente ao computador). A parte prática da frequência desenrola-se em duas sessões (3 + 3 problemas, 2 * 4 valores). Nos exames a parte prática é uma sessão única com 3 problemas. É garantida a nota da parte prática de frequência se for igual ou superior a 4 valores. Na parte prática é permitida a consulta a todo o material de apoio. A parte teórica é sem consulta. O computador digital Computador digital: sistema digital programável que permite armazenar e processar informação em formato digital a velocidades elevadas, realizando operações aritméticas e lógicas elementares. Programa: sequência de instruções que processam os dados. Quer os programas quer os dados são armazenados na memória do computador em formato digital usando um código binário. O 1◦ computador digital (ABC) foi construı́do em 1939 (Atanasoff/Berry). 1944 (MARK I, modelo de von Neumann): velocidade de processamento: 0.00083 MIPS. O EDVAC (1952) foi um dos primeiros computadores electrónicos e ocupava a superfı́cie de um campo de futebol. Modelo de von Neumann O computador é constituı́do pelas seguintes unidades funcionais: • Memória central • Unidade aritmética e lógica • Unidade de controlo • Unidades de entrada e saı́da Hoje em dia o processador engloba a unidade aritmética e lógica e a unidade de controlo. Modelo de von Neumann Unidade de controlo C C I C Unidade A/L D Unidades de E/S R D R D R ´ Memoria Central O processador não efectua operações directamente sobre a memória (à excepção da transferência de dados). O processamento é feito em células especiais de memória no interior da UAL denominadas registos. A tranferência de dados entre a memória central e os dipositivos de entrada e saı́da pode ser feita passando pelo processador ou através de acesso directo à memória (DMA). Informação digital • Os computadores processam informação digital • Grandeza analógica: varia de forma contı́nua (ex: temperatura lida num termómetro de Hg) • Grandeza digital: descontı́nua (existe apenas um número finito de estados). Elemento mı́nimo de informação: dı́gito binário (bit). bit: 2 estados 0 ou 1 Um conjunto de n bits pode tomar 2n configurações distintos, podendo representar 2n objectos ou informações elementares. Exemplo com 3 bits: Configurações possı́veis (8 = 23 ): 000, 001, 010, 011, 100, 101, 110, 111. Podemos utilizar estas combinações de bits para representar de forma unı́voca 8 letras do alfabeto: A 000 E 100 B 001 F 101 C 010 G 110 D 011 H 111 Codificação/descodificação • Os computadores processam (apenas) informação/dados em formato digital. Todos os dados têm que ser codificados num formato digital para serem passı́veis de tratamento informático. Para cada tipo de dado tem de se estabelecer um código que permita atribuir uma configuração binária única a cada dado desse tipo → codificação. A operação inversa é a descodificação. Vamos passar em revista alguns dos códigos actualmente usados para representação digital de vários tipos de dados num computador: • Números inteiros • Números inteiros relativos • Números reais • Caracteres • Som • Imagem • etc,... Codificação de inteiros A forma mais simples de representar um número inteiro num formato digital corresponde a utilizar a sua representação binária, isto é, na base 2. O número é representado por n bits an−1 an−2 · · · a0 onde ai = 0, 1 e i = 0, ..., n − 1. O inteiro correspondente a esta sequência de bits é: an−1 2 n−1 + an−2 2 n−2 . . . + a0 = n−1 ∑ ai 2i i=0 Este código designa-se por código binário ponderado. O maior número inteiro que é possı́vel representar com um conjuto de n bits é n−1 ∑ 2n = 2 n − 1 i=0 n bits → 2n números inteiros, incluindo o zero: 0, 1, . . ., 2n−1 . Conversão decimal/binário A conversão de um número inteiro na base 10 em código binário ponderado é muito fácil. Exemplo: Conversão do decimal 203 para a base 2. Representando o quociente e o resto da divisão inteira pelos operadores div e mod, temos: 203 101 50 25 12 6 3 1 div div div div div div div div 2 2 2 2 2 2 2 2 =101; 203 mod 2 = 1 = a0; = 50; 101 mod 2 = 1 = a1; = 25; 50 mod 2 = 0 = a2; = 12; 25 mod 2 = 1 = a3; = 6; 12 mod 2 = 0 = a4; = 3; 6 mod 2 = 0 = a5; = 1; 3 mod 2 = 1 = a6; = 0; 1 mod 2 = 1 = a7; Então 20310 = 110010112 A conversão inversa (binário → decimal) é trivial. Exemplo: 110010112 = 1 × 27 + 1 × 26 + 1 × 23 + 1 × 2 + 1 = 20310 Notação octal/hexadecimal É comum utilizar-se a notação hexadecimal, (base 16) para exprimir de forma mais compacta números binários. Cada dı́gito hexadecimal equivale a 4 dı́gitos binários. Noutra notação, denominada octal, o número é representado na base 8, em que cada dı́gito octal (0 . . . 7) agrupa 3 bits. Em hexadecimal são utilizados os 16 sı́mbolos 0, 1, 2, . . . , 9, A, B, C, D, E, F. Por exemplo, o número 20010 pode ser escrito como 110010002 , C816 ou 3108 . Binário 0000 0001 0010 0011 0100 0101 0110 0111 Decimal 0 1 2 3 4 5 6 7 Hexa. 0 1 2 3 4 5 6 7 Octal 0 1 2 3 4 5 6 7 Binário 1000 1001 1010 1011 1100 1101 1110 1111 Decimal 8 9 10 11 12 13 14 15 Hexa. 8 9 A B C D E F Octal 10 11 12 13 14 15 16 17 A transcrição de um número inteiro na base 10 para base 2 pode ser facilitada, sobretudo quando se trata de números grandes, fazendo primeiro a transcrição para hexadecimal, e utilizando a tabela acima para transcrever cada dı́gito hexadecimal na correspondente sequência de 4 algarismos binários. Adição de números inteiros Como é que os computadores fazem operações aritméticas? Comecemos por um exemplo simples, a adição de números inteiros. Começando pelo bit menos significativo, adicionam-se os 2 primeiros bits, escrevendo por baixo a respectiva soma. Se o resultado exceder a capacidade de um único bit faz-se o “transporte” de um 1 para a coluna seguinte, e soma-se esse transporte conjuntamente com os bits dessa coluna. Por conveniência, vamos representar no bit Ci de um conjunto auxiliar de n bits o transporte que vai da coluna i para a coluna i + 1 e nos bits Ti de um outro conjunto auxiliar o transporte que vem da coluna i − 1 para a coluna i. A operação adição em binário toma então a seguinte forma simbólica: Tn Tn−1 ... T1 T0 An An−1 . . . A1 A0 Bn Bn−1 . . . A1 B0 Sn Sn−1 ... S1 S0 Cn Cn−1 . . . C1 C0 Nota: T0 = 0 e Ti = Ci − 1. Se Cn 6= 0 → ocorreu transbordo (overflow) ! Adição de números inteiros (cont.) Consideremos um computador que utiliza 8 bits para codificar inteiros. O maior número inteiro representável nesta codificação é 255 (confirme!). A operação de adição em binário efectua-se de forma análoga à adição em decimal: Exemplo: 100 + 88 = 188. 10010 = 011001002 8810 = 010110002 T 10000000 A 01100100 = 100 B 01011000 = 88 S 10111100 = 188 C 01000000 C7 = 0 → OK Exemplo de transbordo em 8 bits: 189 + 88 = 277 > 28 − 1 = 255. T 11110000 A 10111101 = 189 B 01011000 = 88 S 00010101 6= 277 C 11111000 C8 = 1: resultado inválido Adição de números inteiros (cont.) Os dı́gitos binários S e C são funções (booleanas) de 3 variáveis: os dois digitos a adicionar e o transporte anterior: S(A, B, T ); C(A, B, T ). A tabela de verdade destas funções booleanas é a seguinte: A B T S C 0 0 0 0 0 0 0 1 1 0 0 1 0 1 0 0 1 1 0 1 1 0 0 1 0 1 0 1 0 1 1 1 0 0 1 1 1 1 1 1 Pode-se mostrar que as funções S e C têm a seguinte forma algébrica, S(A, B, T ) = ĀB̄T + ĀBT̄ + AB̄T̄ + ABT C(A, B, T ) = AB + BT + AT onde a disjunção e conjunção lógicas são representandas pelo soma e produto, e X̄ representa a negação do valor lógico de X. (Verifique a tabela de verdade!) Implementação electrónica das operações lógicas É possı́vel implementar funções lógicas utilizando circuitos electrónicos elementares (gates) capazes de executar as operaç ões lógicas “e” (AND), “não” (NOT) e “ou” (OR). Se representarmos os valores “0” e “1” por um sinal eléctrico (ex. 0 = 0 volts e 1 = 5 volts), as gates dão um sinal eléctrico à saida correspondente ao valor lógico da operação. Estes circuitos são representados internacionalmente pelos seguintes sı́mbolos: Α Α ¬Α Α Α∧Β Β NOT Α∨Β Β AND OR A B A∧B A B A∨B A ¬A 0 0 0 0 0 0 0 1 0 1 0 0 1 1 1 0 1 0 0 1 0 1 1 1 1 1 1 1 Gates lógicas Circuito somador de 1 bit A ¬A B ¬B T ¬T AB AT C BT ¬A¬BT ¬AB¬T S A¬B¬T ABT Somador de 1 bit. Circuito somador de 16 bits B15 A15 T15 Somador 1 bit # 15 C15 S15 B 2 A 2 T2 Somador 1 bit # 2 C2 S2 B 1 A 1 T1 Somador 1 bit # 1 C1 S1 B 0 A 0 T0 = 0 Somador 1 bit # 0 C0 S0 S = A+B Somador de 16 bits. A = A15 · · · A2 A1 A0 B = B15 · · · B2 B1 B0 S = S15 · · · S2 S1 S0 T0 = 0 Ti+1 = Ci Testando o valor do bit C15 é possı́vel verificar se ocorreu transbordo. A operação é inválida se C15 = 1. NB: Actualmente, as somas são efectuadas por um outro tipo de circuitos, diferente do apresentado – multiplexadores. Codificação de números relativos Existem vários códigos que permitem codificar números inteiros relativos (códigos bipolares). O código bipolar mais simples é o código de sinal e valor absoluto: • o bit mais significativo representa o sinal • os restantes n − 1 bits o valor absoluto. Por convenção, o bit de sinal tem o valor 1 quando o número é negativo e o valor 0 quando é positivo. Por exemplo, numa palavra de 8 bits, os números ±19 têm, neste código, a seguinte representação binária: +1910 = 00010011 −1910 = 10010011 Vantagem deste código: simplicidade. Desvantagens: • podem-se representar apenas 2n − 1 números distintos com n bits e não 2n (o zero tem representação dupla) • a adição de números relativos é problemática. Código de complementos de 2 No código de complementos de 2 representam-se os números positivos como no código anterior, e representa-se cada número negativo somando 1 ao complemento do número positivo que lhe corresponde. Um eventual transbordo que ocorra nesta soma é ignorado. Vejamos como representar o número −19 numa palavra de 8 bits usando este código: +1910 = 00010011 −1910 = 11101100 + 00000001 = 11101101 Regra prática para converter um número binário no respectivo complemento de 2: • copiam-se os dı́gitos, a começar pelo bit menos significativo até encontrar o primeiro “1”, que também se copia. A partir daı́, substituem-se os “0”s por “1”s e vice-versa. Vantagens da representação de complementos de 2: • O zero tem representação única (verifique!). • O bit mais significativo pode ser interpretado como bit de sinal: este bit é zero nos números positivos e 1 nos números negativos. • O bit menos significativo determina sempre se um número é par ou ı́mpar, o bit é zero nos números pares e 1 nos ı́mpares. • O número total de números que é possı́vel representar neste código numa palavra de n bits é 2n , correspondendo ao intervalo −2(n−1) , . . . , 2(n−1) − 1. Adição de números relativos A implementação da adição de números relativos é muito simples em complementos de 2. Utiliza-se o mesmo algoritmo da adição de números inteiros, sendo apenas necessário ter atenção a um eventual transbordo, cuja detecção é mais delicada. Exemplo: T 00001010 A 00000101 = 5 B 11110101 = −11 S 11111010 = −6 C 00000101 A subtracção é feita de forma semelhante: para subtrair A de B, soma-se a B o complemento de 2 de A , ou seja B − A = B + (−A). Transbordo A inspecção do bit de transporte Cn já não é suficiente para testar se ocoreu transbordo! Exemplos: T 11111110 A 00000001 = 1 B 11111111 = −1 S 00000000 = 0 C 11111111 bit de transporte = 1; resultado correcto. Adição de números relativos (cont.) T 11111110 A 01111111 = 127 B 00000001 = 1 S 10000000 = −128 bit de transporte = 0; resultado C 01111111 incorrecto. As seguintes regras devem ser aplicadas para verificar se há transbordo numa soma de números inteiros relativos em complementos de 2: 1. Se A e B têm sinais diferentes, nunca ocorre transbordo na operação A + B e o resultado é sempre válido. 2. Se A e B têm o mesmo sinal e A + B o sinal oposto, então ocorreu transbordo e o resultado é inválido. Forma altenativa de detectar um resultado inválido: comparar os bits Tn e Cn ; se não forem iguais ocorreu transbordo e o resultado da operação é inválido. Multiplicação e divisão de números inteiros relativos Para multiplicar um número por 2, basta deslocar os bits para a esquerda uma casa, colocando um 0 no bit menos significativo. Para dividir um número por 2, deslocam-se todos os bits para a direita uma casa, mantendo o bit do sinal com o mesmo valor. Exemplo: 54 = 00110110 54 ∗ 2 = 54 << 2 = 01101100 54/2 = 54 >> 2 = 00011011 No caso de números ı́mpares, o deslocamento para a direita dá como resultado a divisão por 2 arredondada para −∞, ou seja, 51 deslocado de um bit para a direita dá como resultado 25; a mesma operação efectuada em −51 dá como resultado −26. Verifique! As operação de deslocamento de bits podem ser implementadas de forma eficiente nos circuitos de electrónica digital. As multiplicações (e divisões) de inteiros por potências de 2 contam-se entre as operações mais rápidas que o processador pode efectuar, bem mais rápidas do que uma multiplicação ou divisão por outros números! Combinando as operações de adição e de shift é possı́vel efectuar todas as operações aritméticas sobre inteiros. Números reais: representação em vı́rgula flutuante Os números reais formam um conjunto denso → não é possı́vel representar exactamente um (intervalo) de reais num computator! Representação em vı́rgula fixa: número fixo de casas decimais, digamos 6. Basta codificar um inteiro (o número sem a vı́rgula)! Não é uma boa escolha → limita a gama dinâmica de valores que se podem representar com um conjunto de n bits. Utiliza-se a notação em vı́rgula flutuante, semelhante à notação cientı́fica: mmmmmm × 10eeee , • mmmmmm → mantissa • eeee → expoente Por exemplo, o número 0.000341237 pode ser escrito de várias formas: 0.00341237 × 10−1 , 0.0341237 × 10−2 , . . . 3.41237 × 10−4 A última é a forma canónica normalizada (não tem zeros atrás do ponto decimal). A representação em vı́rgula flutuante é efectuada habitualmente em 32, 64 ou 128 bits (precisão simples, dupla, e quádrupla). Em precisão simples: s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm Representação em vı́rgula flutuante (IEEE754) • bit 31 → bit de sinal • bits 30 · · · 23 expoente • bits 22 · · · 0 mantissa Nota: utiliza-se normalização, pelo que se subentende que o primeiro algarismo da mantissa é diferente de zero. Na base 2 este algarismo só pode ser 1, e portanto não se representa. Assim, usando 23 bits a mantissa tem 24 bits efectivos de precisão! O expoente é codificado com um offset de 127, ou seja é preciso subtrair 127 ao número inteiro que representa o expoente para obter o seu valor real. Os valores reais do expoente com significado variam entre -127 e 127 (0 a 254 sem offset). Reserva-se o padrão eeeeeeee = 11111111 = 255 para representar os infinitos (s = 0, m = 0, +∞), (s = 1, m = 0, −∞). Também existem dois “zeros” (s = 0, m = 0, 0+ ) e (s = 1, m = 0, 0− ). Os padrões com a mantissa 6= 0 e expoente = 255 não representam números, mas códigos de erro (NAN - not a number, por exemplo divisão por zero). P. bits sinal mants. expo. min max alg. sign. S 32 1 23 8 1.2E-38 3.4E38 6–9 D 64 1 52 11 2.2E-308 1.8E308 15-17 Representação em vı́rgula flutuante (cont.) Numa codificação de 32 bits, não se pode armazenar mais informação do que a permitida pelos 32 bits→ o número de “números reais” com representação distinta neste esquema de codificação = 232 , tal como para os números inteiros. A interpretação dos bits é diferente, mas o número de possibilidades distintas de codificação é o mesmo. No caso dos números inteiros, a distribuição é uniforme, mas já o mesmo não se passa com a representação em vı́rgula flutuante! Os “números reais” representados neste formato não estão uniformemente distribuidos. De facto, a distribuição é uniforme numa escala logarı́tmica: • existem tantos números (224 para uma mantissa de 23 bits, ou seja cerca de 8 milhões) entre 2−3 e 2−2 como entre 252 e 253 . A representação dos números reais em vı́rgula flutuante não é (em geral) exacta e a aritmética efectuada com esta representação também não! O processo de normalização pode causar sérios erros de arredondamento. Exemplo: matematicamente 1 + x − 1 = x, mas em aritmética de vı́rgula flutuante 1 + x − 1 pode resultar num número muito diferente de x. Caracteres Da mesma forma que sequências de bits podem representar números, também podem representar letras e outros sinais de pontuação. O código mais comum para representação de caracteres é o ASCII (American Standard Code for Information Exchange). A codificação é feita em 7 bits, sendo possı́vel representar 27 = 127 sı́mbolos. Alguns destes sı́mbolos correspondem a caracteres de controle de periféricos (impressora, modem...). Nota: no código ASCII há um offset constante = 32 entre o código das letras minúsculas e maiúsculas. Bit # 6 = 1 → letra minúscula. Outros códigos: EBCDIC (IBM), Icon (Unysis), em desuso. No código ASCII estão contemplados todos os caracteres da lı́ngua inglesa, mas não caracteres utilizados noutras lı́nguas, como os caracteres acentuados e o “ç” do português. É possı́vel utilizar o oitavo bit para extender o conjunto ASCII com 127 novos sı́mbolos que incluem todos os caracteres acentuadas da maioria das lı́nguas ocidentais mas não de outras regiões do mundo. Para fazer face a este problema foi desenvolvido um novo standard internacional, unicode, que se está a impor rapidamente. Na versão 3.2 do unicode cada caracter é codificado num (máximo) de 32 bits, dando para 4 294 967 296 caracteres/pictogramas diferentes. Suficiente, mesmo para o chinês! Codificação de som O formato digital de som mais comum é o PCM CD-Digital Audio (PCM CD-DA) que é utilizado para gravar música em CDs. Este formato foi especificado em 1980 pela Philips e pela Sony, inventores do disco compacto. O som é digitalizado com uma velocidade de amostragem de 44.1 kHz, cerca do dobro da frequência máxima audı́vel para os humanos. A amostragem é feita em 2 canais (estéreo), com uma resolução de 16 bits. Assim, cada segundo de som num CD ocupa 44100 × 2 × 2 = 176400 octetos. Os dados são registados no disco em blocos de 2352 octetos. Cada segundo de som ocupa 75 blocos. Um CD tem a capacidade de 74 minutos de registo, ou seja 2352 × 75 × 74 × 60 = 783216000 octetos, cerca de 747 MB. Cada minuto de música no formato PCM CD-DA, sem compressão, ocupa 10 MB! Naturalmente, técnicas de compressão de dados permitem hoje em dia registar som, com boa qualidade, ocupando menos “espaço”. É o caso dos formatos digitais MP3 e OGG, populares na internet. Existem ainda vários outros formatos “aúdio” em uso, como o utilizado pela Microsoft nos ficheiros “WMA”. Recentemente, a dupla Philips/Sony lançou um novo formato de áudio digital, o Super Áudio CD: SA-CD. Este formato tem uma qualidade áudio superior e permite registar vários canais (suround). Codificação de imagem Uma imagem pode ser guardada em formato digital (scanner, câmara fotográfica digital,...) e processada por computador. Na digitalização, a imagem é dividada num número elevado de pequenos elementos - os pixels. Uma imagem registada numa câmara fotográfica digital tem entre 2–5 milhões de pixels. Imagem a preto e branco (sem graus de cinzento): cada pixel só contém um elemento de informação (branco ou negro) que pode ser representado por um unico bit. Uma imagem de 600 × 400 pixels ocupa cerca de 24k octetos. Sendo necessário registar nı́veis de cinzento, por exemplo de uma fotografia a preto e branco, é comum utilizar-se um octeto (8 bits) por pixel, o que permite descriminar entre 28 = 256 nı́veis de cinzento. Imagem a cores: é necessário codificar, para cada pixel, a percentagem das cores primárias, vermelho (R), verde (G) e azul (B) do pixel. Se utilizarmos 8 bits para codificar a percentagem de cada uma das cores primárias, serão necessários 24 bits por pixel. A esta codificação dá-se o nome de RGB de 24 bits. Permite representar 224 ∼ 16.7 milhões de cor distintas. Uma imagem colorida codificada em RGB-24, mesmo de pequena dimensão, ocupa muitos octetos! Daı́ ser comum utilizar uma técnica de compressão (JPG, GIF, ...) que permita reduzir fortemente o “espaço” ocupado pela imagem digitalizada, sem degradar apreciavelmente a sua qualidade. Código executável (programas) Os programas, a serem executados pelo processador, também são armazenados em código binário e têm de ser transferidos para a memória RAM do computador antes da sua execução. O programa é uma sequência de instruções. O conjunto de instruções, e a sua codificação, variam muito de processador para processador, pelo que um certo código-máquina só corre num dado processador. Suponhamos, um processador de 16 bits onde os 4 bits mais significativos são usados para codificar a instrução e os 12 bits restantes os dados, com o seguinte conjunto de instruções: Código Instrução Operando 0000 Pausa/start - 0001 Carregar registo # memória 0010 Transferência p/ memória # memória 0011 Soma (ao registo) # memória 0100 Multiplica (o registo por) # memória 0101 Salto # memória Consideremos o seguinte programa que ocupa os endereços 0. . . 21 (0. . . 15 em hexadecimal) da memória: Endereço Instruções/Dados 0,1 0000 2,3 1010 4,5 4012 6,7 300E 8,9 3014 A,B 2016 C,D 5000 E,F 0010 10,11 0011 12,13 0100 14,15 0111 Interpretando cada uma das instruções, verificamos que este programa efectua o cálculo 17 × 32 + 16 + 49, ou ainda Y ∗V + X +W , onde X, Y , V e W são os valores armazenados nos pares de octetos 14-15, 16-17, 18-19, 20-21, sendo o resultado armazenado nos octetos 22-23. Utilizando uma linguagem mnemónica das instruções código-máquina (assembler) o programa fica mais legı́vel: X #14,16 Y #16,17 Z #18,32 W #20,49 U #22,0 LOD Y MTP V ADD ADD STR JMP X W U STP Numa linguagem de alto nı́vel, este pequeno programa seria expresso pelo seguinte código: X = 16 Y = 17 V = 32 W = 49 U = Y*V+X+W print U Um outro programa (compilador ou interpretador) traduz estas instruções em código-máquina, previamente à sua execução. Num compilador, a tradução do programa é efectuada de uma só vez. Num interpretador as instruções são traduzidas uma de cada vez, à medida que são executadas. A escrita de um programa numa linguagem de alto nı́vel é muito menos penosa do que em linguagem máquina, ou assembler. Existem várias linguagens de alto nı́vel: FORTRAN, Pascal, Ada, C, Módula, BASIC, LISP, Scheme, Algol, Erlang, Java, Python, etc. Linguagens de programação • 1949: Primeiro Assembler (ShortCode). • 1951: Grace Hooper escreveu o primeiro compilador. • 1957: FORTRAN: FORmula TRANslator. A linguagem dominante no cálculo cientı́fico. Várias encarnações: FORTRAN IV, FORTRAN 77, FORTRAN 90, FORTRAN 95, FORTRAN 2000. • 1958: LISP (John McCarthy, MIT): Desenhada para Inteligência Artificial. Novo paradigma de programação. • 1958: ALGOL: criada por um comité cientı́fico internacional para uso cientı́fico. Foi a primeira linguagem a dispôr de uma gramática formal BNF. Recursão. ALGOL68. • 1959: COBOL: COmmon Business Oriented Language (contabilidade). • 1964: BASIC (John Kemeny, Thomas Kurtz). Linguagem interpretada. Simples, embora limitada. • 1968: PASCAL (Niklaus Wirth, ETH). Programação estruturada. Desenvolvida num ambiente académico, encoraja bons hábitos de programação. Alocação dinâmica de memória. Sucessores: MODULA, MODULA-2. • 1972: C (Dennis Ritchie, Bell Labs): Sucedeu ao ”B”e ao ”BCPL”, inclui muitas das estrutura de controlo de fluxo tornadas populares pelo Pascal. Desenhada para ser simples, rápida e eficiente (aritmética de pointers, alocação dinâmica de memória). Próxima da máquina. Poderosa mas não amigável. Utilizada para programação do sistema. Ligação estreita com o UNIX (escrito em C). Classificada como “assembler de alto nı́vel”... • 1983: C++ (Bjarne Stroustroup ): C with Classes. Programação orientada por objectos. • 1987: Perl (Larry Wall): Linguagem de scripting. Poderosa para tratamento de texto, internet. • 1990: Python (G. van Rossum). linguagem de scripting para o sistema distribuido Amoeba, nasceu num meio académico (Univ. de Amesterdão). Descendente do ABC. Programação orientada por objectos, influenciada pelo C e muitas outras linguagens. • 1994: Java (Sun Labs). Multiplataforma, compilação intermédia para bytecode, programação na internet. Paradigmas de programação... • Programação imperativa: FORTRAN, PASCAL, C, ..., Python. • Programação funcional: LISP, Scheme, Miranda, Erlang, ..., possı́vel em Python. • Programação por objectos: Smalltalk ,C++, Java, Python. • Scripting: Perl, TCL/Tk, Python. Sistema operativo Sistema operativo: programa de baixo-nı́vel que tem a seu cargo a interface com o hardware, incluindo periféricos. Distribui os recursos do sistema (tempo de CPU, memória) entre as várias tarefas em execução (programas) . Sistema operativo moderno: • núcleo (kernel): pequeno programa que está sempre a correr no computador e efectua a gestão dos recursos (atribuição de memória, prioridades dos programas); • programas que intercomunicam com o kernel e entre si, muitas vezes numa relação de cliente/servidor. Alguns sistemas operativos incluem de raı́z uma interface gráfica (Windows), outros não. Não há só o Windows... Sistemas operativos: 386BSD, AIX, AOS, Amoeba, Angel, Artemis microkernel, BeOS, Brazil, COS, CP/M, CTSS, Chorus, DACNOS, DOSEXEC 2, GCOS, GEORGE 3, GEOS, ITS, KAOS, Linux, LynxOS, MPV, MS-DOS, MVS, Mach, MacOS, MINIX, Multics, Multipop-68, Novell NetWare, OS-9, OS/2, Pick, Plan 9, QNX, RISC OS, STING, System V, System/360, TOPS-10, TOPS-20, TRUSIX, TWENEX, TYMCOM-X, Thoth, Unix, VM/CMS, VMS, VRTX, VSTa, VxWorks, WAITS, Windows 3.1, Windows 95, Windows 98, Windows NT, . . . . Sistemas operativos mais populares: • grandes sistemas: UNIX, VMS, AX/P, AU/X. • computadores pessoais: Windows, MacOS, Linux, FreeBSD. O que é o Python? O Python é uma VHLL (Very High Level Language). O próprio criador da linguagem dá dela a seguinte definição: Python is an interpreted, interactive, object-oriented programming language. Python combines remarkable power with very clear syntax. It has modules, classes, exceptions, very high level dynamic data types, and dynamic typing. Guido van Rossum (Univ. Amesterdão), que criou esta linguagem de programação em 1991, teve como fonte de inspiração para o nome a série britânica de humor da década de 70 “Monty Python Flying Circus”, de que é fã incondicional. . . And now for something different... Who ordered the Spanish Inquisition? O Python ainda está em desenvolvimento por uma grande equipa de colaboradores, liderada por GvR. A licensa de utilização é do tipo GPL. Está disponı́vel gratuitamente (em código fonte, C) para a maioria dos sistemas operativos. Humor pitónico... Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren’t special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one– and preferably only one –obvious way to do it. Although that way may not be obvious at first unless you’re Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it’s a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea – let’s do more of those! copyleft: Tim Peters. Programação em Python Estrutura de um programa em Python: • Os programas são compostos de módulos • Os módulos contêm instruções e expressões • As instruções e as expressões criam e processam objectos. O que é um objecto? É dificil dar, nesta altura, uma definição rigorosa deste conceito. De uma forma simples, os objectos correspondem a uma certa região da memória do computador à qual está associada um endereço único e na qual estão armazenados: • dados • informações sobre os dados • funções que actuam sobre estes dados. A instrução básica em Python consiste em criar um objecto e dar-lhe um nome; a esta instrução dá-se o nome de atribuição (de nome!). Exemplo: >>> x = 123 cria o objecto 123, algures na memória do computador e dá-lhe o nome x. Diz-se também que x é uma referência para o objecto 123 ou que “aponta” para o objecto 123, tudo com o mesmo significado. Depois de criados, os objectos passam a ser referidos através dos seus nomes. Por exemplo, a instrução: >>> print x imprime no ecrã o valor do objecto cujo nome é x. Nem todos os objectos têm um valor, mas todos os objectos têm um tipo e um endereço únicos. Para obter o tipo de um objecto, utiliza-se a instrução type(nome), neste caso type(x). >>> x = 123 >>> type(x) <type ’int’> >>> Para obtermos o endereço de um objecto utiliza-se a instrução id(nome). Assim: >>> x = 1.23456 >>> print x 1.23456 >>> type(x) <type ’float’> >>> id(x) 135625436 >>> É possı́vel dar mais do que um nome a um mesmo objecto. A esta operação dá-se o nome de aliasing e pode revelar-se útil em certas casos. >>> x = 45 >>> y = 45 >>> id(x) 135363888 >>> id(y) 135363888 >>> Pode-se ainda fazer-se este “baptismo duplo” na mesma linha: >>> x = y = 45 >>> id(x) 135363888 >>> id(y) 135363888 >>> No entanto, um mesmo nome não pode ser usado por mais do que um objecto ao mesmo tempo! >>> x = 20 >>> x = 43 >>> print x 43 >>> Prevalece sempre a última instrução de atribuição... O objecto 20 deixou de ter nome e o Python encarregar-se-á de apagar os objectos sem nome atribuı́do da memória do computador (recolha de lixo). Tipos e categorias de objectos Cada objecto tem um tipo e uma categoria. Existem vários tipos de objectos: • os números, dos quais há os seguintes tipos: inteiros, inteiros longos, números em vı́rgula flutuante, números complexos. • as colecções: sequências e mapas. Nas sequências temos os tipos listas, tuplas e cadeias de caracteres. Nos mapas, existe um tipo único, o dicionário. • funções, classes e métodos • ficheiros • etc... Cada objecto pertence a uma das categorias: mutável ou imutável. Um objecto imutável não pode ser alterado, uma vez criado só lhe podemos dar ou mudar o nome, ou destruı́-lo. Um objecto mutável pode sobre alterações. Nota: um objecto pode destruir-se com a instrução del. >>> x = 123 >>> print x 123 >>> del(x) >>> print x Traceback (most recent call last): File "<stdin>", line 1, in ? NameError: name ’x’ is not defined Importante: os números, as cadeias de caracteres e as tuplas são objectos imutáveis. Algumas notas sobre os tipos numéricos: • inteiros: representação em complementos de 2 usando 32 bits. • inteiros longos: quando o número inteiro está fora do intervalo [−2147483648, 2147483647], utiliza o número de bits necessário, dentro da disponibilidade de memória do computador. • vı́rgula flutuante: representação IEEE754 de dupla precisão (64 bits.) • complexos: a parte real e imaginária são codificados em 64 bits. os sı́mbolo j e J representam a unidade imaginária. Exemplo: z = 2+3j. As expressões aritméticas habituais podem ser usadas com objectos do tipo numérico. A conversão de inteiro → longo inteiro → vı́rgula flutuante funciona do modo esperado, excepto no caso da divisão de inteiros, que é entendida como a divisão inteira. Exemplos: >>> n = 1 >>> z = 2*n >>> print z 2 >>> x = 1.23456 >>> print 2*3.456+5*x 13.0848 >>> print z/3 0 Expressões aritméticas Estão disponı́veis os seguintes operadores aritméticos binários: Operador Significado Precedência + soma 0 - subtracção 0 * multiplicação 1 / divisão 1 // divisão inteira 1 % resto da divisão inteira 1 ** potência 2 Na avaliação de uma expressão aritmética a operador potência têm o mais elevado grau de precedência, seguindo-se a multiplicação, divisão e resto da divisão (todas ao mesmo nı́vel) e por último as somas e subtracções. Exemplo: >>> print 2**3+2*2 12 Quando se pretenda impor uma ordem de avaliação diferente da imposta pelos graus de precedência dos operadores, devem utilizar-se parêntesis: >>> print 2**(3+2*2) 128 Operadores bit a bit É também possı́vel efectuar operações bit a bit com os seguintes operadores: Operador Significado Precedência | OR binário 0 ‘ XOR binário 1 & AND binário 2 >> deslocamento para a direita 3 << deslocamento para a esquerda 3 ∼ NOT binário 4 >>> x = 1 >>> x << 2 4 Notar que as operações aritméticas têm precedência sobre as operações bit a bit! >>> x = 1 >>> x <<2+1 8 >>> (x<<2)+1 5 Números complexos Números imaginários são escritos com o sufixo j or J. Números complexos têm a forma (real+imag j), e podem também ser criados com a função complex(real,imag). >>> 1j * 1J (-1+0j) >>> 1j * complex(0,1) (-1+0j) >>> 3+1j*3 (3+3j) >>> (3+1j)*3 (9+3j) >>> (1+2j)/(1+1j) (1.5+0.5j) Um número complexo z possui uma parte real e imaginária, accessı́veis atravéz de z.real e z.imag. Para calcular o módulo do número complexo usa-se a função abs(z). >>> >>> 3.0 >>> 4.0 >>> 5.0 >>> z = 3.0+4.0j z.real z.imag abs(z) # sqrt(z.real**2 + z.imag**2) Não é possı́vel converter directamente um número complexo num número inteiro ou de vı́rgula flutuante, mesmo quando a sua parte imaginária é zero! >>> a = complex(3,0) >>> float(a) Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: can’t convert complex to float; use e.g. abs(z) As funções disponı́veis no módulo math não funcionam com números complexos. As funções para números complexos estão disponı́veis no módulo cmath. >>> import math,cmath >>> print math.sqrt(-1) Traceback (most recent call last): File "<stdin>", line 1, in ? ValueError: math domain error >>> print cmath.sqrt(-1) 1j >>> z = cmath.exp(cmath.pi*1j) >>> print z.real,z.imag,abs(z) -1.0 1.22460635382e-16 1.0 Cadeias de caracteres (strings) As sequências de caracteres, tal como os números, são objectos imutáveis. Um objecto do tipo sequência de caracteres é criado delimitando a sequência por pares de aspas (simples, duplas ou triplas) e atribuindo-lhe um nome. Exemplos: s1 s2 s3 s4 = = = = ’This is a string’ "This is also a string, for sure." "Is this a string? Yes, it’s a string." """ This is a string with two lines. """ As cadeias de caracteres são sequências ou seja, são objectos indexáveis. O ı́ndice de cada caracter é um número que indica a sua posição na sequência. O primeiro caracter tem o ı́ndice 0, o segundo o ı́ndice 1, etc. Para uma cadeia com n caracteres, o último caracter tem o ı́ndice n-1. É um erro tentar aceder a um ı́ndice inexistente. >>> s =’spam’ >>> print s[0],s[2] s a >>> print s[4] Traceback (most recent call last): File "<stdin>", line 1, in ? IndexError: string index out of range O comprimento de uma sequência pode ser obtido através da função len. >>> town = ’Coimbra’ >>> print len(town) 7 Um ı́ndice negativo i de uma sequência s é interpretado como i + len(s). Assim, s[-1] é o mesmo que s[len(s)-1], ou seja representa o último elemento da sequência e s[-2] o penúltimo. >>> town = ’Coimbra’ >>> print town[-1],town[-2] a r >>> print town[-8] Traceback (most recent call last): File "<stdin>", line 1, in ? IndexError: string index out of range Duas ou mais sequências podem ser concatenadas com o operador +. >>> phrase = "This " + "is " + "spam" >>> phrase ’This is spam’ A operação n*s em que n é um inteiro, é interpretada no sentido de n adições, funcionando como operador de repetição: >>> print 3*’abc’ abcabcabc Não se pode alterar uma string, uma vez que é um objecto imutável. >>> name = ’Pythom’ >>> name[5] = ’n’ Traceback (most recent call last): File "<stdin>", line 1, in ? TypeError: object doesn’t support item assignment As sequências de caracteres possuem um conjunto rico de métodos, que são invocados com a sintaxe s.method(). Como as strings são objectos imutáveis, os métodos de sequência devolvem novas sequências, nunca alterando a sequência original! >>> s =’coimbra’ >>> print s.upper() COIMBRA >>> print s >>> coimbra >>> s = s.upper() >>> print s COIMBRA Para conhecer os métodos que possui uma string, basta no modo interactivo criar uma (qualquer) string s e digitar a instrução dir(s). Há mais de uma dúzia de métodos úteis. Dois exemplos: >>> s = ’coimbra is a nice town’ >>> s.title() ’Coimbra Is A Nice Town’ >>> s.replace(’town’,’place’) ’coimbra is a nice place’ Fatiagem de sequências Entende-se por “fatia” de uma sequência, uma subsequência da sequência. Todas as sequências em Python podem ser “fatiadas” com uma sintaxe genérica, bastante poderosa. Uma fatia da sequência S representa-se de forma genérica com a notação S[i:j:k], cujo significado é o seguinte: “Subsequência de S que se inicia com o elemento de ı́ndice i e contém os elementos cujos ı́ndices são inferiores a j, saltando de k em k elementos”. Assim, os elementos da fatia são os elementos de L de ı́ndices: i, i+k,i+2*k, ..., i<j Se em S[i:j:k] o número i faltar é substituı́do por 0; se o número j faltar, será substituı́do pelo maior número inteiro do computador. Se k faltar será substituido pelo número 1. Exemplos: >>> town = "Coimbra is a nice town" >>> town[2:8] ’imbra ’ >>> town[2:10:2] ’ibai’ >>> town[5:] ’ra is a nice town’ >>> town[:10] ’Coimbra is’ >>> town[:-3:2] ’Cibai iet’ Listas As listas são sequências heterogéneas e mutáveis de objectos. Para criar um objecto deste tipo enumeram-se os elementos da lista, separados por vı́rgulas, entre parêntesis rectos. L = [’abc’, 123, ’Python’] Uma lista vazia é representada por []. Como as listas são objectos mutáveis, podemos alterar um objecto por outro, ou remover um elemento da lista. >>> stuff = [’123’,123,1 +3j,’numbers’] >>> stuff [’123’, 123, (1+3j), ’numbers’] >>> del stuff[2] >>> stuff [’123’, 123, ’numbers’] >>> stuff[0] = 2*123 >>> stuff [246, 123, ’numbers’] Para acrescentar um objecto a uma lista, utiliza-se o método append: >>> clubs = [’Benfica’, ’Sporting’] >>> clubs.append(’Porto’) >>> clubs [’Benfica’, ’Sporting’, ’Porto’] Uma lista pode conter outras listas. Uma matriz 2 × 2 pode ser guardada numa lista de elementos em que cada elemento é uma linha da matriz, também representada numa lista: >>> M = [[1,2,3],[4,5,6],[7,8,9]] >>> print M[2][1] 7 Tal como as strings, as listas podem ser concatenadas e repetidas usando os operadores + e *: >>> L =[’a’,’ab’] >>> L = L + L >>> L [’a’, ’ab’, ’a’, ’ab’] >>> 2*L [’a’, ’ab’, ’a’, ’ab’, ’a’, ’ab’, ’a’, ’ab’] As listas podem conter referências a outros objectos. >>> x = 123 >>> L = [x,x*x] >>> L [123, 15129] >>> L = 2*L >>> L [123, 15129, 123, 15129] >>> L = L[2:] + [2*L[0]] >>> L [123, 15129, 246] >>> L.sort() >>> L [123, 246, 15129] Tuplas A grande diferença entre as tuplas e as listas é que as primeiras são objectos imutáveis. Na construção de uma tupla utilizam-se parêntesis curvos em vez de rectos: >>> t = (1,’a’,1j) >>> type(t) <type ’tuple’> >>> print t (1, ’a’, 1j) >>> type(t[2]) <type ’complex’> Existe uma notação especial para designar uma tupla de 1 único elemento: (’single’,) e não (’single’)! Tal como as outras sequências, as tuplas podem ser indexadas e concatenadas. O desempacotamento de uma tupla pode ser feito de uma forma muito simples: >>> x,y,z = (’one’,’two’,’three’) >>> print x,y,z one two three O número de nomes do lado esquerdo do sinal = têm de ser igual ao dos elementos da sequência, caso contrário ocorre um erro. O desempacotamento também funciona para strings e listas. >>> c1,c2,c3,c4 = ’Spam’ >>> print c3 a As tuplas podem conter sequências que contêm outras sequências. Apesar de serem objectos imutáveis, as tuplas, podem conter objectos mutáveis, tais como listas: >>> t = (’Python’,[’C’,’Pascal’,’Perl’]) >>> id(t) 1078912972 >>> lang = t[1] >>> lang[2] = ’Python’ >>> print t (’Python’, [’C’, ’Pascal’, ’Python’]) >>> id(t) 1078912972 Neste exemplo,a tupla t é imutável, mas porque um dos seus elementos é uma lista, e esta foi alterada pelo programa, o resultado é um pouco surpreendente... O adjectivo imutável refere-se à tupla em si, não aos objectos contidos na tupla! Embora as tuplas possam parecer redundantes face às listas, é por vezes muito útil ter a certeza que uma certa “lista” de objectos não é alterada. O empacotamento numa tupla é ideal para estes casos. Veremos adiante que as tuplas têm um papel muito importante na linguagem Python. Por exemplo, as funções podem devolver objectos múltiplos empacotados numa tupla. Dicionários Os dicionários, que pertencem à categoria dos mapas, são a estrutura de dados mais poderosa do Python. Podem ser indexadas por qualquer objecto imutável, e não apenas por números (como acontece com as sequências). Tal como as listas, os dicionários são objectos mutáveis aos quais se podem mudar, acrescentar ou eliminar elementos. Ao contrário das listas, acrescentar um novo valor a um dicionário faz-se por simples indexação do novo elemento. >>> tel = {’pedro’: 4098, ’ana’: 4139} >>> tel[’guida’] = 4127 >>> tel {’pedro’: 4139, ’ana’: 4127, ’guida’: 4098} >>> tel[’guida’] 4098 >>> del tel[’pedro’] >>> tel[’berto’] = 5991 >>> tel {’berto’: 5991, ’ana’: 4127, ’guida’: 4098} >>> tel.keys() [’berto’, ’ana’, ’guida’] >>> tel.has_key(’ana’) True >>> ’ana’ in tel True Os objectos não são guardados numa forma ordenada, tal como mostra o exemplo. Para testar se uma chave existe num dicionário usa-se o método has key (ou o teste ’key’ in ’dict’). É possı́vel obter uma lista das chaves e dos valores de um dicionário recorrendo aos métodos keys() e values(). Para ordenar qualquer destas listas podemos utilizar o método sort. >>> tel = {’berto’: 5991, ’ana’: 4127, \ ’guida’: 4098} >>> tel.keys() [’guida’, ’berto’, ’ana’] >>> tel.values() [4098, 5991, 4127] >>> keys = tel.keys() >>> keys.sort() >>> keys [’ana’, ’berto’, ’guida’] Tentar aceder a um elemento de um dicionário que não existe dá um erro. No entanto, podemos substituir a indexação pela chamado do método get(x,dft) que permite obter o valor por defeito dft quando se acede a um elemento x inexistente (em vez de um erro). >>> print d.get(’ana’,0),d.get(’fernando’,0) 4127 0 Esta função pode ser útil, por exemplo, na implementação de uma matriz esparsa (em que a maioria dos elementos é zero) utilizando um dicionário. Identificadores e palavras reservadas A atribuição de um nome a um objecto é uma das instruções mais básicas de um programa em Python. Os nomes não podem começar por números nem conter alguns caracteres com significado operatório (como *,+,−,%), mas podem ter um comprimento arbitrário. Notar que este-nome é ilegal mas este nome não. Também não podem coincidir com uma das palavras reservadas da linguagem: and assert break close continue def del elif else except exec finally for from global if import in is lambda not or pass print raise return try else Outras palavras que convém não utilizar como indentificadores são as que correspondem a constantes pré-definidas como True, False e None ou funções intrı́nsecas como float. Como regra, nomes curtos mas sugestivos são a melhor escolha para um identificador. Exemplo: x = 0.034 é uma má escolha de um identificador, mas inflation = 0.034 # inflation rate in portugal é uma boa escolha (notar a curta explicação, quando o nome é definido, a seguir ao sı́mbolo de comentário #), melhor do que a alternativa desnecessariamente verbosa, embora sintacticamente legal: portuguese_inflation_rate = 0.034 Estruturas de controle O fluxo de um progama (sequência e encadeamento das instruções) é ditado pelas estruturas de controle, que em Python são as seguintes: • if ... elif ...else • while...else • for...else • try...except...finally • raise • break • continue Comparado com outras linguagens de programação, o número de estruturas de controle é reduzido, o que facilita a aprendizagem. As estruturas de controle foram desenhadas para serem poderosas e de uso genérico. Por exemplo, a instrução for ... else permite percorrer qualquer objecto iterável e não se limita a arrays (listas homogéneas de objectos ocupando uma porção contı́gua da memória), como em C ou Pascal e na maioria das outras linguagens de programação. Selecção: if...elif...else if <test>: <statements> [elif <test>: <statements>] [else: <statements>] Exemplo: if choice == ’eggs’: print ’Eggs for lunch.’ elif choice == ’ham’ print ’Ham for lunch’ elif choice == ’spam’ print ’Hum, spam for lunch’ else: print "Sorry, unavailable choice." Nota importante: O teste de igualdade entre x e y é x == y e não x = y! Verdadeiro e falso A interpretação do valor lógico “verdadeiro” (True) ou “falso” (False) de um objecto em Python segue as seguintes regras: • Um objecto é considerado verdadeiro se for diferente de zero, caso seja um número, ou se o objecto não estiver ”vazio”. • Um objecto é considerado falso se não for verdadeiro, ou seja nos seguintes casos: número zero, objecto vazio ou o objecto None. O valor lógico de um objecto é calculado pela função bool; o valor devolvido é True ou False, os dois únicos objectos do tipo bool(eano). Regras para expressões: • As comparações (== != > < >= <=) devolvem o valor True ou False consoante sejam falsas ou verdadeiras. • not a é False se a for verdadeiro e True se a for falso. • x or y devolve o primeiro dos objectos que for verdadeiro; se nenhum for verdadeiro, devolve o último objecto. • x and y devolve o primeiro dos objectos que for falso; se nenhum for falso, devolve o último objecto. Os valores obtidos de acordo com estas regras são os esperados da lógica de Boole. Ciclos de repetição: while...else while <test>: <statements> [if <test>: break] [if <test>: continue] <statements> [else: <statements>] Nota: As cláusulas break, continue e else são opcionais. Se o teste da cláusula break for verdadeiro, o programa salta para o fim do bloco while. Se o teste da cláusula continue for verdadeiro, o programa abandona o ciclo actual e continua no próximo ciclo. Se existir a cláusula else e se a instrução break não for accionada, o programa executa as instruções que se seguem a else no final dos ciclos. Exemplo 1: a = 0; b = 10 while a < b: print a, a += 1 # a = a+1 Resultado: >>> 0 1 2 3 4 5 6 7 8 9 Exemplo 2: x = 10 while x: x -= 1 # x = x-1 if x % 2 = 0: Resultado: >>> 8 6 4 2 0 Exemplo 3: y = input(’Give an integer number: ’) x = y/2 while x > 1: if y % x == 0: print y,’is not prime.’ break x -= 1 else: print y,"is prime!" Resultado: >>> Give an integer number: >>> 5 is prime! Exemplo 4: name = ’Spam’ while name: print name, name = name[1:] Resultado: >>> Spam pam am m 5 Exemplo 5: # Guess a number game mynumber = ’123456’ while 1: n = input(’Guess the number: ’) if n == mynumber: print ’You guessed the number!’ break; else: print ’Sorry, wrong guess.’ print ’Game is over’ Resultado: Guess the number: 43465 Sorry, wrong guess. Guess the number: 7161527 Sorry, wrong guess. Guess the number: 999999 Sorry, wrong guess. Guess the number: 123456 You guessed the number! Game is over. Ciclos de iteração: instrução for ...else Em Python a instrução for é utilizada (exclusivamente) para iterar sobre objectos. Na versão actual do Python todas as sequências, os dicionários e os ficheiros têm um iterador definido por defeito, que devolve cada um dos seus elementos em sequência, mas é possı́vel definir iteradores para outros objectos. Sintaxe: for x in object: <instruções> [if condição: break>] [else: <instruções>] Se existir a cláusula opcional else, a instruções que se lhe seguem serão executadas apenas no caso de o ciclo atingir o último elemento do objecto sem a condição break ser accionada. Exemplos: basket = [’orange’,’banana’,’apple’] for fruit in basket: print fruit phrase = ’Coimbra is a nice town.’ for c in phrase: print c,ord(c) # ord(c) = ASCII code Ciclos for: continuação Exemplo de utilização da cláusula else. Suponhamos que queremos testar se existe algum número negativo na lista L. Podemos fazê-lo com o seguinte código: for x in L: if x < 0: print ‘‘There are negative numbers’’ break else: print ‘‘All numbers are non-negative’’ O Python possui a função intrı́nseca range que é muito útil para criar listas de inteiros que são frequentemente usadas para iterar ciclos for: range(n) -> [0,1,2, ...n-1] range(i,j) -> [i,i+1,...j-1] range(i,j,k) -> [i,i+k, i+2k, ...] Exemplos: range(5) = [0,1,2,3,4] range(2,5) = [2,3,4] range(1,10,2) = [1,3,5,7,9] range(0,-10,-3) = [0,-3,-6,-9] Para ciclos que exijam listas muito extensas é preferı́vel utilizar a função xrange, semelhante à anterior, mas que permite economizar memória, sendo a lista criada à medida que for necessário. Para iterar no modo clássico, equivalente ao do Pascal ou C, utilizando um ı́ndice inteiro que percorre a sequência, utiliza-se a construção >> for i in range(len(lista)): >> print lista[i] que é totalmente equivalente à forma mais pitónica: >> for x in lista: >> print x Advertência importante sobre a utilização de ciclos for: a lista iteradora não deve ser alterada no interior do ciclo sob pena de se obterem resultados errados! Exemplo (errado!): for x in lista: if x < 0: lista.remove(x) Para o efeito devemos iterar não sobre a lista mas sobre uma cópia (ou clone) da lista: for x in lista[:]: if x < 0: lista.remove(x) A operação lista[:] cria um cópia da lista sobre a qual se efectua a iteração, podendo agora a lista original ser manipulada, sem problemas, no interior do ciclo. Por vezes é útil, ao iterar sobre uma sequência, aceder ao elemento e simultaneamente ao seu ı́ndice. Nestes casos, em vez de utilizar um código do estilo: for i in range(len(L)): print i,L[i] é mais prático e eficiente utilizar a função intrı́nseca enumerate: for i,x in enumerate(L): print i,x A função enumerate está definida não apenas para sequências mas para todos os objectos iteráveis: dicionários, ficheiros, etc. Por exemplo, este programa imprime todas as linhas de um ficheiro anotadas com o número da linha: f = open("myfile.txt",’r’) for i,line in enumerate(f): print i,">",line Para iterar simultâneamente sobre várias sequências existe uma outra função muito útil: zip. Exemplo: colors = ("red","green","blue") clubs = ("Benfica","Sporting","Porto") for club,color in zip(clubs,colors): print club,color Funções As funções em Python são de certa forma semelhantes às funções da linguagem C e às funções/procedimentos do Pascal. Sintaxe: def nome([arg1,arg2,...,argn]): <statements> return [value1,value2,...valuen] Notas: • a instrução def cria um objecto do tipo função e atribui-lhe um nome • return devolve o(s) resultado(s) para a instrução que chamou a função • para chamar uma função, depois de criado este objecto, basta invocá-la pelo seu nome. A passagem de argumentos é feita por referência, ou seja por atribuição de um nome no espaço de nomes local da função. Isto significa que objectos imutáveis não podem ser alterados dentro de uma função (nem fora dela), mas os objectos mutáveis (ex. listas, dicionários) sim. Utilidade das funções: • Reutilização do código • Decomposição de uma tarefa complexa numa série de tarefas mais simples (funções) • Facilita a leitura e futuras modificações do programa Exemplo 1: >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> def produto(x,y): return x*y print produto(2,3) 6 z =2 y = produto(9,z) print y 18 frase = ’aa’ z = produto(frase,2) print z ’aaaa’ Exemplo 2: >>> def intersect(s1,s2): >>> res = [] >>> for x in seq1: >>> if x in seq2: >>> res.append(x) >>> return res >>> >>> intersect([1,2,3],[1,7,2]) >>> [1,2] Valores por defeito de argumentos É por vezes útil atribuir valores por defeitos a um ou mais argumentos de uma função, o que permite omiti-los na chamada da função. Exemplo: >>> def add_tax(x,iva=19): ... """Adds the IVA tax (given as percent) ... to value x. ... """ ... return x*(1+iva/100.) ... >>> print add_tax(100) 119.0 >>> print add_tax(100,5) 105.0 >>> print add_tax(100,iva=7) 107.0 Quando omitido, o argumento iva toma o valor por defeito (19%). O exemplo ilustra ainda a possibilidade de chamar explicitamente por nome um argumento da função, o que ajuda, por vezes, à legibilidade do programa. Chama-se a atenção para o seguinte: na chamada de uma função, a seguir a um argumento dado explicitamente por nome, todos os outros argumentos que lhe seguem também terão que ser dados da mesma forma, pelo que a seguinte chamada da função é ilegal: >>> print add_tax(buy=200,5) SyntaxError: non-keyword arg after keyword arg Notar a string de documentação a seguir à instrução def. Valores por defeito de argumentos É ainda notar que a resolução dos nomes dados por defeito na definição de uma função sé é avaliada uma vez, precisamente quando a função é definida, e não quando a função é chamada; este detalhe pode dar origem a erros subtis! >>> taxa = 19 >>> def add_tax(x,iva=taxa): ... return x*(1+iva/100.) ... >>> print add_tax(100,taxa) 119.0 >>> taxa = 20 >>> print add_tax(100) 119.0 Um outro resultado inesperado: >>> def save(x,L=[]): ... L.append(x) ... print L >>> save(1) [1] >>> save(2) [1, 2] >>> L =[’a’,’b’] >>> save(3,L) [’a’, ’b’, 3] >>> save(3) [1, 2, 3] Resolução de nomes: regra de alcance A resolução dos nomes no interior de uma função segue a seguinte regra: em primeiro lugar é procurado o nome no espaço dos nomes local da função, isto é os nomes que foram atribuı́dos a objectos no interior da função através de uma instrução de atribuição, ou usando as instruções def ou class. Se o nome não foi encontrado, pesquisa-se de seguida o espaço dos nomes global, isto é, os nomes definidos no módulo principal. Caso o nome também não exista neste espaço, passa-se a pesquisar no espaço dos nomes pré-definidos (built-in). Se o nome não for encontrado a este nı́vel é levantada uma excepção “NameError: name is not defined.” Este regra simples pode ser relembrada com a mnemónica LGB (Local, Global, Built-in). A instrução de atribuição no interior de uma função cria um nome no espaço dos nomes local, pelo que efectuar uma atribuição a uma variável do módulo principal no interior de uma função não tem o efeito esperado! Quando se pretende “alterar” um nome do espaço global é necessário declará-lo como global no interior da função da seguinte forma: >>> >>> >>> >>> >>> >>> >>> >>> z = 22 def addanumber(x): global z z = z + x addanumber(1) print z 23 Deve-se evitar, sempre que possı́vel a alteração de variáveis globais no interior de funções. De uma forma mais elegante, o programa anterior seria escrito assim: >>> >>> >>> >>> >>> >>> >>> z = 22 def addanumber(x): return z + x z = addanumber(1) print z 23 Agora a alteração do valor da variável z é delegado para o programa principal, de forma explı́cita. Trata-se de uma função segura, sem efeitos laterais. Não é necessário utilizar a declaração global para podermos aceder ao valor de z no interior da função. Seguindo a regra LGB o nome é considerado como global. Uma vez que não lhe é atribuı́do um valor no interior da função, z não existe no espaço de nomes local da função! Por outro lado, a atribuição de um nome a um objecto no interior de uma função transforma o nome em local, a menos que ele seja explicitamente declarado global. Isto é assim mesmo que o nome exista já no espaço global (nomes idênticos podem existir em espaços diferentes, mas não têm qualquer relação entre si). Vejamos o que acontece na primeira versão do programa se omitirmos a declaração global. . . >>> >>> >>> >>> >>> >>> z = 22 def addanumber(x): z = z + x addanumber(1) UnboundLocalError: local variable ‘z’ referenced before assignement. A instrução de erro é bem explı́cita. Seguindo a regra LGB, z é uma variável local e não tem nada a ver com a outra variável homónima definida no programa principal. . . Nota: A resolução dos nomes locais é feita estaticamente, quando da “pré-compilação” do código e não quando o programa corre. Isto explica a seguinte situação, que costuma surpreender os “novatos”: >>> >>> >>> >>> >>> >>> x = 99 def prt(): print ‘‘The value of x is’’,x prt() The value of x is 99 O programa funciona. Mas se lhe acrescentarmos uma linha, deixa de funcionar. . . >>> >>> >>> >>> >>> >>> x = 99 def prt(): print ‘‘The value of x is’’,x x = 88 prt() UnboundLocalError: local variable ‘x’ Devolução de valores múltiplos É perfeitamente legı́timo em Python (ao contrário do que acontece em outras linguagens como o C) que uma função devolva mais de um objecto. Os objectos que se pretende devolver enumeram-se na instrução return e são devolvidos empacotados numa tupla. >>> def multiples(x) >>> return x,2*x,3*x,4*x >>> >>> multiples(3) (3,6,9,12) Para desempacotar os objectos devolvidos utiliza-se a seguinte forma: >>> x,y,z,t = multiples(3) >>> print x,y,z,t >>> 3 6 9 12 A devolução de valores múltiplos é muito útil. Por exemplo, façamos uma função para trocar os valores de duas variáveis. Uma versão naı́ve não funciona! >>> >>> >>> >>> >>> >>> >>> >>> >>> def swap(x,y) temp = x x = y y = temp x = 2; y = 3 swap(x,y) print x,y 2 3 Consegue explicar porque é que este swap não funciona? Felizmente, o problema tem uma solução simples: >>> >>> >>> >>> >>> >>> >>> def swap(x,y) return y,x x = 2; y = 3 x,y = swap(x,y) print x,y 3 2 Na realidade, embora seja um bom exemplo da devolução de valores múltiplos, a função swap é inútil porque em Python a troca de duas variáveis pode fazer-se muito simplesmente numa única linha! >>> >>> >>> >>> >>> >>> x = 2; y = 3 print x,y 2 3 x,y = y,x print x,y 3 2 Nota: em Python todas as funções devolvem pelo menos um valor. No caso de não existir return ou quando se evoca return sem referência a um objecto, o objecto devolvido pela função é None. Normalmente, este objecto é simplesmente ignorado quando se chama a função. Recursão Em Python, tal como na maioria das linguagens de programação modernas, as funções podem ser definidas de forma recursiva (i.e., que incluem na própria definição referências à própria função. Por exemplo, a seguinte função calcula n! de forma recursiva. >>> def fact(n): ... if n == 1: ... return 1 ... else: ... return n*fact(n-1) ... >>> fact(3) 6 A sequência de passos que o programa efectua quando executa esta função é o seguinte: fact(3) -> fact(2) -> fact(1) -> volta para fact(2) <fact(3) <- 3*fact(2) (reserva memória no stack) 2*fact(1) (reserva memória no stack) 1 trás: 2*1 3*2*1 = 6 A definição de funções de forma recursiva é expressiva, mas a sua execução (em termos de tempo de cálculo e, nalguns casos, de gasto de memória intermédia stack pode ser dispendiosa. Há contudo algoritmos que podem ser expressos de forma muita simples recursivamente e que, expressos de outra forma são muito complicados e nem sempre o tempo de execução é desfavorável. Processamento genérico de argumentos É por vezes desejável escrever funções que possam aceitar um número variável de argumentos. Para o efeito, os sı́mbolos *args e **keyw utilizados como argumentos de uma função (na sua definição ou chamada) significam um número arbitrário de argumentos posicionais (*args) e de argumentos com valor por defeito (**keyw). No primeiro caso args é uma lista que contém os argumentos posicionais e no segundo keyw um dicionário contendo os argumentos e os valores por defeito. >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> def union(*args): res = [] for seq in args: for x in seq: if not x in res: res.append(x) return res a = [1,2,3]; b =[2,7,8]; c =[0,3] print union(a,b,c) [1, 2, 3, 7, 8, 0] L = [a,b,c] print union(*L) [1, 2, 3, 7, 8, 0] Programação funcional O que é a programação funcional? A programação funcional é um estilo de programação que dá uma ênfase especial à utilização de funções. Numa linguagem de programação puramente funcional, qualquer instrução é uma função pura e um programa é uma sequência de funções puras. Uma função pura é uma função que não altera o valor dos seus argumentos. Nota: em Python as funções não são necessáriamente puras uma vez que um objecto mutável passado como argumento de uma função pode ser alterado no interior da função. • Função pura: def square(x): return x*x • Função impura: def sort(L): L.sort() Tal como nas linguagens de programação funcional, em Python as funções são objectos de primeira categoria, o que significa que podem ser passados como argumentos de funções e serem devolvidos por funções. Também, como qualquer outro tipo de objecto, as funções podem ser armazenadas em tuplas, listas ou dicionários. Exemplo: import math ftrig = (math.sin,math.cos,math.tan) for f in ftrig: print f(math.pi) Linguagens de programação funcional O LISP = LISt Processor, que é uma das linguagens de programação mais antigas é uma linguagem de programação funcional. Um programa em LISP é um conjunto de funções que operam sobre listas. O LISP, ou algum dos seus derivados, são bastante usados em alguns domı́nios especı́ficos da computação, por ex. em Inteligência Artificial. Linguagens funcionais descendentes do LISP: • Haskell • Scheme • Erlang (desenvolvido pela Erikson, telemóveis) Grande parte da teoria da computação está descrita no paradigma da programação funcional. Existem mais artigos teóricos sobre computação formulados em LISP do que em C ou em qualquer outra linguagem! Vantagens da programação funcional: • Elegância formal • Segurança (elimina os “efeitos laterais”) Desvantagens: ineficiência (em termos de rapidez de computação), hermiticidade. Ferramentas de programação funcional A linguagem de programação Python possui algumas ferramentas de programacão funcional, correspondentes a cinco funções pré-definidas: • filter • map • reduce • lambda • apply Utilizando estas ferramentas e algumas das caracterı́sticas intrı́nsecas do Python, em particular a avaliação em curto-circuito de expressões lógicas, é possı́vel escrever programas no estilo funcional. Mesmo quando esse não é o objectivo, as ferramentas de programação funcional do Python revelam-se muito úteis em vários contextos. Programação funcional: filter A função pré-definida filter tem a seguinte sintaxe: filter(f, L) onde f é uma função e L uma sequência (tupla, lista ou cadeia de caracteres). A função filter devolve um valor que é uma sequência que contem todos os elementos de L para os quais o resultado da aplicação da função f tem o valor lógico verdadeiro. No caso de f ser None, filter devolve uma lista com todos os elementos de L que têm o valor lógico verdadeiro. Exemplo: >>> >>> >>> >>> >>> numbers = range(8) def even(x): return x % 2 == 0 print filter(even,numbers) [0, 2, 4, 6] Exemplo: >> >> >> >> >> >> bag = [‘a’,2,0,[],’’] newbag = filter(None,bag) print bag [‘a’, 2, 0, [], ’’] print newbag [‘a’, 2] Programação funcional: map A função pré-definida map tem a seguinte sintaxe: map(f, L1, L2,...) onde f é uma função e L1, L2, ... são uma ou mais sequências. O resultado desta função é uma lista contendo os objectos resultantes da aplicação de f à (às) lista(listas). Se existirem n listas, f deverá ser uma função de n argumentos. Se f for None, a lista devolvida é uma lista dos elementos de L1, ou no caso de haver mais do que uma lista, devolve uma lista de tuplas, cada tupla contendo um elemento de cada sequência. No caso de as listas não terem todas o mesmo número de elementos, os elementos em falta nas listas mais pequenas são substituı́dos por None. Exemplo: >>> city = ‘Coimbra’ >>> print map(None,city) >>> [‘C’, ‘o’, ‘i’, ‘m’,’b’,’r’,’a’] Exemplo: >> >> >> >> >> >> x = [1,2,3] y = [4,5,6] def mul(x,y): return x*y z = map(mul,x,y) print z [4,10,18] Programação funcional: reduce A função pré-definida reduce tem a seguinte sintaxe: reduce(f, L, <default>) Esta instrução devolve um valor que é o resultado da aplicação de uma função binária a uma sequência L, de acordo com o esquema seguinte: f(L[0],L[1]) -> x; f(x,L[2]) -> x; ...f(x,L[-1]) -> retorno Exemplo: >> def add(x,y): >> return x+y >> print reduce(add,range(0,5)) >> 10 # 0+1+2+3+4 Nota: pode ser passado a reduce um terceiro parâmetro, opcional, que é o valor devolvido por defeito no caso de a sequência se encontrar vazia. Exemplo: >> >> >> >> >> >> print reduce(add,range(0,5)) 10 print reduce(add,[]) Type error: reduce of empty sequence. print reduce(add,[],0) 0 Funções anónimas: expressões lambda Na realidade, lambda tem o valor sintáctico de uma expressão que devolve um objecto do tipo função à qual pode ser atribuı́do (opcionalmente) un nome. Muitas vezes o objecto devolvido pela expressão lambda permanece anónimo. Exemplo: Calcular a soma dos números de uma lista (L): def add(x,y): return x+y print reduce(add, L) ou, utilizando a expressão lambda, add = lambda x,y: x+y print reduce(add, L) ou ainda (numa só linha!) print reduce(lambda x,y: x+y, L) Note-se que não é permitido escrever (porquê?): print reduce(def add(x,y): return x+y: x+y, L) Sintacticamente uma expressão lambda tem a forma: lambda args: expr onde args são os argumentos da função (separados por vı́rgulas) e expr uma expressão que define a função. Vantagens e desvantagens do uso de lambda: • Pode ser utilizada em situações onde sintacticamente é exigida uma expressão (+) • Uma função anónima é usada onde é preciso e de seguida descartada, não ficando a poluir o espaço dos nomes (+) • Está restringida a funções simples (é uma expressão!), pelo que não se podem incluir na definição da função estruturas de controle (if, for, while . . . ) (−). Na maior parte dos casos a utlização de lambda em vez de def é uma pura questão de estilo. A utilização de lambda permite escrever programas mais compactos, mas também se pode argumentar que ficam menos legı́veis! É capaz de descobrir o que fazem as funções criadas pelas seguintes expressões lambda? lambda a,b: not b and a or gcd(b, a % b) lambda L: reduce(lambda a,b: gcd(a,b), L) lambda n: map(lambda x,f=lambda x,f:(x<=1)\ or (f(x-1,f)+f(x-2,f)):f(x,f),range(n)) lambda x,y: (x < y and [x] or [y])[0] Abrangências de listas Existe uma forma simples de criar listas sem recorrer sem recorrer às ferramentas de programação funcional e que pode emular algumas das funções de map, filter e/ou lambda. Trata-se das abrangências de listas, que são listas criadas por expressões contendo uma instrução for, eventualmente seguida de uma ou mais instruções for ou if. Exemplos: >>> names = [’ana matos’,’pedro santos’] >>> [name.title() for name in names] [’Ana Matos’, ’Pedro Santos’] >>> vec = [2, 4, 6] >>> [3*x for x in vec] [6, 12, 18] >>> [3*x for x in vec if x > 3] [12, 18] >>> [3*x for x in vec if x < 2] [] >>> [[x,x**2] for x in vec] [[2, 4], [4, 16], [6, 36]] >>> [x, x**2 for x in vec] # error:() required File "<stdin>", line 1, in ? [x, x**2 for x in vec] ˆ SyntaxError: invalid syntax >>> [(x, x**2) for x in vec] [(2, 4), (4, 16), (6, 36)] Abrangências de listas (cont.) Mais alguns exemplos: >>> vec1 = [2, 4, 6] >>> vec2 = [4, 3, -9] >>> [x*y for x in vec1 for y in vec2] [8, 6, -18, 16, 12, -36, 24, 18, -54] >>> [x+y for x in vec1 for y in vec2] [6, 5, -7, 8, 7, -5, 10, 9, -3] >>> [vec1[i]*vec2[i] for i in range(len(vec1))] [8, 12, -54] >>> n = 123456 >>> [int(i) for i in str(n)] [1, 2, 3, 4, 5, 6] >>> sum([int(i) for i in str(n)]) 21 >>> hexa = [’AF’,’12CF’] >>> [int(n,16) for n in hexa] [175, 4815] >>> message = ’Hello, world. What a nice day!’ >>> [word.strip(’,.?!’) \ for word in message.split()] [’Hello’, ’world’, ’What’, ’a’, ’nice’, ’day’] Abrangências de listas vs. programação funcional Suponhamos que pretendı́amos criar uma lista com o quadrado de todos os números inteiros pares inferiores a 10. Utilizando abrangências, >L = [x**2 for x in range(10) if x % 2 == 0] Utilizando as ferramentas de programação funcional, >L = map(lambda x, x*x,\ > filter(lambda x: x % 2 == 0,range(10)) Qual das duas instruções é mais clara? Sem dúvida, a abrangência é mais legı́vel! No entanto, os dois pedaços de código não são equivalentes, embora produzam correctamente a mesma lista de números: [0, 4, 16, 36, 64]. Exemplo: >x = 123 >L = [x**2 for x in range(10) if x % 2 == 0] >print L,x >[0, 4, 16, 36, 64] 9 >x = 123 >L = map(lambda x: x*x,\ > filter(lambda x: x % 2 == 0,range(10)) >print L,x >[0, 4, 16, 36, 64] 123 O valor de x foi alterado no primeiro caso, como efeito lateral do cálculo da abrangência o que pode ser perigoso, se não estivermos a contar com este comportamento! As ferramentas de programação funcional não têm efeitos laterais, pelo que a variável x é apenas e efectivamente uma variável auxiliar, cuja existência não colide com a de outra variável com o mesmo nome no exterior da função. Na tentativa de eliminar os efeitos laterais, as linguagens funcionais puras traduzem as estruturas de controle habituais (if, for, while) por funções. Vejamos um exemplo em Python. O pedaço de código seguinte if x < 0: print "Negative number" else: print math.sqrt(x) print x(2),x(-1) tem a seguinte transcrição funcional: sqroot = lambda x: x < 0 and or math.sqrt(x) print sqroot(2),sqroot(-1) "Negative number"\ Consegue perceber como é que a função sqroot funciona emulando o bloco if? Módulos Cada ficheiro de um programa python é um módulo com um espaço de nomes próprio. Este nomes podem ser importados para o espaço dos nomes de outro programa através da instrução import que tem duas variantes sintáticas: import module e from module import obj No primeiro caso todos os objectos definidos no módulo ficam disponı́veis por qualificação do nome do módulo; no segundo, o objecto importado fica disponı́vel através da simples invocação do seu nome. Exemplo: import math print math.cos(math.pi) ou from math import cos print cos(pi) Para importar todos os objectos definidos num módulo podemos escrever from module import *. Existe ainda a possibilidade de importar um objecto definido num módulo criando um nome alternativo no espaço dos nomes local: from module import name1 as name2 Exemplo: from math import cos as coseno from math import pi print coseno(pi) Esta opção é útil quando a função a importar tem o mesmo nome de uma outra já definida no programa importador. Para evitar poluir o espaço dos nomes, a forma from module import * deve ser evitada. Cada módulo tem um nome, acessı́vel através do identificador reservado name . O módulo do programa principal tem o nome especial main . É vulgar encontrar a construção: if __name__ == "__main__": instructions .... As instruções do bloco if só são processadas quando o ficheiro é executado como programa principal; quando é importado como um módulo são ignoradas, permanecendo invisı́veis ao programa importador. Vantagens da utilização de módulos: • Reutilização de código • Partição do espaço dos nomes • Partilha de funções e estruturas de dados entre vários componentes de um programa A caracterı́stica modular do Python é particularmente apreciada em grandes projectos de software, tendo sido inspirada na linguagem de programação MODULA, um sucessor do PASCAL. Ferramentas de introspecção: metaprogramação Os objectos definidos num módulo podem ser listados com a instrução dir(module), tal como os atributos de qualquer objecto podem ser listados com dir(obj). O tipo e endereço de um objecto na mémória são dados pelas funções type(obj) e id(obj). O Python possui várias outras ferramentes de introspecção, mas uma das mais importantes é o atributo doc de cada objecto, que é suposto conter uma sequência de caracteres com uma descrição sumária do objecto – a string de documentação, ou “doc string”. Os módulos, funções e classes definidas pelo utilizador devem também ser documentadas com uma “doc string”. As strings de documentação são colocadas no inı́cio dos módulos e imediatamente a seguir à instrução de definição das funções e classes. Exemplo: def gcd(a,b): """ Greatest common divider between a and b. Uses Euclide’s algorithm. """ while b: a,b = b, a % b return a print gcd.__doc__ Greatest common divider between a and b. Uses Euclide’s algorithm. Programação por objectos Este capı́tulo está a ser reescrito e será disponibilizado em breve! Iteradores As sequências, os dicionários e os ficheiros são objectos iteráveis, aos quais é possı́vel aceder, elemento a elemento, utilizando uma sintaxe comum: for x in obj: print x A instrução for x in obj chama a função iter(obj) que cria um iterador para o objecto obj. Cada um dos elementos é obtido chamando it.next() onde it é o iterador do objecto. Quando se esgotam os elementos do objecto é levantada uma excepção que indica ao ciclo for o fim da iteração. >>> it = iter(’abc’) >>> it <iterator object at 0x00A1DB50> >>> it.next() ’a’ >>> it.next() ’b’ >>> it.next() ’c’ >>> it.next() Traceback (most recent call last): File "<pyshell#6>", line 1, in -toplevelit.next() StopIteration Iteradores É muito fácil tornar uma classe de objectos iterável. Basta para isso que a classe defina o método iter que deverá devolver um objecto (o iterador) que por sua vez deverá definir o método next. Se next for definido na própria classe, basta ao método iter devolver a instância (self ). Exemplo: >>> class Reverse: "Iterator for looping a sequence backwards" def __init__(self, data): self.data = data self.index = len(data) def __iter__(self): return self def next(self): if self.index == 0: raise StopIteration self.index = self.index - 1 return self.data[self.index] >>> for char in Reverse(’spam’): print char m a p s Geradores Um gerador é qualquer função que contenha a palavra chave yield, utilizada para devolver valores à semelhança de return. Estas funções especiais implementam automaticamente o método next(). Quando se chama next() a função retoma a execução na instrução que se segue a yield (o gerador lembra-se exactamente do ponto onde saiu e os valores de todas as variáveis locais). >>> def reverse(data): for index in range(len(data)-1, -1, -1): yield data[index] >>> g = reverse((1,2,3)) >>> g.next() >>> 3 >>> g.next() >>> 2 >>> g.next() >>> 1 >>> g.next() Traceback (most recent call last): File "<stdin>", line 1, in ?: StopIteration >>> for char in reverse(’golf’): print char f l o g • Na maioria dos casos os geradores são a forma mais simples de implementar um iterador. • Os geradores guardam memória das variáveis locais e do estado de execução entre as chamadas da função. Esta caracterı́stica fundamental, podendo também ser útil noutras situações, simplifica muito a escrita de um iterador! • Um gerador invoca automaticamente a excepção StopIteration que assinala o fim da iteração. • O conceito de gerador é central na definição de alguma linguagens de programação como o “Icon”. Iteradores e geradores são ferramentas poderosas do Python inspiradas nesta linguagem. • Geradores recursivos são muitas vezes utilizados para a resolução de problemas de Inteligência Artificial. Alguns problemas de enunciado simples, mas que desafiam resolução analı́tica, podem ser resolvidos por computador usando algoritmos simples, mas eficazes, baseados nestas ferramentas. Alguns exemplos clássicos (problemas de xadrêz): – Problema das oito raı́nhas – Problema da volta do cavalo Excepções Os erros de sintaxe são normalmente detectados pelo compilador na fase de conversão do programa para bytecode ou na fase de depuração do programa. Contudo, existe um outro tipo de erros que podem manifestar-se na execução do programa mesmo quando este é sintacticamente correcto. A este tipo de erros damos o nome de excepções. Alguns exemplos de acções que quando intentadas no decorrer de um programa dão origem a uma excepção: • Dividir um número por zero. • Aceder a um elemento inexistente de uma lista. • Abir um ficheiro inexistente. • Converter num número uma string que não representa um número. • Iterar um objecto não iterável • ... O interpretador de Python detecta a ocorrência de excepções, dando ao programa a oportunidade de corrigir o erro. Assim, podemos escrever programas robustos que não ”estouram”mesmo quando ocorrem situações...excepcionais. Excepções As excepções são objectos do tipo classe. Quando ocorre uma excepção, é instanciada a classe que define a excepção. Esta operação é detectada pelo interpretador, havendo a possibilidade de processar a excepção com a instrução try...except. Se o programa não tratar a excepção, o interpretador abortará o programa com uma mensagem de erro. Sintaxe da instrução try...except: try: .... except <ExceptionClass>: .... <else>: .... <finally>: .... As cláusulas else e finally são opcionais. O bloco de código indentado por else é executado se e só se não ocorrer a excepção. O bloco de código indentado por finally é sempre executado, quer tenha ocorrido, ou não, uma excepção. O nome da excepção que se pretende “apanhar” e processar também é opcional. Se não for indicado, todas as excepções serão apanhadas. A cláusula except pode ainda tomar as seguintes formas: except (ExceptionClass1, ExceptionClass2,..) except ExceptionClass, instance No primeiro caso (notar os parêntesis obrigatórios) serão apanhadas todas as excepções enumeradas na tupla. No segundo caso, é passada uma instância da classe que pode conter informação adicional sobre a excepção (ver adiante). Nalguns casos, é útil que o próprio código levante explicitamente excepções, utilizando a instrução raise ExceptionClass. Vejamos um exemplo do uso da excepção pré-definida IndexError que é accionada quando se tenta aceder a um elemento de uma sequência com um ı́ndice inexistente: def getn(L,n): print L[n] L = [’a’,’b’,’c’] while 1: try: n = input(’Item? ’) getn(L,n) except IndexError: print "Bad index. Try again." else: break Na página seguinte dá-se a lista de todas as excepções pré-definidas e a sua organização hierárquica. O utilizador pode também definir novas excepções e levantar excepções. Hierarquia das Excepções Exception +-- SystemExit +-- StopIteration +-- StandardError | +-- KeyboardInterrupt | +-- ImportError | +-- EnvironmentError | | +-- IOError | | +-- OSError | | +-- WindowsError | +-- EOFError | +-- RuntimeError | | +-- NotImplementedError | +-- NameError | | +-- UnboundLocalError | +-- AttributeError | +-- SyntaxError | | +-- IndentationError | | +-- TabError | +-- TypeError | +-- AssertionError | +-- LookupError | | +-- IndexError | | +-- KeyError | +-- ArithmeticError | | +-- OverflowError | | +-- ZeroDivisionError | | +-- FloatingPointError | +-- ValueError | | +-- UnicodeError | | +-- UnicodeEncodeError | | +-- UnicodeDecodeError | | +-- UnicodeTranslateError | +-- ReferenceError | +-- SystemError | +-- MemoryError +---Warning +-- UserWarning +-- DeprecationWarning +-- PendingDeprecationWarning +-- SyntaxWarning +-- OverflowWarning +-- RuntimeWarning +-- FutureWarning Verifica-se que IndexError é uma subclasse de LookupError que por sua vez é uma subclasse de StandardError que deriva da classe mãe Exception. De acordo com a hierarquia das excepções, a instrução: try: .... except LookupError ... irá “apanhar” os erros de indexação (de sequências) e os erros de chave inexistente num dicionário, ao passo que try: .... except IndexError .... “apanhará” exclusivamente os erros de indexação. As excepções não servem apenas para assinalar e processar erros. Por vezes, são utilizadas para notificar eventos ou para assinalar situações excepcionais que exijam uma atenção “especial” e acção imediata. Exemplo: def search(word,file): for line in file: if word in line: raise ItemFound try: search(’pyhton’,myfile) except ItemFound: print "Item found." Excepções definidas pelo programador É muito fácil definir novas excepções. As excepções são classes que herdam da classe mãe Exception. Assim, para criar a nova excepção MyException basta criar esta nova classe e invocá-la com a instrução raise: class MyException(Exception): pass try: .... except: raise MyException É possı́vel criar excepções mais sofisticadas. Vamos dar como exemplo a criação de uma excepção NonPositiveNumber como “guardião” do cálculo de raı́zes quadradas. O próprio número faltoso e uma mensagem de erro são guardadas nas instâncias da classe (cada vez que é criada uma excepção a classe é instanciada). class NonPositiveNumber(Exception): def __init__(self,x): self.x = x self.mesg = "is negative!" def my_sqrt(x): from math import sqrt if x <0 : raise NonPositiveNumber(x) return sqrt(x) while 1: try: x = input("Number? ") print my_sqrt(x) except NonPositiveNumber, error: print "Bad value: ", error.x, error.mesg except EOFError: break Neste exemplo, error é a instância da classe NonPositiveNumber criada quando é levantada a excepção. O nome escolhido para a instância é arbitrário. Para aceder a essa instância basta utilizar a sintaxe: except ExceptionClass, instance em lugar da forma mais simples e vulgar: except ExceptionClass A excepção EOFError é utilizada para detectar o fim do input de um ficheiro. Quando os dados estão a ser inseridos pelo teclado, o fim dos input é habitualmente assinalado premindo a combinação de teclas (CTRL+d) que levanta a referida excepção. Depuração de código Uma maneira simples de efectuar testes para potenciais situaç ões de erro no interior de um programa consiste na utlização da instrução assert, que tem a seguinte sintaxe: assert <test>, <message> e que equivale a: if not test: raise AssertionError, message Uma exemplo simples de utilização: def days_of_month(month): assert 1 < month < 12, \ "Error: month must be 1..12." ... Os testes assert são muito úteis quando se procede ao depuramento de código (debug . As mensagens de erro incluem automaticamente alguma informação sobre a parte do código que desencadeou a excepção. Estes testes não serão efectuados, acelerando a execução de um programa já depurado, quando se invoca o interpretador de python com a opção -O: > python -O myprog.py. Ficheiros Os ficheiros permitem guardar objectos de forma permanente. O Python tem um conjunto rico de instruções para manipular ficheiros. Um objecto do tipo ficheiro é criado com a instrução open: f = open(filename,mode) onde filename é o nome do ficheiro (ex. ’report.txt’), e mode uma das alternativas ’r’,’w’,’a’,’r+’ que significam abertura para leitura, escrita, continuação (append) e leitura e escrita. Um ficheiro é um objecto iterável onde o iterador devolve, em sequência, cada linha do ficheiro. Uma vez aberto um ficheiro pode ser lido da seguinte forma: for line in f: print line Os ficheiros possuem vários métodos (fazer dir(f) , onde f é um objecto do tipo ficheiro dá a lista de todos os métodos). Os mais importantes são read, readline, readlines e write. f.read(n) lê n caracteres do ficheiro devolvendo-os numa string. Se n for omitido, todo o ficheiro será lido. Se o final do ficheiro for encontrado,o número de caracteres devolvido poderá ser, eventualmente, inferior ao pedido. O seguinte programa imprime todo o conteúdo do ficheiro ’report.txt’. f = open(’report.txt’,’r’) print f.read() f.readlines(n) lé n linhas do ficheiro devolvendo-as numa lista de strings (uma por linha). lines = f.readlines() for line in lines: print line Os métodos write e writelines permitem escrever uma os mais linhas num ficheiro. O argumento é, no primeiro caso, uma string, no segundo uma lista de strings. Exemplo: f.write(’Hello world!\n’) messages = [’How are you?,’And your wife?’] f.writelines(messages) Notar que apenas strings podem ser guardadas em ficheiros. Objectos de outro tipo têm primeiro de ser convertidos em strings antes de poderem ser guardados num ficheiro. Objectos simples podem ser convertidos com a função str ou o operador de formatação (%). s = str(123.2) n = 123.2; s = 189 s = "%10.2f %d" % (f,n) Objectos mais complexos podem ser serializados automaticamente utilizando o módulo pickle. Objectos persistentes: pickle and shelve Um objecto x pode ser guardado num ficheiro f, aberto para escrita usando o seguinte método do módulo pickle: pickle.dump(x) Para ler de novo o objecto do ficheiro basta fazer x = pickle.load(f) A serialização (conversão para string) dos objectos é feita automaticamente, bem como a sua reconversão no tipo original. Praticamente qualquer objecto pode ser guardado num ficheiro. Contudo há que ter o cuidado de carregar os objectos do ficheiro exactamente pela mesma ordem em que foram guardados! Esta restrição é levantada no módulo shelve. Utilizando este módulo podemos guardar os nossos objectos num ficheiro que funciona como base de dados onde os objectos são indexados por uma string única a cada objecto. A interface de utilização desta base de dados assemelha-se à de um dicionário, como mostra o exemplo seguinte. O módulo shelve permite implementar muito facilmente uma base de dados sofisticada. import shelve db = shelve.open(’myfile’,’n’) # n = new file x,y,z,d = 123, ’abc’, range(20), {1:’a’,2:’bbbb’} class C: def __init__(self,x): self.x = x self.x2 = x**2 def __str__(self): return "C instance> %d %d" % (self.x,self.x2) if ___name__ == "__main__": c = C(123) db[’x’] = x db[’y’] = y db[’z’] = z db[’d’] = d db[’c instance’]= c db[’C class’] = C db.close() db = shelve.open(’myfile’,’c’) # c = append data print db[’d’],db[’x’] db.close() Redirecção de input e output Por vezes é útil redireccionar a saı́da de um programa do écrã para um ficheiro. Também pode ser útil que um programa passe a ler os dados que normalmente entram pelo teclado a partir de um ficheiro. Se o programa correr no sistema operativo Linux (ou num sistema Unix) a redirecção da entrada e saı́da pode fazer-se na linha de comando: python myprog < input.dat > output.out Outros sistemas operativos não dispõem desta facilidade. No entanto, o Python permite fazer a redirecção dos ficheiros standard de entrada, saı́da e de erro de uma forma portátil. Este ficheiros estão definidos no módulo sys com os nomes stdin, stdout e stderr. Por defeito, sys.stdin aponta para o teclado, e sys.stdout e sys.stderr para o ecrã. A redirecção da entrada e saı́da faz-se pondo estes nomes a apontar para outros ficheiros. O pequeno programa da página seguinte mostra um equivalente portátil da instrução Unix dada acima. Para correr o programa, deve digitar-se na linha de comando python myprog input.dat output.out Os ficheiros de nome “input.dat” e “output.dat” serão atribuı́dos no inı́cio do programa a sys.stdin e sys.stdout. Notar que sys.argv é uma lista que contém as strings de evocação do programa: sys.argv[0] é o nome do programa e sys.argv[1:] os argumentos do programa, neste casos os nomes dos ficheiros de input e output. #!/usr/bin/env python """ Example of redirection of input/output. Dumb program that simply copies the input file to the output file. """ import sys try: sys.stdin = open(sys.argv[1],’r’) except: print "Error opening input file.",sys.stdin sys.exit() try: sys.stdout = open(sys.argv[2],’w’) except: print "Error opening output file",sys.stdout sys.exit() for line in sys.stdin: print line[:-1] #remove last char (CR) Como não houve redirecção do ficheiro sys.stderr, as mensagens de erro eventualmente geradas pelo programa serão ainda enviadas para o ecrã. Para que estas mensagens também sejam enviadas para o ficheiro de saı́da que atribuı́mos a sys.stdout bastava incluir a instrução sys.stderr = sys.stdout a seguir à redirecção de sys.stdout. Formatação de output A conversão de objectos em strings, convenientemente formatados, é facilitada pela utlização do operador de formatação %. Uma string formatada é produzida com a expressão: format-string % (o1,o2,o3...) A format-string é uma string que pode conter códigos de formatação da tabela seguinte: %s string %c caracter %d número inteiro decimal %o numero inteiro octal %x número inteiro hexadecimal %f número em vı́rgula flutuante %e número em vı́rgula flutuante, notação cientı́fica %g número em vı́rgula flutuante, formato alternativo Exemplos: >>> x = "This is %5.3f with %d decimals" %\ (math.pi,3) >>> print x This is 3.142 with 3 decimals