Novo Blog

Novo endereço

https://blog.nilo.pro.br

sábado, 30 de agosto de 2014

Servidor de Chat com websockets e asyncio

Leia no novo blog

Já é hora de escrever sobre um projeto mais completo. Aproveito para mostrar o módulo websockets para Python 3.4 que funciona muito bem com asyncio. Para não ter problemas de interface, eu resolvi escrever o cliente do chat em JavaScript. O cliente de exemplo foi baixado daqui. Como quase sempre, os exemplos são muito simples e nos deixam com água na boca sobre o que poderíamos realmente fazer. Quem já tentou escrever um chat em JavaScript sabe que WebSockets são uma mão na roda.

Este artigo faz parte de uma série que escrevo sobre o asyncio do Python 3.4. Você pode ler os outros artigos clicando aqui: Python e asyncio, Asyncio e corotinas e o Lendo o teclado.

A ideia de usarmos WebSockets visa demonstrar a facilidade do módulo websockets que deve ser instalado no Python 3.4:

pip install websockets

Uma vez instalado o módulo, podemos criar um servidor, como o mostrado na documentação do módulo:

import asyncio
import websockets

@asyncio.coroutine
def hello(websocket, path):
    name = yield from websocket.recv()
    print("< {}".format(name))
    greeting = "Hello {}!".format(name)
    yield from websocket.send(greeting)
    print("> {}".format(greeting))

start_server = websockets.serve(hello, 'localhost', 8765)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

Execute o servidor com:
py -3 server.py

Para testar este programa, precisaremos de uma página com nosso cliente de chat. Eu preparei uma página com todo o código html e javascript necessário. Baixe a página cliente.html aqui. Salve o arquivo e abra-o com seu browser preferido: Chrome, Firefox ou o IE (>10).

Como o servidor é muito simples, tudo que podemos fazer é enviar uma mensagem e recebê-la de volta, já que é um servidor do tipo Echo (eco). Veja também que o servidor de WebSockets é inicializado como nossos outros servidores com o módulo asyncio, mas que este não obedece uma interface com métodos definidos para conexão, recebimento de dados etc. O objeto retornado pelo módulo websockets é um objeto com o cliente já conectado. Vejamos primeiramente como criar o servidor:

start_server = websockets.serve(hello, 'localhost', 8765)

A linha acima cria nosso servidor, chamando a função hello sempre que um novo cliente for executado. O nome localhost se refere a nosso computador e 8765 é a porta que utilizaremos para receber as conexões. Vejamos a função hello. Quando uma nova conexão for recebida, a função hello será chamado com dois parâmetros: o primeiro é o cliente já conectado e pronto para ser utilizado; e o segundo é o path ou o caminho usado (veremos isso depois em outro artigo). Na realidade, a corotina hello é responsável pelo tempo de vida e gestão da conexão do cliente. Quando a corotina hello termina, o cliente é desconectado. Veja também que usamos o yield from para enviar e receber dados. O uso do yield from permite que escrevamos nosso código como se sua execução fosse sequencial, como já discutimos nos outros artigos.

Um detalhe importante a notar é que a interface do módulo websockets já entrega os dados no formato de mensagem (como definido pelo protocolo). Diferentemente de um socket TCP/IP comum que trabalha com streams, entregando bytes. Quando o método recv retorna, uma mensagem inteira foi recebida, pouca importa quantos read foram feitos para completar esta tarefa. Esta característica vai facilitar muito a implementação do servidor de chat, uma vez que não precisaremos inventar um delimitador de mensagem, nem separar as mensagens manualmente em nosso código.

Em relação a nosso servidor de chat, o servidor de exemplo é bem limitado. A maior limitação é não permitir a comunicação entre clientes. A ideia do servidor de chat é enviar mensagens a todos os clientes conectados. Desta forma, o servidor deve ser informado sobre e registrar todas as conexões e desconexões do sistema. Vamos manter a lista dos clientes conectados com uma lista. Nossos clientes serão controlados por uma classe Cliente que veremos mais tarde. Observe a implementação parcial da classe Servidor:

class Servidor:
    def __init__(self):
        self.conectados = []
    
    @property
    def nconectados(self):
        return len(self.conectados)
    
    @asyncio.coroutine
    def conecta(self, websocket, path):
        cliente = Cliente(self, websocket, path)
        if cliente not in self.conectados:
            self.conectados.append(cliente)
            print("Novo cliente conectado. Total: {0}".format(self.nconectados))
        yield from cliente.gerencia()

    def desconecta(self, cliente):
        if cliente in self.conectados:
            self.conectados.remove(cliente)
        print("Cliente {1} desconectado. Total: {0}".format(self.nconectados, cliente.nome))

Veja que apenas conecta é uma corotina. Na realidade, todo o trabalho é feito na classe Cliente, que disponibiliza o método gerencia como uma corotina. O importante agora é entender a manutenção da lista de conexões ativas.

Vejamos a classe Cliente (parcial):

class Cliente:    
    def __init__(self, servidor, websocket, path):
        self.cliente = websocket
        self.servidor = servidor
        self.nome = None        
    
    @property
    def conectado(self):
        return self.cliente.open

    @asyncio.coroutine
    def gerencia(self):
        try:
            yield from self.envia("Bem vindo ao servidor de chat escrito em Python 3.4 com asyncio e WebSockets. Identifique-se com /nome SeuNome")
            while True:
                mensagem = yield from self.recebe()
                if mensagem:
                    print("{0} < {1}".format(self.nome, mensagem))
                    yield from self.processa_comandos(mensagem)                                            
                else:
                    break
        except Exception:
            print("Erro")
            raise        
        finally:
            self.servidor.desconecta(self)

Como cada cliente tem seu próprio websocket e precisa se comunicar com o servidor, guardaremos estas referências como atributos. Já preparamos também a gestão de nomes, embora tenhamos inicializado o nome do Cliente com None. O método gerencia, que é uma corotina, envia uma mensagem de boas vindas ao cliente e como no exemplo anterior, utiliza yield from para realizar o envio no loop de eventos. Uma vez que a mensagem inicial é enviada, entramos em um loop infinito que espera uma mensagem do cliente. Quando a conexão é fechada ou acontece um erro, o valor de mensagem é igual a None, por isso, testamos o valor de mensagem para sair do loop infinito criado pelo while True. Da mesma forma que no primeiro servidor de exemplo, nosso cliente é desconectado quando a corotina gerencia termina. Aproveitamos o fim da corotina para informar ao servidor que este cliente está se desconectado.

Ao recebermos uma mensagem, iniciamos o processamento da mesma, utilizando o método corotina processa_comandos da classe Cliente:
    @asyncio.coroutine
    def processa_comandos(self, mensagem):        
        if mensagem.strip().startswith("/"):
            comandos=shlex.split(mensagem.strip()[1:])
            if len(comandos)==0:
                yield from self.envia("Comando inválido")
                return
            print(comandos)
            comando = comandos[0].lower()            
            if comando == "horas":
                yield from self.envia("Hora atual: " + time.strftime("%H:%M:%S"))
            elif comando == "data":
                yield from self.envia("Data atual: " + time.strftime("%d/%m/%y"))
            elif comando == "clientes":
                yield from self.envia("{0} clientes conectados".format(self.servidor.nconectados))
            elif comando == "nome":
                yield from self.altera_nome(comandos)
            elif comando == "apenas":
                yield from self.apenas_para(comandos)
            else:
                yield from self.envia("Comando desconhecido")
        else:
            if self.nome:
                yield from self.servidor.envia_a_todos(self, mensagem)
            else:
                yield from self.envia("Identifique-se para enviar mensagens. Use o comando /nome SeuNome")

Lembrando os bons velhos tempos do IRC, o método processa_comandos reconhece comandos iniciados pela barra /. Desta forma, caso um cliente envie para o servidor /horas, este retornará a hora atual do servidor. Implementamos também os comandos:


  • /data que envia a data atual;
  • /clientes que envia quantos clientes estão conectados ao servidor, 
  • /nome e /apenas que veremos mais adiante. 


Utilizamos o módulo shlex para simplificar o processamento dos comandos, uma vez que a função shlex.split permite processar uma linha de texto como uma linha de comandos do bash, reconhecendo valores entre aspas e retirando os espaços em branco entre os parâmetros. Caso o usuário envie uma mensagem que não se inicia por uma barra, esta mensagem será enviada a todos os outros usuários conectados.

Para melhorar nosso chat, utilizamos o comando /nome para configurar nosso nome. O servidor cuida para que apenas um usuário utilize cada nome, retornando uma mensagem de erro, caso o nome desejado já esteja em uso. Este comando é processado pelo método altera_nome da classe Cliente:

    @asyncio.coroutine
    def altera_nome(self, comandos):                
        if len(comandos)>1 and self.servidor.verifica_nome(comandos[1]):
            self.nome = comandos[1]
            yield from self.envia("Nome alterado com sucesso para {0}".format(self.nome))
        else:
            yield from self.envia("Nome em uso ou inválido. Escolha um outro.")

O método altera_nome simplesmente verifica se passamos um parâmetro depois do comando /nome, pois comandos é uma lista onde cada elemento é um parâmetro (mas o primeiro é o nome do comando em si). Usando o método verifica_nome do servidor, checamos se o nome é único e enviamos uma mensagem de confirmação ou de erro dependendo do resultado. O método verifica_nome da classe Servidor é apresentado abaixo:

    def verifica_nome(self, nome):
        for cliente in self.conectados:
            if cliente.nome and cliente.nome == nome:
                return False
        return True

A verificação percorre toda a lista com os clientes conectados e verifica se um nome igual já foi registrado. Caso não encontre o nome na lista dos clientes já conectados, retorna True, permitindo o registro do nome pelo cliente que o solicitou.

Um outro comando interessante é o  /apenas que permite enviarmos uma mensagem apenas para determinado cliente. Vejamos a implementação do método apenas_para no cliente:

    @asyncio.coroutine
    def apenas_para(self, comandos):
        if len(comandos)<3:
            yield from self.envia("Comando incorreto. /apenas Destinatário mensagem")
            return
        destinatario = comandos[1]
        mensagem = " ".join(comandos[2:])
        enviado = yield from self.servidor.envia_a_destinatario(self, mensagem, destinatario)
        if not enviado:
            yield from self.envia("Destinatário {0} não encontrado. Mensagem não enviada.".format(destinatario))

E do método que realiza o envio na classe Servidor:

    @asyncio.coroutine
    def envia_a_destinatario(self, origem, mensagem, destinatario):        
        for cliente in self.conectados:            
            if cliente.nome == destinatario and origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("PRIVADO de {0} >> {1}".format(origem.nome, mensagem))
                return True
        return False

