Por que armazenar uma função dentro de um dicionário python?

68

Sou iniciante em python e acabei de aprender uma técnica envolvendo dicionários e funções. A sintaxe é fácil e parece uma coisa trivial, mas meus sentidos em python estão formigando. Algo me diz que esse é um conceito profundo e muito pitônico e não estou percebendo sua importância. Alguém pode colocar um nome nessa técnica e explicar como / por que é útil?


A técnica é quando você tem um dicionário python e uma função que pretende usar nele. Você insere um elemento extra no dict, cujo valor é o nome da função. Quando você estiver pronto para chamar a função, emita-a indiretamente consultando o elemento dict, não a função pelo nome.

O exemplo do qual estou trabalhando é de Learn Python the Hard Way, 2nd Ed. (Esta é a versão disponível quando você se inscreve no Udemy.com ; infelizmente, a versão HTML gratuita ao vivo é atualmente o Ed 3 e não inclui mais este exemplo).

Parafrasear:

# make a dictionary of US states and major cities
cities = {'San Diego':'CA', 'New York':'NY', 'Detroit':'MI'}

# define a function to use on such a dictionary
def find_city (map, city):
    # does something, returns some value
    if city in map:
        return map[city]
    else:
        return "Not found"

# then add a final dict element that refers to the function
cities['_found'] = find_city

Em seguida, as seguintes expressões são equivalentes. Você pode chamar a função diretamente ou referenciando o elemento dict cujo valor é a função.

>>> find_city (cities, 'New York')
NY

>>> cities['_found'](cities, 'New York')
NY

Alguém pode explicar qual é o recurso de linguagem e talvez onde ele toca na programação "real"? Esse exercício de brinquedo foi suficiente para me ensinar a sintaxe, mas não me levou até lá.

mdeutschmtl
fonte
13
Por que este post seria fora de tópico? É uma ótima pergunta sobre o conceito de algoritmo e estrutura de dados!
Martijn Pieters
Eu já vi (e fiz) algumas coisas como essas em outros idiomas. Você poderia vê-lo como uma instrução switch, mas agrupar-se bem em um objeto passável com tempo de pesquisa O (1).
KChaloux
11
Eu tinha um palpite de que havia algo importante e auto-referencial em incluir a função dentro de seu próprio ditado ... veja a resposta de @ dietbuddha ... mas talvez não?
Mdeutschmtl

Respostas:

83

Usando um dict, você pode traduzir a chave em uma chamada. A chave não precisa ser codificada, como no seu exemplo.

Geralmente, essa é uma forma de envio de chamadas, em que você usa o valor de uma variável para se conectar a uma função. Digamos que um processo de rede envie códigos de comando, um mapeamento de despacho permite traduzir facilmente os códigos de comando em código executável:

def do_ping(self, arg):
    return 'Pong, {0}!'.format(arg)

def do_ls(self, arg):
    return '\n'.join(os.listdir(arg))

dispatch = {
    'ping': do_ping,
    'ls': do_ls,
}

def process_network_command(command, arg):
    send(dispatch[command](arg))

Observe que a função que chamamos agora depende inteiramente de qual é o valor command. A chave também não precisa corresponder; nem precisa ser uma string, você pode usar qualquer coisa que possa ser usada como chave e se adequar ao seu aplicativo específico.

O uso de um método de despacho é mais seguro do que outras técnicas, como eval(), pois limita os comandos permitidos ao que você definiu anteriormente. Nenhum invasor furtará uma ls)"; DROP TABLE Students; --injeção por uma tabela de despacho, por exemplo.

Martijn Pieters
fonte
5
@ Martjin - isso não poderia ser chamado de implementação do 'padrão de comando' nesse caso? Parece que esse é o conceito que o OP está tentando entender?
PhD
3
@ PhD: Sim, o exemplo que eu construí é uma implementação de Padrão de Comando; os dictatos como o despachante (gerente de comando, invocador, etc.).
Martijn Pieters
Ótima explicação de nível superior, @Martijn, obrigado. Eu acho que entendi a idéia de "despacho".
Mdeutschmtl
28

@Martijn Pieters fez um bom trabalho ao explicar a técnica, mas queria esclarecer algo da sua pergunta.

O importante é saber que você NÃO está armazenando "o nome da função" no dicionário. Você está armazenando uma referência à própria função. Você pode ver isso usando um printna função.

>>> def f():
...   print 1
... 
>>> print f
<function f at 0xb721c1b4>

fé apenas uma variável que referencia a função que você definiu. O uso de um dicionário permite agrupar coisas semelhantes, mas não é diferente de atribuir uma função a uma variável diferente.

>>> a = f
>>> a
<function f at 0xb721c3ac>
>>> a()
1

Da mesma forma, você pode passar uma função como argumento.

>>> def c(func):
...   func()
... 
>>> c(f)
1
unholysampler
fonte
5
Mencionando função de primeira classe iria ajudar :-)
Florian Margaine
7

Observe que a classe Python é realmente apenas um açúcar de sintaxe para o dicionário. Quando você faz:

class Foo(object):
    def find_city(self, city):
        ...

quando Você ligar

f = Foo()
f.find_city('bar')

é realmente o mesmo que:

getattr(f, 'find_city')('bar')

que, após a resolução do nome, é igual a:

f.__class__.__dict__['find_city'](f, 'bar')

Uma técnica útil é mapear a entrada do usuário para retornos de chamada. Por exemplo:

def cb1(...): 
    ...
funcs = {
    'cb1': cb1,
    ...
}
while True:
    input = raw_input()
    funcs[input]()

Como alternativa, isso pode ser escrito na classe:

class Funcs(object):
    def cb1(self, a): 
        ...
funcs = Funcs()
while True:
    input = raw_input()
    getattr(funcs, input)()

a sintaxe de retorno de chamada melhor depende do aplicativo específico e do gosto do programador. O primeiro é um estilo mais funcional, o segundo é mais orientado a objetos. O primeiro pode parecer mais natural se você precisar modificar as entradas no dicionário de funções dinamicamente (talvez com base na entrada do usuário); o último pode parecer mais natural se você tiver um conjunto de mapeamentos de predefinições diferentes que podem ser escolhidos dinamicamente.

Lie Ryan
fonte
Eu acho que essa intercambiabilidade é o que me fez pensar "pitônico", o fato de que o que você vê na superfície é apenas uma maneira convencional de apresentar algo muito mais profundo. Talvez isso não seja específico do python, embora os exercícios python (e programadores python?) Pareçam falar muito sobre os recursos da linguagem dessa maneira.
Mdeutschmtl
Outro pensamento, existe algo específico para o python na maneira como ele está disposto a avaliar o que parecem dois "termos" apenas sentados um ao lado do outro, a referência de ditado e a lista de argumentos? Outros idiomas permitem isso? É uma espécie de equivalente de programação do salto na álgebra de 5 * xpara 5x(perdoe a analogia simples).
Mdeutschmtl
@mdeutschmtl: não é realmente exclusivo do Python, embora os idiomas que não possuem função de primeira classe ou objeto de função nunca possam ter situações em que um acesso ao dicionário imediatamente seguido pela chamada da função possa ser possível.
Lie Ryan
2
@mdeutschmtl "o fato de que o que você vê na superfície é apenas uma maneira convencional de apresentar algo muito mais profundo". - isso é chamado de açúcar sintático e existe em todo o lugar
Izkata
6

Existem duas técnicas que vêm à minha mente às quais você pode estar se referindo, nenhuma das quais é pitônica, pois são mais amplas que uma língua.

1. A técnica de ocultação / encapsulamento e coesão de informações (elas geralmente andam de mãos dadas, então eu as estou juntando).

Você tem um objeto que possui dados e anexa um método (comportamento) que é altamente coeso com os dados. Se você precisar alterar a função, estender a funcionalidade ou fazer outras alterações, os chamadores não precisarão alterar (supondo que nenhum dado adicional precise ser passado).

2. Tabelas de expedição

Não é o caso clássico, porque há apenas uma entrada com uma função. No entanto, as tabelas de expedição são usadas para organizar comportamentos diferentes por uma chave, para que possam ser consultadas e chamadas dinamicamente. Não tenho certeza se você pensa nisso, pois não se refere à função de maneira dinâmica, mas mesmo assim você ainda ganha uma ligação tardia eficaz (a chamada "indireta").

Tradeoffs

Uma coisa a observar é que o que você fará funcionará bem com um namespace conhecido de chaves. No entanto, você corre o risco de colisão entre os dados e as funções com um espaço de nome desconhecido das chaves.

dietbuddha
fonte
Encapsulamento porque os dados e a função armazenada no ditado (ionário) estão relacionados e, portanto, têm coesão. Os dados e a função são de dois domínios muito diferentes; portanto, à primeira vista, o exemplo parece agrupar entidades muito diferentes.
ChuckCottrill
0

Publico esta solução que considero bastante genérica e que pode ser útil, pois é simples e fácil de adaptar a casos específicos:

def p(what):
    print 'playing', cmpsr

def l(what):
    print 'listening', cmpsr

actions = {'Play' : p, 'Listen' : l}

act = 'Listen'
cmpsr = 'Vivaldi'

actions[act].__call__(cmpsr)

também é possível definir uma lista em que cada elemento é um objeto de função e usar o __call__método incorporado. Créditos a todos pela inspiração e cooperação.

"O grande artista é o simplificador", Henri Frederic Amiel

Emanuele
fonte