[Rodolfo Carvalho] 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 :)