Um outro método importante é o que envia uma mensagem a todos os clientes conectados:

    @asyncio.coroutine
    def envia_a_todos(self, origem, mensagem):
        print("Enviando a todos")
        for cliente in self.conectados:            
            if origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("{0} >> {1}".format(origem.nome, mensagem))

Veja que enviamos a mensagem a todos da lista, mas que tomamos o cuidado para não enviar a mensagem ao mesmo cliente que a enviou, pois esta seria impressa uma segunda vez e nosso cliente Javascript já fez este trabalho por nós. A listagem completa abaixo:

import asyncio
import websockets
import time
import shlex

class Servidor:
    def __init__(self):
        self.conectados = []
    
    @property
    def nconectados(self):
        return len(self.conectados)
    
    @asyncio.coroutine
    def conecta(self, websocket, path):
        cliente = Cliente(self, websocket, path)
        if cliente not in self.conectados:
            self.conectados.append(cliente)
            print("Novo cliente conectado. Total: {0}".format(self.nconectados))            
        yield from cliente.gerencia()

    def desconecta(self, cliente):
        if cliente in self.conectados:
            self.conectados.remove(cliente)
        print("Cliente {1} desconectado. Total: {0}".format(self.nconectados, cliente.nome))            

    @asyncio.coroutine
    def envia_a_todos(self, origem, mensagem):
        print("Enviando a todos")
        for cliente in self.conectados:            
            if origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("{0} >> {1}".format(origem.nome, mensagem))

    @asyncio.coroutine
    def envia_a_destinatario(self, origem, mensagem, destinatario):        
        for cliente in self.conectados:            
            if cliente.nome == destinatario and origem != cliente and cliente.conectado:
                print("Enviando de <{0}> para <{1}>: {2}".format(origem.nome, cliente.nome, mensagem))
                yield from cliente.envia("PRIVADO de {0} >> {1}".format(origem.nome, mensagem))
                return True
        return False

    def verifica_nome(self, nome):
        for cliente in self.conectados:
            if cliente.nome and cliente.nome == nome:
                return False
        return True


class Cliente:    
    def __init__(self, servidor, websocket, path):
        self.cliente = websocket
        self.servidor = servidor
        self.nome = None        
    
    @property
    def conectado(self):
        return self.cliente.open

    @asyncio.coroutine
    def gerencia(self):
        try:
            yield from self.envia("Bem vindo ao servidor de chat escrito em Python 3.4 com asyncio e WebSockets. Identifique-se com /nome SeuNome")
            while True:
                mensagem = yield from self.recebe()
                if mensagem:
                    print("{0} < {1}".format(self.nome, mensagem))
                    yield from self.processa_comandos(mensagem)                                            
                else:
                    break
        except Exception:
            print("Erro")
            raise        
        finally:
            self.servidor.desconecta(self)

    @asyncio.coroutine
    def envia(self, mensagem):
        yield from self.cliente.send(mensagem)

    @asyncio.coroutine
    def recebe(self):
        mensagem = yield from self.cliente.recv()
        return mensagem

    @asyncio.coroutine
    def processa_comandos(self, mensagem):        
        if mensagem.strip().startswith("/"):
            comandos=shlex.split(mensagem.strip()[1:])
            if len(comandos)==0:
                yield from self.envia("Comando inválido")
                return
            print(comandos)
            comando = comandos[0].lower()            
            if comando == "horas":
                yield from self.envia("Hora atual: " + time.strftime("%H:%M:%S"))
            elif comando == "data":
                yield from self.envia("Data atual: " + time.strftime("%d/%m/%y"))
            elif comando == "clientes":
                yield from self.envia("{0} clientes conectados".format(self.servidor.nconectados))
            elif comando == "nome":
                yield from self.altera_nome(comandos)
            elif comando == "apenas":
                yield from self.apenas_para(comandos)
            else:
                yield from self.envia("Comando desconhecido")
        else:
            if self.nome:
                yield from self.servidor.envia_a_todos(self, mensagem)
            else:
                yield from self.envia("Identifique-se para enviar mensagens. Use o comando /nome SeuNome")

    @asyncio.coroutine
    def altera_nome(self, comandos):                
        if len(comandos)>1 and self.servidor.verifica_nome(comandos[1]):
            self.nome = comandos[1]
            yield from self.envia("Nome alterado com sucesso para {0}".format(self.nome))
        else:
            yield from self.envia("Nome em uso ou inválido. Escolha um outro.")

    @asyncio.coroutine
    def apenas_para(self, comandos):
        if len(comandos)<3:
            yield from self.envia("Comando incorreto. /apenas Destinatário mensagem")
            return
        destinatario = comandos[1]
        mensagem = " ".join(comandos[2:])
        enviado = yield from self.servidor.envia_a_destinatario(self, mensagem, destinatario)
        if not enviado:
            yield from self.envia("Destinatário {0} não encontrado. Mensagem não enviada.".format(destinatario))



servidor=Servidor()
loop=asyncio.get_event_loop()

start_server = websockets.serve(servidor.conecta, 'localhost', 8765)

try:
    loop.run_until_complete(start_server)
    loop.run_forever()
finally:
    start_server.close()

Você pode baixar o arquivo completo clicando aqui.

Para testar, abra o arquivo cliente.html que você já deve ter salvo em seu computador. Se você já ativou o servidor com

py -3 servidor2.py

nosso cliente já deve estar conectado. Caso contrário, recarregue a página no navegador para forçar a reconexão. Quando conectado, o cliente exibe uma barra azul no topo da página. Para simular vários clientes, abra várias vezes o arquivo cliente.html. Vejamos uma sessão simples com 3 clientes:

No primeiro cliente:
/nome Cliente1
/horas

No segundo cliente:
/nome Cliente2
/data

No terceiro cliente:
/nome Cliente3
/apenas Cliente1 Olá 1
/apenas Cliente2 Olá 2
Olá todos!

Não esqueça de copiar linha por linha nas respectivas janelas. Digite ENTER para enviar a mensagem, uma de cada vez. Divirta-se!


sábado, 23 de agosto de 2014

Asyncio - Lendo o teclado

Leia no novo blog

Continuando a série sobre o módulo asyncio do Python 3.4, vamos ver como criar um jogo simples, em modo texto. O objetivo do jogo é simplesmente mostrar um labirinto e deixar o jogador se mover utilizando o teclado numérico (4 - esquerda, 6 - direita, 8 - cima, 2 - baixo e S para sair). Para exercitar nossos músculos da época do DOS com programação assíncrona, vamos exibir o relógio na última linha. O resultado final deve se parecer com a imagem abaixo:


Você precisa instalar o colorconsole e o Python 3.4.1 para executar este programa.

from random import shuffle, randrange
from colorconsole import terminal
from concurrent.futures import ThreadPoolExecutor
import datetime
import asyncio

# Colorconsole: https://github.com/lskbr/colorconsole
# Make_make: http://rosettacode.org/wiki/Maze_generation#Python
def make_maze(w = 16, h = 8):
    vis = [[0] * w + [1] for _ in range(h)] + [[1] * (w + 1)]
    ver = [["|  "] * w + ['|'] for _ in range(h)] + [[]]
    hor = [["+--"] * w + ['+'] for _ in range(h + 1)]
 
    def walk(x, y):
        vis[y][x] = 1
 
        d = [(x - 1, y), (x, y + 1), (x + 1, y), (x, y - 1)]
        shuffle(d)
        for (xx, yy) in d:
            if vis[yy][xx]: continue
            if xx == x: hor[max(y, yy)][x] = "+  "
            if yy == y: ver[y][max(x, xx)] = "   "
            walk(xx, yy)
 
    walk(randrange(w), randrange(h))
    maze = []
    for (a, b) in zip(hor, ver):
        maze.append("".join(a))
        maze.append("".join(b))
    return maze
 
class Jogo:
    LARGURA = 24
    ALTURA = 11
    def __init__(self):        
        self.tela = terminal.get_terminal(conEmu=False)
        self.tela.enable_unbuffered_input_mode()
        self.labirinto_cores = (terminal.colors["RED"],terminal.colors["BLACK"])
        self.jogador_carac = (terminal.colors["WHITE"],terminal.colors["BLUE"],'*')
        self.labirinto = make_maze(Jogo.LARGURA,Jogo.ALTURA)
        self.loop = asyncio.get_event_loop()
        self.tpool = ThreadPoolExecutor(max_workers=2)
        while True:
            self.x = randrange(Jogo.LARGURA*3)
            self.y = randrange(Jogo.ALTURA*2)
            if self.pode_mover(self.x, self.y):
                break
        self.jogando = True

    def fim_do_jogo(self):
        self.jogando = False
        self.loop.stop()

    @asyncio.coroutine
    def le_teclado(self):
        while self.jogando:            
            key = yield from self.loop.run_in_executor(self.tpool, self.tela.getch)            
            if(key!=None):
                nx, ny = self.x, self.y
                if key == b'4':
                    if nx > 1:
                        nx-=1
                elif key == b'6':
                    if nx < Jogo.LARGURA*3-1:
                        nx+=1
                elif key == b'8':
                    if ny > 0:
                        ny -=1
                elif key == b'2':
                    if ny < Jogo.ALTURA*2:                        
                        ny +=1
                elif key == b"S":
                    self.fim_do_jogo()
                    break
            if self.pode_mover(nx,ny) and (nx, ny) != (self.x,self.y):
                self.x, self.y = nx, ny
                self.desenha()           

    def pode_mover(self, x,y):
        return self.labirinto[y][x]==' '

    def desenha(self):
        self.tela.set_color(*self.labirinto_cores)
        #self.tela.clear()
        self.tela.gotoXY(0,0)
        self.tela.set_title("Labirinto") 
        for linha in self.labirinto:
            print(linha)
        self.tela.gotoXY(self.x, self.y)
        self.tela.cprint(*self.jogador_carac)

    @asyncio.coroutine
    def relogio(self):
        while self.jogando:
            self.tela.print_at(10,23,datetime.datetime.now().strftime("%d/%m/%y %H:%M:%S"))
            yield from asyncio.sleep(1)

    def execute(self):
        self.tela.clear()
        self.desenha()
        try:
            asyncio.async(self.le_teclado())
            asyncio.async(self.relogio())
            self.loop.run_forever()
        except KeyboardInterrupt:
            print("exit")
        finally:
            self.tpool.shutdown()
            self.loop.close()
            self.tela.restore_buffered_mode()

jogo = Jogo()
jogo.execute()

Para adicionar um pouco de variação ao jogo, eu baixei uma função que gera labirintos deste site. O programa foi modificado para retornar uma lista de strings, que é utilizada para detectar as paredes do labirinto.

