Páginas

Saturday, March 21, 2009

Python tricks: locals(), globals() e keyword arguments (kwargs)

Nessa semana durante uma sessão de pair programming lá na globo.com chegamos a um código que começava a se repetir... um bom momento para melhora, e lá introduzi uma "técnica" muito legal: usar o um dicionário que contém o escopo local para dinamizar o acesso a variáveis/nomes.

Esse dicionário já existe builtin no Python, apesar de eu acreditar que muita gente não saiba ou não o use...
Trata-se do locals() (e seu irmão globals()).

Uso:
def preencher_mensagem(id_mensagem, titulo=None, subtitulo=None, link=None):
id_titulo = MENSAGENS[id_mensagem]['titulo']
id_subtitulo = MENSAGENS[id_mensagem]['subtitulo']
id_link = MENSAGENS[id_mensagem]['link']

if titulo is not None:
preencher_titulo(id_titulo, titulo)

if subtitulo is not None:
preencher_subtitulo(id_subtitulo, subtitulo)

if link is not None:
preencher_link(id_link, link)


Abstraim o resto do código, e pensem nos if's.
Com mais e mais parametros para preencher, isso fica muito repetitivo.
Por que não um loop?
def preencher_mensagem(id_mensagem, titulo=None, subtitulo=None, link=None):
id_titulo = MENSAGENS[id_mensagem]['titulo']
id_subtitulo = MENSAGENS[id_mensagem]['subtitulo']
id_link = MENSAGENS[id_mensagem]['link']

for parte in 'titulo subtitulo link'.split():
texto = locals()[parte]
id = locals()['id_%s' % parte]
preencher = globals()['preencher_%s' % parte]

if texto is not None:
preencher(id, texto)

Notem que com o locals() podemos acessar o dicionário de nomes locais e usar seus valores tanto para leitura quanto para escrita (não recomendada), chamar métodos, etc. O equivalente para o escopo global é o globals().
Ainda poderíamos fazer melhor e usar um dicionário dos parâmetros passados para a função/método, ao invés de 'apelar' para o escopo local.
Basta colocar um argumento que leve ** na frente, e ele será um dicionário de todos os parâmetros passados por nome. Melhor definição, formalismo e mais exemplos na documentação oficial do Python.
def preencher_mensagem(id_mensagem, **partes):
for parte, texto in partes.iteritems():
id = MENSAGENS[id_mensagem][parte]
preencher = globals().get('preencher_%s' % parte)

if preencher is not None:
preencher(id, texto)

Agora, para os que querem rodar alguma coisa que funcione, fiz um script completo que pode ser executado.
Fiz um "banco de dados" fictício só para fins de demonstração.

# -*- coding: utf-8 -*-
# Exemplo usado no meu blog em lifeatmymind.blogspot.com
# Rodolfo Carvalho 2009-03-21

#----- Meu "Banco de Dados" ------------------
MENSAGENS = {1: {'titulo': 4,
'subtitulo': 3,
'link': 1},
2: {'titulo': 1,
'subtitulo': 4,
'link': 2},
3: {'titulo': 2,
'subtitulo': 2,
'link': 4},
4: {'titulo': 3,
'subtitulo': 1,
'link': 3}}

TITULOS = {1: '', 2: '', 3: '', 4: ''}
SUBTITULOS = {1: '', 2: '', 3: '', 4: ''}
LINKS = {1: '', 2: '', 3: '', 4: ''}
#---------------------------------------------

def preencher_titulo(id_titulo, titulo):
TITULOS[id_titulo] = titulo

def preencher_subtitulo(id_subtitulo, subtitulo):
SUBTITULOS[id_subtitulo] = subtitulo

def preencher_link(id_link, link):
LINKS[id_link] = link

#---------------------------------------------

def preencher_mensagem1(id_mensagem, titulo=None, subtitulo=None, link=None):
id_titulo = MENSAGENS[id_mensagem]['titulo']
id_subtitulo = MENSAGENS[id_mensagem]['subtitulo']
id_link = MENSAGENS[id_mensagem]['link']

if titulo is not None:
preencher_titulo(id_titulo, titulo)

if subtitulo is not None:
preencher_subtitulo(id_subtitulo, subtitulo)

if link is not None:
preencher_link(id_link, link)

def preencher_mensagem2(id_mensagem, titulo=None, subtitulo=None, link=None):
id_titulo = MENSAGENS[id_mensagem]['titulo']
id_subtitulo = MENSAGENS[id_mensagem]['subtitulo']
id_link = MENSAGENS[id_mensagem]['link']

for parte in 'titulo subtitulo link'.split():
texto = locals()[parte]
id = locals()['id_%s' % parte]
preencher = globals()['preencher_%s' % parte]

if texto is not None:
preencher(id, texto)

def preencher_mensagem3(id_mensagem, **partes):
for parte, texto in partes.iteritems():
id = MENSAGENS[id_mensagem][parte]
preencher = globals().get('preencher_%s' % parte)

if preencher is not None:
preencher(id, texto)

#---------------------------------------------

def imprimir_mensagens():
def print_linha(conteudo=''):
print '| %s |' % conteudo.center(76)

for id, msg in MENSAGENS.iteritems():
print
print '(%d)' % (id,)
print '-' * 80
print_linha('* %s *' % TITULOS[msg['titulo']])
print_linha('%s' % SUBTITULOS[msg['subtitulo']])
print_linha()
print_linha('%s' % LINKS[msg['link']])
print '-' * 80

if __name__ == '__main__':
preencher_mensagem = preencher_mensagem3

preencher_mensagem(id_mensagem=1,
titulo=u'Olá mundo!',
subtitulo=u'Veja como é divertido usar Python',
link='http://lifeatmymind.blogspot.com')
preencher_mensagem(id_mensagem=2,
titulo=u'Esta é a segunda mensagem cadastrada',
link='http://lifeatmymind.blogspot.com')
preencher_mensagem(id_mensagem=3,
subtitulo=u'Eu não tenho título...',
link='http://lifeatmymind.blogspot.com')
preencher_mensagem(id_mensagem=4,
titulo=u'Última mensagem',
subtitulo='Sou uma mensagem sem link')
imprimir_mensagens()



Um outro exemplo de uso interessante seria:

def tell_story(king, princess, action):
print ('There was a King called %(king)s that had a beautiful daughter. '
'Her name, %(princess)s, would have to be shout in order to make '
'her %(action)s.' % locals())

tell_story("Peter", "Fiona", "ride a horse")
tell_story("Allan", "Britney", "dance")


No trecho acima tem duas coisas interessantes:
  1. Você pode escrever strings grandes sem poluir seu código com linhas super extensas. Siga a recomendação de manter no máximo 80 caracteres por linha. Para escrever strings longas, use-se do artifício da continuação de linha implícita por causa dos parênteses e da concatenação automática de strings postas lado a lado.
>>> "Eu sou uma string" " que continua em outra parte"
'Eu sou uma string que continua em outra parte'

>>> ("Eu sou uma string" " que continua em outra parte"
... " e tambem em outra linha!")
'Eu sou uma string que continua em outra parte e tambem em outra linha!'

  1. Você pode usar o locals() como dicionário para formatação de strings!

O código original é equivalente a:
def tell_story(king, princess, action):
print ('There was a King called %(king)s that had a beautiful daughter. '
'Her name, %(princess)s, would have to be shout in order to make '
'her %(action)s.' % dict(king=king, princess=princess, action=action))

tell_story("Peter", "Fiona", "ride a horse")
tell_story("Allan", "Britney", "dance")

Porém, o original é bem mais sucinto :)

No comments: