Back-End

27 jul, 2015

Implementando multimétodos em Python

Publicidade

Em Clojure (e em algumas outras linguagens), um multimétodo é uma implementação do despacho múltiplo como uma alternativa ao despacho único.

Tradicionalmente, se você definir vários métodos com o mesmo nome em diferentes classes, o tipo/classe do primeiro argumento (em Python, self) é usado para escolher qual método devemos chamar. Isso é chamado de “despacho único” porque a decisão de qual método será chamado é deixada para o tipo inferido de um único argumento.

Os multimétodos usam a abordagem de enviar o despacho até o usuário; você pode despachar em qualquer valor, sendo necessário somente fornecer uma função que retorne o valor sobre o qual você deseja enviar, e um método para cada valor possível. Para certos casos, isso é muito mais flexível do que o despacho único.

Usos para multimétodos

Pode ser um pouco difícil de encontrar bons exemplos de uso dos multimétodos; os casos de uso só tendem a surgir quando você está escrevendo código de mesmo tipo com lotes de ifs aninhados, ou está escrevendo funções com nomes como get_age_human_years e get_age_dog_years. Toda vez que você estiver fazendo coisas diferentes dependendo de alguma heurística de alguns dados de entrada, você pode ser um bom candidato para utilizar um multimétodo.

Outro caso de uso é trancar a funcionalidade para classes existentes (talvez unificar a API para alguma classe biblioteca com um local). Monkey-patch é considerado um estilo ruim em Python, então essa pode ser uma solução para isso.

from requests import Request

class MockRequest(object):
    def send(self):
        pass  # Or whatever

@multi
def send_request(req):
    return type(req)

@method(send_request, Request)
def send_request(req):
    req.prepare().send(
        stream=stream,
        verify=verify,
        proxies=proxies,
        cert=cert,
        timeout=timeout
    )

@method(send_request, MockRequest)
def send_request(req):
    req.send()

Isso economiza um pouco de nidificação e, mais importante, separa o código de produção real do código de teste. Você pode definir os mesmos métodos em arquivos diferentes (embora isso provavelmente criaria um monkey-patching por definição).

Outro bom caso de uso é seguir meu eminente conselho de usar plain-old-dicts em vez de classes, sempre que possível. Neste caso, você pode querer despachar em algumas chaves desses dicionários. Aqui está um exemplo que criei para meu diretório Projetos. Eu o escrevi para extrair o conteúdo de alguns arquivos XML analisados, e obter os dados que podem estar em vários formatos:

(defmulti get-items :tag)

(defmethod get-items :rss [doc]
  (->>
    doc
    :content
    first
    :content
    (filter-tag :item)
    (map :content)))


(defmethod get-items :feed [doc]
  (->>
    doc
    :content
    (filter-tag :entry)
    (map :content)))

(defmethod get-items :default [_] [])

Para mais alguns exemplos, você pode ler o trabalho de Julia Stefan Karpinski sobre o assunto (Julia tem uma grande implementação criada usando multimétodos).

Multimétodos em Python

Eu não sou o primeiro cara a fazer isso. Guido postou sua versão em 2005, também usando decorators. No entanto, ele decidiu restringir o despacho para tipos simples, proporcionando a função de despacho livre, mas limitada ao mesmo tempo. Em qualquer caso, estou contente de que a técnica geral tenha sido aprovada por Guido.

Minha solução visa usar um sistema inspirado nos multimétodos do Clojure, permitindo que o desenvolvedor forneça uma função de despacho (que poderia ser apenas map (type, args) para imitar o exemplo de Guido.

Com algumas modificações, tenho sido capaz de chegar a uma versão Python melhorada usando decorators que se parecem com isto:

@multi
def area(shape):
    return shape.get('type')

@method(area, 'square')
def area(square):
    return square['width'] * square['height']

@method(area, 'circle')
def area(circle):
    return circle['radius'] ** 2 * 3.14159

@method(area)
def area(unknown_shape):
    raise Exception("Can't calculate the area of this shape")

area({'type': 'circle', 'radius': 0.5})  # => 0.7853975
area({'type': 'square', 'width': 1, 'height': 1})  # => 1
area({'type': 'rhombus'})  # => Throws exception

Aqui está como isso funcionaria em Clojure:

(defmulti area (fn [shape] (get shape :type)))

(defmethod area :rectangle [square]
  (* (get square :width) (get square :width)))

(defmethod area :circle [circle]
  (* 3.14159 (get circle :radius) (get circle :radius)))

(defmethod area :default [shape]
  (throw (Exception. "Can't calculate the area of this shape)))

Como funciona

Como na versão de Guido (embora eu a tenha redescoberto de forma independente), o decorator multi adiciona uma propriedade __multi__ ao próprio objeto de função. Isto é simplesmente um dicionário Python simples:

def multi(dispatch_fn):
    def _inner(*args, **kwargs):
        return _inner.__multi__.get(
            dispatch_fn(*args, **kwargs),
            _inner.__multi_default__
        )(*args, **kwargs)

    _inner.__multi__ = {}
    _inner.__multi_default__ = lambda *args, **kwargs: None  # Default default
    return _inner

Ao adicionar um método, você passa a função de despacho existente para o decorator. A função encapsulada se adiciona ao dicionário __multi__ da função de despacho, e a função de despacho é retornada para que não seja sobrescrita.

def method(dispatch_fn, dispatch_key=None):
    def apply_decorator(fn):
        if dispatch_key is None:
            # Default case
            dispatch_fn.__multi_default__ = fn
        else:
            dispatch_fn.__multi__[dispatch_key] = fn
        return dispatch_fn
    return apply_decorator

Simples! Eu estava pensando em empacotá-la como uma biblioteca, mas não parece apropriado para um script com apena 20 linhas de código. Sinta-se livre para usá-lo como desejar! Você pode encontrar todo o código e alguns exemplos neste repositório Gist.

Alguém tem mais exemplos nos quais multimétodos fazem uma grande diferença?

***

Adam Bard faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: http://adambard.com/blog/implementing-multimethods-in-python/