O loop de eventos é parecido com o dos artigos anteriores, mas desta vez um ThreadPool está sendo criado. Um thread extra é necessário, pois vamos bloquear a rotina até que uma tecla seja pressionada. Esta construção funciona com várias rotinas bloqueantes que não podem ser utilizadas com asyncio, pois interromperiam a execução de todas as outras corotinas.

    @asyncio.coroutine
    def le_teclado(self):
        while self.jogando:            
            key = yield from self.loop.run_in_executor(self.tpool, self.tela.getch)            
            if(key!=None):
                nx, ny = self.x, self.y
                if key == b'4':
                    if nx > 1:
                        nx-=1
                elif key == b'6':
                    if nx < Jogo.LARGURA*3-1:
                        nx+=1
                elif key == b'8':
                    if ny > 0:
                        ny -=1
                elif key == b'2':
                    if ny < Jogo.ALTURA*2:                        
                        ny +=1
                elif key == b"S":
                    self.fim_do_jogo()
                    break
            if self.pode_mover(nx,ny) and (nx, ny) != (self.x,self.y):
                self.x, self.y = nx, ny
                self.desenha()

Criamos le_teclado como uma corotina, mas como o módulo colorconsole não foi criado para trabalhar com corotinas, precisamos chamar self.tela.getch usando um Executor, no caso, nosso ThreadPool. Desta forma, nossa chamada bloqueante será executada em um thread do ThreadPool e nosso loop de eventos vai continuar a executar normalmente. Quando pressionarmos uma tecla, a função self.tela.getch vai retornar e a partir daí, trabalharemos com o resultado no yield from. O resto do método verifica se a tecla é de movimento ou de saída do jogo. No final, verificamos se o jogador se mexeu ou se a nova posição seria a de uma parede. Caso a posição seja alterada, redesenhamos a tela, para que a nova posição do jogador seja visível.

    @asyncio.coroutine
    def relogio(self):
        while self.jogando:
            self.tela.print_at(10,23,datetime.datetime.now().strftime("%d/%m/%y %H:%M:%S"))
            yield from asyncio.sleep(1)

A corotina relogio exibe a data e hora atuais a cada 1 segundo na tela. Esta execução ocorre mesmo se nada pressionarmos no teclado, confirmado a correta execução de nosso loop de eventos. Este programa pode ser executado para conter inimigos e movê-los em outra corotina, similar a do relógio.

Testado no Windows 8.1 com Python 3.4.1, colorconsole 0.7.1.

domingo, 10 de agosto de 2014

Asyncio e corotinas

Leia no novo blog

Continuando a série sobre o módulo asyncio do Python 3.4, vou apresentar as corotinas e como elas simplificam a escrita de nossos programas com o loop de eventos. Com a saída do Python 3.4, eu atualizei o livro de Introdução à Programação com Python. Alguns assuntos fogem ao escopo do livro que é destinado a iniciantes. Eu estou continuando uma série de posts curtos sobre alguns tópicos que acho interessantes e quem sabe até podem virar base para um novo livro. Clique aqui para ler o primeiro artigo.

No artigo anterior, apresentamos uma chamada ao loop de eventos bem simples:
import asyncio

def print_and_repeat(loop):
    print('Hello World')
    loop.call_later(2, print_and_repeat, loop)

loop = asyncio.get_event_loop()
loop.call_soon(print_and_repeat, loop)
loop.run_forever()

Vejamos como reescrever este exemplo simples usando corotinas:
import asyncio

@asyncio.coroutine
def print_and_repeat(loop):
    while True:
        print('Hello World')
        yield from asyncio.sleep(2)

loop = asyncio.get_event_loop()
try:
 loop.run_until_complete(print_and_repeat(loop))
finally:
 loop.close()

Este exemplo foi extraído da documentação do Python, vamos ver o que mudou. A principal mudança no início do programa é o uso do decorador @asyncio.coroutine. Este decorador transforma nossa função em uma corotina e permite a utilização do yield from, como definido na PEP380. Veja que o cabeçalho da função não foi alterado, mas que substituímos a chamada de loop.call_later pela combinação de um while True com um yield from no final. Uma corotina pode ser suspensa e esperar o processamento de uma outra corotina. No caso, asyncio.sleep(2) é uma corotina do módulo asyncio que suspende a execução da função pelo número de segundos passados como parâmetro. Na realidade, esta chamada retorna uma corotina que é marcada como completa após os 2 segundos. Isto pode ser realizado pois no yield from, criamos a nova corotina e indicamos ao loop que não continue a executar print_and_repeat até que a nova corotina esteja concluída. A partir deste ponto, a execução volta ao loop de eventos que monitora a conclusão da nova corotina criada, suspendendo a execução da anterior. Uma vez que o a corotina do sleep é concluída, após 2 segundos, o loop reativa a chamada suspensa de print_and_repeat e a execução continua, voltando para o while. Parece complicado, mas veja como ficou fácil de escrevermos a função. Fica bem mais claro nossa intenção de realizar uma repetição do print('Hello World') a cada 2 segundos.

Modificamos também a chamada de execução da corotina, pois agora utilizamos loop.run_until_complete para iniciar nossa corotina principal. Aproveitamos para colocar tudo entre um try...finally para terminar a execução do loop corretamente (mesmo em caso de exceção). Perceba que no exemplo anterior, com call_soon, passamos a função e seus parâmetros, mas não executamos a função em si. No caso de run_until_complete, estamos passando o retorno da chamada de print_and_repeat que é uma corotina, uma vez que a marcamos com o decorador @asyncio.coroutine.

No post anterior comparamos a velocidade de execução entre as várias formas de se executar código em paralelo com Python. Agora veremos como usar o asyncio para criar uma aplicação prática, como um cliente e um servidor TCP/IP, mas indo além dos exemplos da documentação do Python. É preciso lembrar que o módulo asyncio ainda é muito novo e que tanto a documentação quanto a implementação de algumas funcionalidades ainda estão sendo alteradas.

Vamos começar pelo servidor. Um servidor TCP/IP é um exemplo clássico de programa chato a escrever. Normalmente, você pode escolher utilizar threads ou se aventurar com select e chamadas não bloqueantes para gerenciar várias conexões. Este problema se agrava em aplicações mais complexas, onde algum processamento precisa ser realizado antes de se gerar a resposta, por exemplo, a um comando do usuário. Usando o módulo asyncio, esta tarefa fica bem mais fácil. Primeiro, porque o tratamento de dados é gerenciado por uma classe, responsável pelo protocolo. Esta classe traz métodos que são chamados em situações comuns ao programarmos um servidor TCP/IP, como chegada de uma nova conexão, desconexão, chegada de dados para leitura entre outras. Além disso, o asyncio também traz classes especializadas em quebrar os dados em linhas, o que facilita a implementação de protocolos com comandos em formato texto, terminados por enter (CR).

O servidor é controlado por uma classe chamada EchoServer, pois o desenvolvi a partir do servidor de Echo dado como exemplo na documentação, mas com alguns detalhes que observei no código do módulo asyncio. O protocolo implementado é bem simples, a cada linha, a data e hora atuais são enviadas. Se o cliente enviar sair a conexão é terminada. Vamos ver o programa completo e discutir parte por parte.

import asyncio
import time
from common import *

class EchoServer(asyncio.streams.FlowControlMixin):    
    ativas = 0
    def connection_made(self, transporte):        
        peername = transporte.get_extra_info('peername')
        print('Conexão de {}'.format(peername))
        EchoServer.ativas+=1
        print("Conexões ativas: {}".format(EchoServer.ativas))
        self.transporte = transporte
        self.leitor = asyncio.StreamReader()
        self.leitor.set_transport(self.transporte)
        self.escritor = asyncio.StreamWriter(transport=self.transporte, protocol=self, reader=self.leitor, loop=asyncio.get_event_loop())        
        asyncio.async(self.gerencia())
    
    @asyncio.coroutine
    def gerencia(self):        
        while True:
            dados = yield from self.leitor.readline()                        
            self.escritor.write(strToByte(time.strftime("%c")+"\r\n"))            
            yield from self.escritor.drain()
            comando = byteToStr(dados).strip().lower()
            if(comando == "sair"):
                self.transporte.close()
                return

    def connection_lost(self, exp):                
        EchoServer.ativas-=1
        print("Conexões ativas: {}".format(EchoServer.ativas))
        super().connection_lost(exp)

    def data_received(self, dados):                
        print('data received: {0}'.format(byteToHex(dados)))
        print('     received: {0}'.format(strPrintable(dados)))
        print('       string: {0}'.format(byteToStr(dados)))
        self.leitor.feed_data(dados)
        

loop = asyncio.get_event_loop()
coro = loop.create_server(EchoServer, '127.0.0.1', 8888)
servidor = loop.run_until_complete(coro)
print('Escutando {}'.format(servidor.sockets[0].getsockname()))

try:
    loop.run_forever()
except KeyboardInterrupt:
    print("exit")
finally:
    servidor.close()    
    loop.close()

A classe EchoServer herda de uma classe fornecida em asyncio.streams, chamada FlowControlMixin. A classe FlowControlMixin é por sua vez derivada de Protocols, também fornecida pelo módulo asyncio. A ideia desta classe é implementar um protocol factory, ou seja, um construtor de instâncias responsáveis pela implementação da gestão de cada nova conexão. Um protocolo normal, precisa herdar apenas de Protocols, mas para utilizar alguns métodos para leitura buferizada de linhas, especialmente o write.drain que veremos logo após, a implementação contida em FlowControlMixin é interessante.

Aproveitamos a nova classe para contar o número de conexões ativas. Cada nova conexão recebida por nosso servidor TCP/IP chama o construtor de nossa classe e o método connection_made, passando o transporte (entenda como o socket já conectado) como parâmetro.

Utilizando o parâmetro transporte, chamamos o método get_extra_info('peername') para obter o endereço do cliente que acabou de se conectar ao servidor. Logo em seguida, incrementamos o número de conexões. Veja que como o código que roda no loop de eventos não é multi-threaded, não precisamos de locks ou de outros mecanismos de controle, já que apenas uma função roda a cada vez. O resto do método connection_made prepara as instâncias do leitor e do escritor, objetos das classes StreamReader e StreamWriter respectivamente. Estes objetos vão fornecer corotinas úteis para ler e escrever os dados de forma não bloqueante. Veja que passamos transporte tanto para o escritor quanto para o leitor e que uma série de parâmetros são necessários para a inicialização do escritor.

No fim de connection_made, usamos a função asyncio.async para iniciar o processamento da corotina self.gerencia, dentro do loop de eventos. Veja que o método gerencia foi marcado com o decorador @asyncio.coroutine.

O método gerencia contém uma estrutura de repetição while que espera uma linha do cliente. Veja que utilizamos yield from para suspender a execução de gerencia enquanto self.leitor.readline() não terminar. Neste ponto, a execução volta para o loop de eventos e retorna apenas quando self.leitor.readline() contém uma linha enviada pelo cliente ou caso uma exceção tenha ocorrido. O uso do yield from é fundamental, pois caso o readline() esperasse o cliente enviar a linha para continuar a execução, todo o loop de eventos seria bloqueado. Como o uso do yield from, a execução volta para o loop que é livre para executar outros métodos e outras corotinas. O objetivo é não fazer o computador esperar por dados ou resultados que demoram muito tempo (ou um tempo desconhecido, possivelmente longo, para retornar). Outra característica de yield from é que o resultado do self.leitor.readline() é retornado e no caso, armazenado na variável dados.

A execução segue normalmente e nosso servidor envia a hora e a data atual, veja que uma linha foi acrescentada ao final da string. Este fim de linha é importante, pois como nosso protocolo é em formato texto e orientado a linhas, esperamos o enter (CR) para processar o comando ou a resposta.
Depois de escrever a resposta, usamos self.escritor.drain() que é uma outra corotina. Esta nova corotina não completa até o que o buffer de escrita seja enviado. Desta forma, podemos garantir que os dados foram enviados (ainda que não possamos ter certeza se estes foram recebidos pelo cliente) antes de continuarmos. Como usamos yield from com esta corotina, a execução é suspensa até que o drain seja completado.

Como usamos Python 3.4, os dados são do tipo byte e não string. A função byteToStr converte de bytes para string, usando a codificação UTF-8. Esta função será apresentada no programa common.py, compartilhando rotinas úteis tanto para nosso cliente quanto para nosso servidor. Para facilitar o processamento de comandos, retiramos os espaços em branco do início e fim do comando, inclusive enter (CR) e LF, e convertemos o resultado para minúsculas com lower. Se o comando for igual a "sair", chamamos o método close de self.transporte para encerarmos a conexão. Veja que ao fecharmos a conexão, finalmente retornamos como em uma função normal, utilizando return e terminando assim a execução de nossa corotina gerencia.

O método connection_lost é chamado quando a desconexão do cliente é detectada. O número de conexões ativas é decrementado e o método connection_lost da superclasse é chamado. O parâmetro exp contém None caso seja uma desconexão normal ou a exceção em caso de erro. Neste exemplo não estamos tratando os possíveis erros para nos concentrarmos no asyncio.

Já o método data_received é chamado sempre que dados forem recebidos pelo transporte. Os dados recebidos são passados como parâmetro (dados). Aqui, incluí algumas funções de debug que exibem os dados em formato hexadecimal, string e UTF-8. Estas funções são necessárias para verificarmos se os dados estão chegando no formato esperado. Você pode executar um teste com um programa de telnet clássico, como o Putty no Windows, mas não esqueça de desativar a opção de negociação do protocolo, para evitar que comandos que não interpretamos sejam enviados, ou simplesmente, teste com o cliente que é apresentado logo abaixo.

Um detalhe muito importante de data_received é a chamada do método self.leitor.feed_data, que envia os dados para o leitor, responsável por quebrar os dados em linhas.

Em nosso programa principal, obtemos o loop de eventos com get_event_loop() e criamos uma corotina que inicializa nosso servidor com loop.create_server. Em create_server, informamos o endereço que nosso servidor irá escutar (ip e porta). Veja que a classe EchoServer foi passada como protocol factory.

Ao chamarmos loop.run_until_complete(coro), o loop de eventos roda até que a corotina criada pelo create_server termine, retornando um objeto servidor, utilizado para parar o servidor e para ter acesso a todas as conexões, mas isso fica para outro post.

Chamamos loop.run_forever() para ativar nosso servidor. Para desativá-lo, digite CTRL+C.

Neste ponto, o endereço 127.0.0.1, porta 8888 estará recebendo conexões. Quando uma conexão for recebida, uma nova instância de EchoServer será criada. Ao se estabelecer a conexão o método connection_made será chamado e ativará uma corotina gerencia para gerenciar a recepção e o envio de linhas de comandos. O método data_received é chamado sempre que novos dados forem recebidos (seja uma linha completa ou não). O método connection_lost é chamado quando o cliente se desconectar.

Vejamos o código fonte de common.py:

import string

def byteToHex(data, sep=" "):
    return sep.join("{0:02X}".format(x) for x in data)

def strPrintable(data, sep=" "):
    return sep.join("{0:2s}".format(chr(s) if chr(s) in string.printable and s>30 else ".") for s in data)

def strToByte(s, encoding ="utf-8"):
    return s.encode(encoding)

def byteToStr(data, encoding ="utf-8"):
    return data.decode(encoding, errors="replace")

E o código fonte de nosso cliente.py:

import asyncio
import time
from common import *

class EchoClient(asyncio.streams.FlowControlMixin):        
    def connection_made(self, transporte):        
        peername = transporte.get_extra_info('peername')
        print('Conectado à {}'.format(peername))                
        self.transporte = transporte
        self.leitor = asyncio.StreamReader()
        self.leitor.set_transport(self.transporte)
        self.escritor = asyncio.StreamWriter(transport=self.transporte, protocol=self, reader=self.leitor, loop=asyncio.get_event_loop())        
        asyncio.async(self.gerencia())
        self.feito = asyncio.Future()
    
    @asyncio.coroutine
    def gerencia(self):        
        for x in range(10):                        
            self.escritor.write(strToByte("Alô\r\n"))
            yield from self.escritor.drain()

            dados = yield from self.leitor.readline()            
        self.escritor.write(strToByte("sair\r\n"))
        yield from self.escritor.drain()                                         
        self.transporte.close()
        self.feito.set_result(True)

    def connection_lost(self, exp):        
        print("Conexão perdida")
        super().connection_lost(exp)

    def data_received(self, dados):                
        print('dados recebidos: {0}'.format(byteToHex(dados)))
        print('      recebidos: {0}'.format(strPrintable(dados)))
        print('         string: {0}'.format(byteToStr(dados)))
        self.leitor.feed_data(dados)

        
loop = asyncio.get_event_loop()
coro = loop.create_connection(EchoClient, '127.0.0.1', 8888)
transporte, protocolo = loop.run_until_complete(coro)

try:
    loop.run_until_complete(protocolo.feito)
except KeyboardInterrupt:
    pass
finally:    
    loop.close()

Execute o servidor e depois o cliente, cada em uma janela ou terminal diferente. Veja que o cliente termina sua execução após enviar 10 vezes o comando Alô e sair. Execute várias vezes o cliente e veja que o servidor continua ativo. Experimente aumentar o número de comandos de 10 para 100 no cliente e reexecute. Tente executar a partir de uma terceira janela outro cliente simultaneamente. Observe que conseguimos implementar um cliente e um servidor TCP/IP em um pouco mais de 100 linhas de código em Python. Um servidor capaz de atender vários clientes sem utilizar múltiplos threads. Você pode comentar ou remover os prints que não precisar, eles servem apenas para debugar.

O código do cliente é muito parecido com o código do servidor. A principal mudança é o método gerencia e o atributo self.feito, criado com asyncio.Future(). Vejamos a criação da instância de nosso cliente. A função create_conection é na realidade uma corotina que recebe o protocolo (no caso EchoClient), o ip e a porta do servidor. Ao chamarmos loop.run_until_complete(coro), uma tupla com o transporte e o protocolo é retornada. Este retorno é importante, pois precisamos ter acesso a instância de EchoClient criada para gerenciar nossa conexão, no caso protocolo. Com a instância de EchoClient retornada em protocolo, podemos rodar o loop até que feito seja marcada como finalizada: loop.run_until_complete(protocolo.feito). Esta etapa é importante, pois devemos executar o loop até que o cliente tenha tempo para terminar seu trabalho. Veja que no final de gerencia, marcamos self.feito como concluída: self.feito.set_result(True).

Se você quiser testar o servidor com vários clientes simultaneamente, adicione a seguinte função ao código de cliente.py e modifique o programa principal para:

@asyncio.coroutine
def roda_varias(vezes):
    pendente = []
    for t in range(vezes):
        pendente.append(asyncio.async(loop.create_connection(EchoClient, '127.0.0.1', 8888)))
    for y in pendente:            
        transporte, protocolo = yield from y
        yield from asyncio.wait_for(protocolo.feito, None)

loop = asyncio.get_event_loop()
coro = loop.create_connection(EchoClient, '127.0.0.1', 8888)
client = loop.run_until_complete(coro)

try:
    loop.run_until_complete(roda_varias(100))
except KeyboardInterrupt:
    pass
finally:    
    loop.close()

Você também pode utilizar start_server e open_connection para receber diretamente uma tupla com StreamReader e StreamWriter, mas estes exemplos você pode encontrar na documentação do Python.

No próximo artigo, uma nova classe comum será usada para gerenciar os protocolos e outra forma de instanciação será passada para realizar uma comunicação entre vários clientes.



sábado, 28 de junho de 2014

Python asyncio - Métodos assíncronos em Python

Leia no novo blog

Com a saída do Python 3.4, eu atualizei o livro de Introdução à Programação com Python. Alguns assuntos fogem ao escopo do livro que é destinado a iniciantes. Eu vou começar a escrever uma série de posts curtos sobre alguns tópicos que acho interessantes e quem sabe até podem virar base para um novo livro.

Uma das novidades do Python 3.4 é o módulo asyncio que traz várias rotinas para chamada de métodos assíncronos em Python. A programação assíncrona é um pouco diferente do que normalmente estamos habituados a escrever em Python, mas é uma excelente alternativa a utilização de threads e uma boa escolha para resolver problemas com muitas entras ou saídas (I/O).

import asyncio

def print_and_repeat(loop):
    print('Hello World')
    loop.call_later(2, print_and_repeat, loop)

loop = asyncio.get_event_loop()
loop.call_soon(print_and_repeat, loop)
loop.run_forever()

O mecanismo usado no exemplo é bem simples. A variável loop contém o loop de eventos, uma vez que chamamos asyncio.get_event_loop() e esta retorna o loop de eventos atual. Na linha seguinte, chamamos o método call_soon para agendar a chamada de um método. call_soon adiciona a chamada da função print_and_repeat, definida anteriormente. O segundo parâmetro de call_soon é na realidade o parâmetro para print_and_repeat. Desta forma, loop.call_soon(print_and_repeat, loop) adiciona ao loop de eventos uma chamada a função print_and_repeat, passando loop como primeiro parâmetro. Confuso? Vamos ver o que acontece ao executarmos:

Z:\artigos>c:\python34\python asyncio1.py
Hello World
Hello World
Hello World
Hello World

O programa executa e fica na linha do loop.run_forever(). O método run_forever() processa os eventos e é necessário para o bom funcionamento do nosso programa. Experimente remover esta linha e veja que nada é impresso na tela, com o programa finalizando logo em seguida. Isto acontece porque sem run_forever, os eventos não são processados e o Python executa o script até o fim, finalizando sem nunca ter chamado print_and_repeat, ou seja, sem processar a lista de eventos.
Voltando ao nosso exemplo, você entendeu como Hello Word foi impresso várias vezes? Como se estivesse dentro de um for ou while? Isto acontece na última linha de print_and_repeat, onde com o método call_later agendamos uma próxima chamada a print_and_repeat depois de 2 segundos. Logo, o primeiro parâmetro de call_later é o tempo a esperar antes de chamar a função, o segundo a função em si, seguido dos parâmetros a passar a esta função, como fizemos na chamada de call_soon anteriormente.

O código equivalente, sem utilizar eventos seria algo como:

import time

while True:
    print("Hello World")
    time.sleep(2)

Se é tão simples, por que complicar? Eventos facilitam a execução de código alternadamente, algo difícil de realizar sem utilizarmos threads. Basicamente, solicitamos ao loop de eventos para executar nossas funções de acordo com sua disponibilidade, como se passássemos em nosso programa uma lista de tarefas a executar. O loop de eventos atualiza constantemente a lista de tarefas e isto permite a execução de tarefas em ordem diferente da que estas foram incluídas.
Vejamos um outro exemplo, com call_later:

import asyncio
import time
import random

def faz_algo(loop):
    espera = random.random()
    print("Fazendo algo... espera = %f" % espera)
    loop.call_later(espera, faz_algo, loop)

def print_and_repeat(loop):
    global último
    agora = time.time()
    print('Alô - Tempo decorrido: %f' % (agora - último))
    último = agora
    loop.call_later(2, print_and_repeat, loop)

último = time.time()
loop = asyncio.get_event_loop()
loop.call_soon(print_and_repeat, loop)
loop.call_soon(faz_algo, loop)
loop.run_forever()

Que ao executar nos exibe:

Z:\artigos&gt;c:\python34\python asyncio2.py
Alô - Tempo decorrido: 0.007002
Fazendo algo... espera = 0.360861
Fazendo algo... espera = 0.411369
Fazendo algo... espera = 0.788518
Fazendo algo... espera = 0.253137
Fazendo algo... espera = 0.193489
Alô - Tempo decorrido: 2.003724
Fazendo algo... espera = 0.142127
Fazendo algo... espera = 0.523892
Fazendo algo... espera = 0.250166
Fazendo algo... espera = 0.467522
Fazendo algo... espera = 0.304402
Fazendo algo... espera = 0.831368
Alô - Tempo decorrido: 1.988947
Fazendo algo... espera = 0.647338
Fazendo algo... espera = 0.378327
Fazendo algo... espera = 0.013393
Fazendo algo... espera = 0.284973
Fazendo algo... espera = 0.368350
Alô - Tempo decorrido: 2.002417
Fazendo algo... espera = 0.257536

Os valores de espera podem variar de uma execução a outra, o importante é observar que chamamos a função faz_algo e print_and_repeat são chamadas alternadamente, ou melhor, quase que ao mesmo tempo, como se estivéssemos utilizando threads. O que o novo programa faz é executar print_and_repeat como no primeiro exemplo, mas também a função faz_algo. A função faz_algo gera um tempo de espera aleatório, usando random.random() para agendar a próxima execução. Como print_and_repeat executa apenas a cada 2 segundos, o loop de eventos fica livre para executar outras tarefas, o que podemos ver na saída de nosso programa.
O programa equivalente, sem o loop de eventos ficaria parecido com:

import time
import random

def faz_algo():
    espera = random.random()
    print("Fazendo algo... espera = %f" % espera)
    time.sleep(espera)

def print_and_repeat():
    global último
    agora = time.time()
    print('Alô - Tempo decorrido: %f' % (agora - último))
    último = agora    

último = 0
while True:
   agora = time.time()
   if agora - último >= 2:
      print_and_repeat()
   else:
      faz_algo()

Que produz uma saída semelhante:

Z:\artigos>c:\python34\python asyncio3.py
Alô - Tempo decorrido: 1403984084.967565
Fazendo algo... espera = 0.233725
Fazendo algo... espera = 0.309948
Fazendo algo... espera = 0.551685
Fazendo algo... espera = 0.835331
Fazendo algo... espera = 0.008247
Fazendo algo... espera = 0.140163
Alô - Tempo decorrido: 2.081037
Fazendo algo... espera = 0.597524
Fazendo algo... espera = 0.004582
Fazendo algo... espera = 0.054279
Fazendo algo... espera = 0.037356
Fazendo algo... espera = 0.951933
Fazendo algo... espera = 0.003549
Fazendo algo... espera = 0.856917
Alô - Tempo decorrido: 2.506460
Fazendo algo... espera = 0.435528
Fazendo algo... espera = 0.599356
Fazendo algo... espera = 0.798355
Fazendo algo... espera = 0.594801

O importante é notar que quanto mais tarefas temos a realizar, mais complicado ficaria escrever o programa equivalente, sem o loop de eventos. Um conceito a também observar é que apenas uma das funções roda de cada vez. Este detalhe permite construir nossos programas como fazemos normalmente, sem nos preocuparmos com threads. Outro detalhe é o módulo asyncio traz várias outras classes que ajudam a trabalhar de forma assíncrona com arquivos e sockets, por exemplo. A programação com loop de eventos não resolve todos os tipos de problema. Vejamos uma função chamada calcula_algo que utiliza o processador para realizar um cálculo relativamente demorado. Veja o programa abaixo:

import asyncio
import time
import random

def calcula_algo(loop, id):
    limite = random.randint(30000,50000)
    print("Calculando %d" % id)
    z=1
    for x in range(1,limite):
        z*=x
    print("Fim do Cálculo %d" % id)
    loop.call_soon(calcula_algo, loop, id+2)

def faz_algo(loop):
    espera = random.random()
    print("Fazendo algo... espera = %f" % espera)
    loop.call_later(espera, faz_algo, loop)

def print_and_repeat(loop):
    global último
    agora = time.time()
    print('Alô - Tempo decorrido: %f' % (agora - último))
    último = agora
    loop.call_later(2, print_and_repeat, loop)

último = time.time()
loop = asyncio.get_event_loop()
loop.call_soon(print_and_repeat, loop)
loop.call_soon(faz_algo, loop)
loop.call_soon(calcula_algo, loop, 1)
loop.call_soon(calcula_algo, loop, 2)
loop.run_forever()

Que produz como saída:

Z:\artigos>c:\python34\python asyncio4.py
Alô - Tempo decorrido: 0.007003
Fazendo algo... espera = 0.761420
Calculando 1
Fim do Cálculo 1
Calculando 2
Fim do Cálculo 2
Calculando 3
Fim do Cálculo 3
Calculando 4
Fim do Cálculo 4
Fazendo algo... espera = 0.395006
Alô - Tempo decorrido: 6.790526
Calculando 5
Fim do Cálculo 5
Calculando 6
Fim do Cálculo 6
Calculando 7
Fim do Cálculo 7
Calculando 8
Fim do Cálculo 8
Fazendo algo... espera = 0.540093
Alô - Tempo decorrido: 5.859909
Calculando 9
Fim do Cálculo 9
Calculando 10
Fim do Cálculo 10
Calculando 11
Fim do Cálculo 11
Calculando 12
Fim do Cálculo 12
Fazendo algo... espera = 0.174978
Alô - Tempo decorrido: 6.830562
Calculando 13
Fim do Cálculo 13
Calculando 14

Veja que o atraso para chamar as outras funções é agora muito mais importante, ultrapassando os 6 segundos entre as chamadas de print_and_repeat e tendo um atraso considerável também no processamento de faz_algo. Bem, este comportamento é esperado, uma vez que a função calcula_algo é o que se chama de CPU bound, ou seja, é uma função que precisa mais da atenção do processador do computador que uma operação de criação de arquivo (I/O bound).

Para utilizar corretamente seu computador, você deve começar a separar seus problemas em CPU bound e I/O bound. No caso de problemas CPU bound, threads oferecem a melhor performance, pois temos vários processadores no mesmo computador. Já para problemas I/O bound, ou seja, que precisam acessar o disco ou a rede (ou entrada de dados vinda do teclado), o loop de eventos assíncrono é mais rápido e fácil de programar. Em problemas mistos, onde temos código CPU bound e código I/O bound a executar, uma solução mista precisa ser aplicada.

Por exemplo, threads são relativamente caros para serem criados e são difíceis de controlar e programar. Nos próximos posts, abordarei outros detalhes do módulo asyncio. Antes de continuarmos com métodos assíncronos, vamos comparar o tempo de execução entre as soluções assíncronas, múltiplos threads e com múltiplos processos. Vamos avaliar versões modificadas da função calcula_algo para cada um dessas formas de paralelização. O problema testado será o tempo de total de execução de 20 chamadas a calcula_algo.

Antes de começarmos, vamos remover a parte aleatória da função e transformar o valor de limite em uma constante. Desta forma as comparações serão mais justas e não dependerão do número obtido por randint().

import asyncio
import time

def calcula_algo(loop, id):
    limite = 40000
    print("Calculando %d" % id)
    z=1
    for x in range(1,limite):
        z*=x
    print("Fim do Cálculo %d" % id)
    if id < 20:
        loop.call_soon(calcula_algo, loop, id+1)
    else:
        loop.stop()

inicio = time.time()
loop = asyncio.get_event_loop()
loop.call_soon(calcula_algo, loop, 1)
loop.run_forever()
fim = time.time()
print("Tempo total: %f s" % (fim-inicio))

Execute o programa e veja que o cálculo foi feito de forma sequencial, ou seja, uma chamada após a outra. Em meu computador o teste executou em aproximadamente 30.37 s. O tempo em seu computador pode e vai variar, pois depende do seu processador e do que sua máquina está fazendo durante os testes.

import time
import threading

def calcula_algo(id):
    limite = 40000
    print("Calculando %d" % id)
    z=1
    for x in range(1,limite):
        z*=x
    print("Fim do Cálculo %d" % id)

inicio = time.time()
ativos = []
for x in range(20):
    t = threading.Thread(target=calcula_algo, args=(x,))
    t.start()
    ativos.append(t)
for t in ativos:
    t.join()
fim = time.time()
print("Tempo total: %f s" % (fim-inicio))

Compare a saída do programa com múltiplos threads com a saída do programa assíncrono. Veja que as chamadas foram iniciadas quase que ao mesmo tempo e que terminaram em uma ordem aleatória. Esta falta de previsibilidade de threads é uma das razões de evitarmos seu uso, especialmente em Python. Devido a uma característica do interpretador Python, que utiliza um lock global, o famoso GIL, programas com múltiplos thread em Python não são eficientes, pois apenas um thread executa de cada vez, o que nos faz voltar ao problema do programa assíncrono adicionado ao tempo de criação e execução dos threads. Em meus testes, este programa teve um desempenho um pouco pior que o programa assíncrono, terminando em 30.67 s. Se você executar novamente este programa, verá que a utilização de CPU não chega nem perto de 100%. Em meu sistema Core i7 que possui 8 cores (4 reais + 4 hyperthreaded), a utilização não passou de 20%.
Vejamos uma solução mais Pythonica, utilizando múltiplos processos e o excelente módulo multiprocessing.

import sys
import time
from multiprocessing import Pool

def calcula_algo(id):
    limite = 40000
    print("Calculando %d" % id)
    z=1
    for x in range(1,limite):
        z*=x
    print("Fim do Cálculo %d" % id)

if __name__ == '__main__':
    nproc = int(sys.argv[1])
    print("Executando com %d processos.")
    inicio = time.time()
    processos = Pool(nproc)
    processos.map(calcula_algo,list(range(20)))
    fim = time.time()
    print("Tempo total: %f s" % (fim-inicio))

Execute o programa várias vezes, passando a cada execução, um dos parâmetros 1, 2, 4, 8 e 20. O parâmetro indica quantos processos teremos em nosso pool. Um pool de processos é um conjunto de processos inicializados pelo módulo multiprocessing. Estes processos ficam disponíveis para o nosso programa e são processos do sistema operacional e não simples threads. Toda a comunicação entre processos é gerenciada pelo módulo multiprocessing. Este módulo é muito interessante, pois realizar este tipo de tarefa em outras linguagens é bem mais complicado. Uma das vantagens do multiprocessing é que cada processo roda seu próprio interpretador Python e assim são capazes de rodar simultaneamente, sem os problemas do GIL que falamos anteriormente. Veja no Gerenciador de Tarefas de seu sistema operacional que múltiplos processos python (ou python.exe no Windows) rodam ao mesmo tempo durante a execução de nosso programa e que agora você deve ter obtido 100% de utilização durante alguns momentos. Veja o resultado da execução de todos os testes em meu computador no gráfico abaixo (passe o mouse sobre as colunas para ver seu valor):



A solução com o multiprocessing melhora ao adicionarmos processos, mas esta melhoria se estabiliza em volta do número de processadores de sua máquina e começa a piorar um pouco depois disso.

O mau desempenho do módulo asyncio é apenas um exemplo de má utilização :-D. Os métodos assíncronos devem ser utilizados com funções que não bloqueiam e que terminam rapidamente. Usar métodos assíncronos com funções CPU bound não pode trazer bons resultados. No entanto, devido aos problemas com o GIL, métodos assíncronos podem simplificar o trabalho de programação e manter a performance de múltiplos threads. Pois a execução sequencial das funções evita a necessidade de sincronizar seus dados. Mesmo com os problemas de GIL, programas em Python que usam threads devem se preparar para execução simultânea de funções, pois a execução salta de um thread a outro durante a execução das funções. Veremos uma outra comparação entre threads e métodos assíncronos em outro posto, usando arquivos.

Isto é só uma pequena amostra do que podemos fazer em Python. Em outro post, abordaremos exemplos mais práticos. O importante é saber a diferença entre a execução assíncrona, com threads e com múltiplos processos. Veremos também como usar como usar um pool de threads em Python e assim combinar as vantagens de threads e métodos assíncronos.

quinta-feira, 9 de maio de 2013

sábado, 26 de janeiro de 2013

Múltiplos monitores com o Remote Desktop

Leia no novo blog

Depois de usar dois ou mais monitores para programar é difícil querer usar apenas um. O Remote Desktop da Microsoft suporta vários monitores, mas normalmente só funciona bem quando a máquina remota e a local possuem 2 monitores.

No trabalho, eu uso um computador com 2 monitores Full HD e um notebook com a resolução de tablet :-D O que torna impossível usar o notebook para programar. Eu normalmente abro uma sessão do Remote Desktop pela manhã e só a fecho na hora de ir embora... muitas vezes não uso nem o teclado do notebook durante o dia, ele se transforma em um disco e CPU remotos !

O  problema é que a máquina local possui 2 monitores e a máquina remota apenas 1. O Remote Desktop usa a resolução máxima de apenas um dos monitores da máquina local, o que deixa um monitor sobrando. Embora o notebook tenha uma tela de baixa resolução, o processador e a memória são muito bons, logo ele é minha máquina de desenvolvimento padrão (Core i7, 8GB). Nada como abrir o Visual Studio 2012 pela manhã e vários outros programas ao lado dele, cada um na sua própria tela.

Para usar o Remote Desktop com os dois monitores locais, é preciso adicionar um parâmetro na linha de comando: /span

Para não esquecer, é melhor mesmo criar um atalho, já com este parâmetro.

mstsc.exe /span

O resultado é um monitor remoto com a resolução total dos dois monitores locais, no meu caso: 2 x 1980 = 3960 pontos ! Funciona também se os dois monitores locais tiverem resolução diferente, mas a quantidade de linhas será a menor entre os dois. No meu caso, acabei com uma tela remota de 3960 x 1080 pontos.

Seria tudo perfeito se algumas operações do Windows não incomodassem. O monitor super largo, estendendo-se por dois monitores físicos, continua sendo um e apenas um grande monitor para a máquina remota. Logo, se maximizarmos uma janela, esta ocupará os dois monitores locais! Isso pode ser resolvido com as áreas de redimensionamento do Windows 7. Para "maximizar" uma janela em apenas um dos monitores, basta arrastar a janela até o canto esquerdo ou direito (mouse todo à esquerda ou à direita, mas na metade da altura da tela). A tela muda de cor, algo como um azul escuro transparente no meu tema. Quando esta troca de cor aparecer, basta soltar o botão e a janela que você estava arrastando muda de tamanho para ocupar a área azul. O resultado é uma janela que ocupa toda a área de um dos monitores.

Um resultado colateral é que a barra de tarefas do Windows passa a ocupar os dois monitores, desta forma, eu tenho o botão de start no monitor da esquerda e o relógio no da direita!

Fica a fica para os fanáticos com 2 monitores!

sexta-feira, 3 de agosto de 2012

Python Computer e Raspberry Pi

Preparação

Segunda (30/07/12), recebi meu primeiro Raspberry Pi, comprado na Element 14. Depois de um mês de espera, recebo na caixa do correio, mas um tanto tarde na Bélgica… 21h. Na verdade, chegou perto do meio dia, mas como não estava em casa, o carteiro deixou como carta normal mesmo.
P1060883
A caixa é bem pequena, como um cartão de crédito. Na realidade, eu encontrei o computador no meio de outras cartas Open-mouthed smile!
O problema é que não tinha nenhuma loja aberta para comprar um cabo ou outro, caso precisasse. O jeito era improvisar.
Eu sabia que ia precisar de um Hub USB com alimentação, pois a USB do Pi não fornece muita energia. Sabendo que o computador estava para chegar, eu comprei um Hub USB de 4 portas, ainda no sábado, aproveitando a visita ao supermercado:
P1060882
E também cartas SD:
P1060878
Pois o Pi só dá boot pela carta SD. Comprei uma carta de 4GB e outra de 8GB, pois uma era classe 4 e a outra classe 10. Aparentemente o Pi tem problemas com cartas classe 10, mas a minha funcionou perfeitamente, mas sem ganho de performance notável.
Antes mesmo de conectar o computador, é preciso baixar o Linux e gravá-lo na carta SD. Instruções detalhadas em inglês são encontradas aqui. Se você usa Windows, precisa baixar o Win32DiskImage. Ele não tem setup, é só descompactar e executar direto. Você precisa ser administrador do computador para gravar a imagem. Eu utilizei o Raspbian, mas outras distribuições também estão disponíveis. A imagem do Raspbian tem uns 450MB e ao descompactá-la você terá o arquivo .img, necessário para o Win32DiskImage.
Gravar a imagem é bem fácil. Introduza o cartão SD no leitor do computador. Normalmente o Windows se oferece para abri-lo. Aproveite a chance para verificar se este está vazio, pois a gravação da imagem vai apagar todo o conteúdo do cartão. Com o Win32DiskImage aberto, selecione o arquivo .img com o Raspbian e ao lado verifique se o drive com a carta SD está correto. Se você tem mais de uma carta SD ou caso apareça mais de uma opção, verifique o que está fazendo, evitando assim perder seus dados. Clique no botão Write e espere a gravação ser finalizada.
Com a carta SD pronta, resta a conexão do Raspberry Pi.
P1060886
Como já estava tarde, montei uma mesa de trabalho no chão da sala, usando uma caixa de monitor e uma folha A4 para dar noção do tamanhão do Raspberry Pi. Sem medir muito, eu chuto que numa folha A4 caberiam 9 Raspberry Pis!
Esta placa contém um SoC (System on a Chip) fabricado pela Broadcom, no caso do modelo B, um BCM2835 com:
  • Processador ARM1176JZF-S (armv6k) rodando a 700 MHz
  • 256 MB de Ram
  • 2 portas USB 2.0
  • 1 porta Ethernet
  • GPU Broadcom VideoCore IV
  • Leitor de carta SD
  • Saída de vídeo HDMI
  • Saída de vídeo composto (conector RCA)
  • Saída de áudio padrão, 3.5 mm
  • Conector de entrada e saída genérico com GPIO/UART/I2C e SPI
  • Dimensões: 85.60 x 53.98 mm
  • Peso: 45g
Um verdadeiro micro sem ventilador e de baixo custo (35€ com frete).
Para conectá-lo à minha TV, emprestei o cabo do vídeo game Open-mouthed smile. Mouse e teclado eu peguei os que uso com meu notebook. Duro foi achar o cabo com o conector Micro B USB. Eu consegui um carregador de celular:
P1060884
Ficou assim:
P1060889
É preciso ter cuidado ao conectar e desconectar o Raspberry Pi. Como a placa não tem gabinete, todas as precauções contra eletricidade estática devem ser tomadas. Não se pode esquecer também de não forçar os conectores e principalmente que o Pi não tem On/Off: ligou a força ele já está ON e dando o boot!
P1060887
Até aqui tudo certo. Mas ao tentar usar o teclado, este não respondia e o Pi chegou a travar. Lembrei então que ele precisa de pelo menos 700mA para funcionar. Checando o carregador de celular que encontrei, vi que ele só fornecia 300 mA. Ele foi então substituído por um cabo USB normal ligado ao carregador de um iPhone. Depois, eu eliminei o carregador e liguei tudo no Hub alimentado. Com a fonte certa, tudo passou a funcionar corretamente.
Ao dar o boot, ou executando sudo raspi-config, você tem a tela do utilitário de configuração simplificada abaixo:
raspconfig
Como meu cartão era de 4 GB, a primeira coisa que fiz foi selecionar expand_rootfs. Esta opção aumenta o disco criado pela restauração da imagem para utilizar todo o cartão. Depois, eu configurei o teclado (configure_keyboard), troquei a senha do usuário pi (change_pass) e o fuso horário (change_timezone). Para sair é só selecionar Finish e dar boot.
O Pi não tem relógio permanente, ele usa um servidor de tempo para ajustar o relógio durante o boot. Configurar o fuso horário é importante por isso.
Esqueci de falar do acesso à Internet. Eu apenas liguei um cabo Ethernet que estava sobrando e o Pi pega um IP usando DHCP. O acesso fica muito fácil e no próximo boot ele já sincroniza o horário, pega um IP e você fica pronto para usar a Internet ou sua rede local. Eu não tenho um cartão WiFi USB, mas se for o caso, na wiki do Raspbery Pi tem a lista dos WiFi compatíveis.
Meu teste era plugar um leitor de DVD externo (USB) e assistir um vídeo antes de dormir. Instalei o VLC, usando o apt-get, mas não consegui ver vídeo algum. Tentei também com arquivos AVI, sem sucesso. Acho que nestes casos o melhor é tentar a OpenElec com o XBMC.
Entrei no X, digitando startx. Baixei o Invasores e funcionou de primeira, com som na TV!
P1060893
Fiquei surpreso ao constatar que o PyGame já vinha instalado. Mesmo um jogo simples como o Invasores teve algumas paradas. Tentei então os jogos pré-instalados e observei o mesmo problema. O X é pesado demais para o Pi.
O Browser é muito simples e não suporta HTML5, muito menos flash. Além disso… quando digo muito lento, não estou exagerando. Lembra os primeiros anos de Windows… mas roda. Instalei o Chromium, versão open source do Google Chrome, mas também é muito lento e pesado. Não podemos esquecer que um browser moderno consome muita memória e que rodar JavaScript exige um processador bom. Nem memória, nem processador sobram no Raspberry Pi para este tipo de coisa. Os drivers ainda estão sendo desenvolvidos e quase tudo roda sem otimização/aceleração da GPU. Novas versões devem melhorar esta situação.

Python Computer

Meu objetivo ao comprar o Raspberry Pi era de configurar um Linux para funcionar como estação de ensino de Python. Como o Pi de Raspberry Pi vem de Python… apareceu a oportunidade. Eu chamei meu projetinho de Python Computer. A ideia era configurar o Linux como um computador de 8 bits que ligava e caia no interpretador Basic. No caso, eu queria um Linux que ligasse e entrasse no interpretador Python. Eu já havia feito um teste usando SilverLight para executar o Python Computer em um browser. Porém, esta solução dependeria de acesso a Internet, versão do browser, plug ins, etc. Ter tudo num pequeno computador me parece mais interessante.
Para isso, o X não é realmente necessário. No Linux, a PyGame pode rodar no framebuffer. Esta configuração me pareceu mais realista que um ambiente gráfico completo no Raspberry Pi.
A primeira coisa que fiz foi mudar o tamanho da fonte de texto, pois a padrão é muito pequena para se ler na TV.
Isso pode ser feito editando o arquivo /etc/default/console-setup como root:

sudo vim /etc/default/console-setup

Na linha que contém FONTFACE escreva:

FONTFACE=”TERMINUS”

e na com FONTSIZE:

FONTSIZE=”16x32”

Salve o arquivo.

Para configurar o vim, crie o arquivo ~python/.vimrc com as seguintes linhas:


syntax on
set ts=4
set expandtab
set shiftwidth=4
set softtabstop=4
set smartindent
set number
set cindent
set nowrap
set go=+b

autocmd BufRead *.py set makeprg=python\ -c\ \"import\ py_compile,sys;\ sys.stderr=sys.stdout;\ py_compile.compile(r'%')\"
autocmd BufRead *.py set efm=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m
autocmd BufRead *.py nmap <F5> :!python %<CR>



Agora crie o usuário python:

sudo useradd –m –s /usr/bin/ipython python

E edite o inittab:

sudo vim /etc/inittab

Modifique a linha do terminal 6, no meu inittab a linha 59. De:

6:23:respawn:/sbin/getty 38400 tty6

para:

6:23:respawn:/sbin/getty 38400 tty6 –a python

Isto fará que com que o ipython seja aberto no terminal 6 durante o boot, sem precisar de login. Faça o boot e digite ALT+F6
ipythonboot
Se quiser apagar estas mensagens do login, edite o arquivo /etc/motd.
Para editar um arquivo sem sair do ipython, digite: edit nomedoarquivo.py
vim_inv
Se o arquivo não existir ele será criado. Ao voltar sair do editor, ele volta para o ipython e executa o arquivo.
O vim não é o editor mais boa pinta do pedaço, mas em modo texto não conheço outro melhor. Para quem estiver saudades dos programas da Borland, instale o joe:

sudo apt-get install joe

e no /etc/profile, adicione:

export EDITOR=/usr/bin/joe

O joe é mais amigável e usa os comandos do Wordstar/Sidekick… e ambientes Turbo da Borland.
Para rodar jogos escritos com a PyGame, modifique a linha que escolhe a quantidade (profundidade) de cores para que 16 bit colors seja escolhido.
No invasores, edite o arquivo video.py, modifique o método modo da classe Video:

self.tela = pygame.display.set_mode(dimensao, 0, 16)

Depois, é só rodar o Invasores no framebufer!
jogo2
jogo
Mesmo alterando a partição da RAM entre a CPU e a GPU para o máximo, ou seja, 128 MB/128 MB, não consegui rodar em resoluções maiores que 1024x768. Lembrando que no X o Invasores roda normalmente.
Se você gosta de usar o modo texto, não deixe de dar uma olhada no screen. Como a tela é larga, eu a dividi em duas partes. Fica muito legal usar metade para rodar algo e a outra para ler a documentação ou editar os fontes.
Uma dica para ligar e desligar o Pi é utilizar uma régua com interruptor:
P1070022
Quanto a Internet, como o Pi fica ao lado do meu notebook, configurei o compartilhamento de Internet do Windows 7 e funcionou direitinho. Desta forma, eu uso o WiFi do notebook para dar acesso ao Pi. Outra vantagem é que posso usar o WinSCP para copiar arquivos entre o notebook e o Pi e mesmo fazer sessões com SSH. Fica a foto da mesa de guerra:
P1070032

Custo

Item
Preço
Raspberry Pi
35€
Hub USB 4 portas
12€
Carta SD 4 GB
5€
Teclado
10€
Mouse
12€
Cabo HDMI
6€
Monitor
150€
Total
230€

Como eu já tinha o monitor, teclado e mouse, não saiu tão caro. Nesta lista ainda falta adicionar 12€ para uma caixinha e mais 6€ para uma fonte com cabo Micro B USB que ainda não chegaram. O hub é opcional se você ligar apenas o teclado e o mouse.

Conclusão

O Pi é muito legal, mas é um brinquedo para desenvolvedores que gostem muito de Linux. Com o tempo, os drivers ficarão mais maduros e a performance deve melhorar. Eu acredito que logo teremos um browser com suporte a HTML5 e um XBMC mais redondo. Por enquanto, usar Python no console já chamou a atenção das crianças aqui de casa, como um modo especial de programação e criação de jogos. O fato de ver um computador aberto com as luzes piscando também chamou a atenção. Outra vantagem do Pi é que não roda Facebook, Twitter ou YouTube. Para  o ensino, gostaria de medir a diferença entre usar um ambiente como o Pi em relação a interfaces ricas como o Windows.

quarta-feira, 1 de agosto de 2012

Por que UTF-8 e não ASCII para o Português? (PARTE II)

Leia no novo blog

Continuação do post, originalmente feita na lista Python-Brasil:

Vou tentar de novo, a thread já falou de 3 coisas diferentes:
1. Codificação a usar em programas Python: por que UTF-8 é altamente recomendável
2. Codificações em geral e problemas causados e resolvidos por ela
3. Um bug do Python no Windows, quando o prompt é configurado para página 65001

Vou tentar explicar para todo mundo, pois é um tópico recorrente.

Mas antes de voltar nestes tópicos, temos que voltar a arquivos.

Tentativa 2:

Para entender a codificação de caracteres, temos que entender do que se trata.
Quem programa conhece o código ASCII, que mapeia cada caractere do alfabeto latino, códigos de controle e alguns símbolos em 7 bits.
É 7 bits, por isso vai de 0 a 127. Isso funcionava bem na década de 60... quando se enxugava bit para salvar tempo de transmissão de dados e armazenamento, muito antes dos torrents e afins :-D. A internacionalização disto ainda não estava em foco, aliás, ASCII significa Código Padrão Americano para Intercâmbio de Informação. Americano, diga-se estado-unidense.

Nossos computadores hoje usam 8 bits por byte, mas nem sempre foi assim. A IBM e depois a Microsoft entre outras empresas aproveitaram o bit extra para completar 8 bits e adicionaram mais 128 caracteres, uma vez que cada bit adicionado dobra a capacidade de representação, potência de 2, etc. Esses caracteres foram usados para representar aqueles caracteres como bordas, símbolos e alguns acentos. Quem usou DOS, lembra bem disso.

Como 256 símbolos não são suficientes para representar todos os caracteres de todas as línguas, a IBM e outros fabricantes, criaram páginas de código específicas para cada país ou língua. Assim, a página de código 437 (cp437) continha símbolos de desenho e caracteres acentuados como o ç e o ñ, usados em  línguas como o francês e o espanhol, que atendem às necessidades da América do Norte e algumas línguas européias. Um exemplo de língua não atendida completamente é o português, pois na página 437 não tem ã nem õ. Esse problema foi resolvido com a página 850, que troca alguns caracteres de desenho e símbolos pouco usados por acentos de várias línguas do ocidente europeu.
Depois de muita história, voltemos a como isso muda nossos bytes.

No código ASCII, a letra A maiúscula é representada pelo número 65 em decimal ou 0x41 em hexadecimal. O B é a letra seguinte, então deram o número 66 ou 0x42.

Se você tem um arquivo com apenas duas letras AB uma após a outra, ele vai ocupar (seus dados) dois bytes no disco. O conteúdo binário dos dados do arquivo em disco são a sequência de bytes 0x41 e 0x42 (AB ou 65 e 66). É muito importante entender esta codificação antes de continuar lendo. Se você não entende que um A é guardado como o número 65, esqueça UTF-8... será preciso reler ou pedir ajuda a um amigo. Antigamente, curso de informática começava com sistema binário e tabela ASCII, hoje o primeiro programa já baixa páginas na Internet... mas a teoria de base é deixada para trás.

Tanto na página 437 quanto na 850, toda a tabela ASCII, ou seja, seus 127 caracteres foram preservados. Assim, nosso arquivo AB é mostrado do mesmo jeito em ambas as páginas. A diferença começa aparecer quando usamos os caracteres diferentes entre elas.

Agora imagine que adicionamos um Ã, usando a página 850, pois escrevemos num computador configurado para português:
ABÃ
No disco teríamos 3 bytes:
0x41 0x42 0xC3
Na página 850, o à é traduzido para o símbolo 199, ou 0xC3 em hexadecimal.
Agora, imagine que enviamos esse arquivo para um amigo americano, que abre num computador que usa a página 437. O conteúdo no disco continua o mesmo: 0x41 0x42 0xC3, mas o que ele vê na tela é:
AB╟
Para onde foi nosso Ã? Para lugar algum... ela estará lá, se usarmos a mesma página de código, ou seja a 850, que usamos para escrever.
Com apenas 3 bytes, já podemos ver o que pode acontecer... agora imagine com arquivos inteiros !
Como os computadores se espalharam pelo mundo, várias páginas foram criadas para o russo, grego, etc. Imagine então escrever um arquivo com partes em grego, partes em russo e chinês... uma catástrofe.

O que uma tabela de codificação faz é mapear um valor numérico para um símbolo gráfico ou caractere. Você escolhe a tabela que quer usar, mas para fazer uma tradução entre tabelas precisa saber qual a tabela usada para codificar os dados atuais e para qual tabela você quer traduzir.
Outro problema é que línguas como o chinês precisam de mais de 256 símbolos para um texto normal, uma vez que o alfabeto deles é muito maior que o nosso. Surgem então tabelas de múltiplos bytes, onde mais de um byte era usado para cada caractere. Ainda assim, você precisava saber qual tabela multibyte foi usada... repetindo a confusão. Quem já trabalhou com Windows em C++ usando MBCS sabe a dor que isso causa...

Uma das soluções para múltiplas linguas é criar um tabelaço que resolveria todos os problemas, foi criado o UNICODE. Desta forma, todas as línguas seriam representadas. O problema é que para conter todos os símbolos, vários bytes teriam que ser utilizados até para caracteres latinos.
Assim, cada letra, numa simplificação seria representada por 2 bytes (simplificação, porque 2 bytes não são suficientes, pois temos mais de 65536 caracteres no Unicode !). Continuando, nosso A em unicode é representado como 0x00 0x41 e o B como 0x00 0x42. É cada letra passa a ser representada por dois bytes e um deles é o temido 0x00 ! O Ã ficou na posição 0x00 0xC3. No disco:
ABÃ
ficaram assim:
0x00 0x41 0x00 0x42 0x00 0xC3

Agora usamos 6 bytes para 3 caracteres. Ainda nem falamos de byte order ou de BOM... isso fica para outro dia :-D

Com 6 bytes para 3 letras, logo apareceram problemas de armazenamento de dados, pois os arquivos começaram a dobrar de tamanho e a tomar 2x mais tempo para serem transmitidos... em teoria. Uma forma mais enxuta de representar estes caracteres foi desenvolvida: o UTF-8.
Usando a mesma tabela base do Unicode, mas introduzindo um esquema de troca de páginas, ABÃ em UTF-8 são escritos no disco como:
0x40 0x41 0xC3 0x83

O Ã foi traduzido como 0xC3 0x83 !
Passamos de 6 para 4 bytes, sem perder a capacidade de escrever em praticamente qualquer língua!

O que acontece no Python. Um arquivo py de apenas uma linha para imprimir ABÃ pode ser escrito como:
print "ABÃ"

No disco ele será gravado se usarmos um editor utf-8 para escrevê-lo:
0x70 0x72 0x69 0x6E 0x74 0x20 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A

São esses bytes que o Python.exe vai ler.
Em UTF-8 estes bytes seriam traduzidos para:
0x70 p
0x72 r
0x69 i
0x6E n
0x74 t
0x20 -> espaço em branco
0x22 "
0x41 A
0x42 B
0xC3 --> primeiro byte do Ã
0x83 --> segundo byte do Ã
0x22 "
0x0D --> CR
0x0A --> LF


Mas o interpretador Python não sabe disso !

C:\Users\nilo>\Python27\python.exe Desktop\test.py
  File "Desktop\test.py", line 1
SyntaxError: Non-ASCII character '\xc3' in file Desktop\test.py on line 1, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details

Ele diz: Non-ASCII e depois \xc3 que é outra forma de dizer 0xC3
Por que? No Python 2, o arquivo foi lido como ASCII, tendo apenas  símbolos de 0 a 127. 0xC3 é 199, ou seja, fora da tabela ASCII, daí o erro.
Neste caso, para resolver temos que colocar o # coding: utf-8
O programa fica assim:

# coding: utf-8
print "ABÃ"

Que em hexa é:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A                print "ABÃ"


Veja que a segunda linha continua exatamente a mesma coisa.
Mas ao executar-mos temos:

C:\Users\nilo>\Python27\python.exe Desktop\test.py
ABÃ


Não deu erro, mas também não imprimiu o que queríamos. Vejamos o que deu errado.
Primeiro a página de código do meu console:
C:\Users\nilo>chcp
Active code page: 850


Ué... mas a página 850 suporta o Ã. Por que o Python não imprimiu corretamente?
Simplesmente porque o cabeçalho # coding: utf-8 apenas indica em que codificação você escreveu o programa. Isso faz com ele consiga ler seu código, mesmo com acentos, desde que você tenha também usado um editor de textos em UTF-8, como diz o cabeçalho. Se você usar um cabeçalho diferente da real codificação do arquivo, os bytes em disco não vão mudar e os caracteres serão traduzidos usando tabelas incorretas. Isso é muito difícil de perceber apenas olhando, por isso eu recomendo o editor Hex. Com o tempo fica claro de fazer até no PsPad.

Ainda temos que resolver o problema da saída. No Python 2, as strings não são traduzidas de uma tabela para outra. Esta ambiguidade foi corrigida, no Python 3, com o tipo byte... mas ai já é outra história. Se você quer que o Python traduza de uma tabela para outra, use o prefixo u na string, de forma a indicar que é uma string unicode, codificada no formato utf-8, como dito no cabeçalho do programa.
Como strings comuns não tem tradução de página automaticamente, a sequência 0xc3 0x83 é mostrada na tela pela tabela da cp 850, que utiliza apenas um byte por caractere. Logo, dois bytes, dois caracteres. Um para o 0xc3 e outro para 0x83.

Vejamos o programa com o u antes das aspas:
# coding: utf-8
print u"ABÃ"

No disco:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x75 0x22 0x41 0x42 0xC3 0x83 0x22 0x0D 0x0A           print u"ABÃ"

Veja que a única diferença é 0x75 (a letra u), mas o resultado é diferente:

C:\Users\nilo>\Python27\python.exe Desktop\test.py
ABÃ

Agora saiu corretamente! Por que?  Porque o Python sabe que a string é unicode e que a saída do console no meu Windows usa a cp850. Então ele converte os bytes durante a impressão para que sejam apresentados corretamente.

Por isso é importante entender a codificação do seu arquivo e a codificação do console, banco de dados, etc. Você precisa ajudar o programa a se comportar bem.

Vejamos agora o erro do cabeçalho inválido, onde declaramos UTF-8, mas nosso editor grava usando a cp1252 do Windows:
Visualmente o arquivo tem o mesmo conteúdo:
# coding: utf-8
print u"ABÃ"
Mas no disco:
0x23 0x20 0x63 0x6F 0x64 0x69 0x6E 0x67 0x3A 0x20 0x75 0x74 0x66 0x2D 0x38 0x0D 0x0A # coding: utf-8
0x70 0x72 0x69 0x6E 0x74 0x20 0x75 0x22 0x41 0x42 0xC3 0x22 0x0D 0x0A
                print u"ABÃ"

Resulta em:
C:\Users\nilo>\Python27\python.exe Desktop\test.py
  File "Desktop\test.py", line 2
    print u"ABÃ"
SyntaxError: (unicode error) 'utf8' codec can't decode byte 0xc3 in position 0:
unexpected end of data


Por que? Bem, se você comparar a segunda linha em hexadecimal com a do exemplo anterior, verá que na cp1252, o à foi traduzido como 0xC3, ou seja, apenas um byte. Mas declaramos no cabeçalho que estaríamos usando UTF-8! O interpretador Python é um programa e confia no que declaramos. Ele lê o arquivo como se fosse UTF-8 e acha o 0xC3 que não é apenas um caractere, mas o marcador de início de troca de página. Depois de ler o 0xC3 ele espera o outro byte desta página, mas acha as aspas (0x22). 0xC3 0x22 é uma sequência inválida em UTF-8 e o interpretador explode com uma exceção de codificação.

Voltando ao início do tópico:
1. Codificação a usar em programas Python: por que UTF-8 é altamente recomendável
Por que você pode enviar seus programas para outros computadores (linux, mac, windows) e usar acentos, evitando problemas futuros. Mas só funciona se seu cabeçalho expressar a codificação real usado no arquivo. Caso contrário não funciona.

2. Codificações em geral e problemas causados e resolvidos por ela
Acho que o início da mensagem responde essa.

3. Um bug do Python no Windows, quando o prompt é configurado para página 65001
Além das páginas da IBM, a Microsoft tem também as suas. Entre elas a cp1252 e a cp 65001 para o UTF8. Se você configurar e se somente se você configurar seu console para usar a página 650001, utf-8, o resultado é o seguinte:

C:\Users\nilo>chcp 65001
Active code page: 65001

C:\Users\nilo>\Python27\python.exe Desktop\test.py
Traceback (most recent call last):
  File "Desktop\test.py", line 2, in
    print u"ABÃ"
LookupError: unknown encoding: cp65001

C:\Users\nilo>\Python32\python.exe Desktop\test.py
Fatal Python error: Py_Initialize: can't initialize sys standard streams
LookupError: unknown encoding: cp65001

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

É só neste caso, bem específico e desnecessário para o português que temos um bug aberto ainda no Python 3.3.
Não é um bug do Windows, pois funciona em Java, C# e C. É apenas a forma que o interpretador trata a cp65001 diferente de utf8.