__lt__ em vez de __cmp__

100

O Python 2.x possui duas maneiras de sobrecarregar os operadores de comparação __cmp__ou os "operadores de comparação avançados", como __lt__. Diz-se que as sobrecargas de comparação avançada são as preferidas, mas por que isso acontece?

Os operadores de comparação avançados são mais simples de implementar cada um, mas você deve implementar vários deles com lógica quase idêntica. No entanto, se você pode usar a cmpordenação interna e de tupla, __cmp__torna - se bastante simples e preenche todas as comparações:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

Essa simplicidade parece atender às minhas necessidades muito melhor do que sobrecarregar todas as 6 (!) Comparações ricas. (No entanto, você pode baixá-lo para "apenas" 4 se confiar no "argumento trocado" / comportamento refletido, mas isso resulta em um aumento líquido de complicações, na minha humilde opinião.)

Existem armadilhas imprevistas das quais preciso estar ciente se apenas sobrecarregar __cmp__?

Eu entendo a <, <=, ==, etc. operadores podem ser sobrecarregados para outros fins, e pode retornar qualquer objeto que eles gostam. Não estou perguntando sobre os méritos dessa abordagem, mas apenas sobre as diferenças ao usar esses operadores para comparações no mesmo sentido que significam para números.

Atualização: Como Christopher apontou , cmpestá desaparecendo no 3.x. Existem alternativas que tornam a implementação de comparações tão fácil quanto as anteriores __cmp__?

Comunidade
fonte
5
Veja minha resposta com relação à sua última pergunta, mas na verdade há um design que tornaria as coisas ainda mais fáceis para muitas classes, incluindo a sua (agora você precisa de um mixin, metaclasse ou decorador de classe para aplicá-lo): se um método especial chave estiver presente, ele deve retornar uma tupla de valores, e todos os comparadores AND hash são definidos em termos dessa tupla. O Guido gostou da minha ideia quando a expliquei, mas depois me ocupei com outras coisas e nunca cheguei a escrever um PEP ... talvez por 3.2 ;-). Enquanto isso, continuo usando meu mixin para isso! -)
Alex Martelli

Respostas:

90

Sim, é fácil implementar tudo em termos de, por exemplo, __lt__uma classe mixin (ou uma metaclasse, ou um decorador de classe, se o seu gosto funcionar dessa forma).

Por exemplo:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

Agora sua classe pode definir justamente __lt__e multiplicar herdar de ComparableMixin (depois de quaisquer outras bases de que precise, se houver). Um decorador de classe seria bastante semelhante, apenas inserindo funções semelhantes como atributos da nova classe que está decorando (o resultado pode ser microscopicamente mais rápido em tempo de execução, a um custo igualmente minuto em termos de memória).

Claro, se sua classe tem uma maneira particularmente rápida de implementar (por exemplo) __eq__e __ne__deve defini-los diretamente para que as versões do mixin não sejam usadas (por exemplo, é o caso de dict) - na verdade, __ne__pode ser bem definido para facilitar isso como:

def __ne__(self, other):
  return not self == other

mas no código acima eu queria manter a simetria agradável de usar apenas <;-). Quanto ao porquê __cmp__tinha que ir, desde que nós fizemos têm __lt__e amigos, por que manter outra, maneira diferente de fazer exatamente a mesma coisa ao redor? É muito peso morto em cada tempo de execução do Python (Classic, Jython, IronPython, PyPy, ...). O código que definitivamente não terá bugs é o código que não está lá - daí o princípio do Python de que deve haver idealmente uma maneira óbvia de executar uma tarefa (C tem o mesmo princípio na seção "Espírito de C" do o padrão ISO, aliás).

Isso não significa que vamos sair do nosso caminho para proibir as coisas (por exemplo, quase equivalência entre mixins e decoradores de classe para alguns usos), mas definitivamente não significa que nós não gostam de transportar cerca de código nos compiladores e / ou tempos de execução que existem de forma redundante apenas para oferecer suporte a várias abordagens equivalentes para executar exatamente a mesma tarefa.

Edição adicional: na verdade, há uma maneira ainda melhor de fornecer comparação E hash para muitas classes, incluindo aquele na questão - um __key__método, como mencionei em meu comentário à questão. Já que nunca tive a oportunidade de escrever o PEP para ele, você deve implementá-lo atualmente com um Mixin (& c) se desejar:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

É um caso muito comum para as comparações de uma instância com outras instâncias se resumirem à comparação de uma tupla para cada uma com alguns campos - e então, o hash deve ser implementado exatamente da mesma forma. O __key__método especial aborda essa necessidade diretamente.

Alex Martelli
fonte
Desculpe pela demora @R. Pate, decidi que, como estava tendo que editar de qualquer maneira, deveria fornecer a resposta mais completa possível, em vez de me apressar (e acabei de editar novamente para sugerir minha velha ideia- chave que nunca cheguei ao PEPping, e também como para implementá-lo com um mixin).
Alex Martelli
Eu realmente gosto dessa ideia- chave , vou usá-la e ver como é. (Embora denominado cmp_key ou _cmp_key em vez de um nome reservado.)
TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixinquando tento isso em Python 3. Veja o código completo em gist.github.com/2696496
Adam
2
No Python 2.7 + / 3.2 +, você pode usar em functools.total_orderingvez de construir o seu próprio ComparableMixim. Como sugerido na resposta de jmagnusson
Dia
4
Usar <para implementar __eq__em Python 3 é uma ideia muito ruim, por causa de TypeError: unorderable types.
Antti Haapala
49

Para simplificar este caso, há um decorador de classe no Python 2.7 + / 3.2 +, functools.total_ordering , que pode ser usado para implementar o que Alex sugere. Exemplo dos documentos:

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
Magnusson
fonte
9
total_orderingnão implementa __ne__, por isso esteja atento!
Flimm
3
@Flimm, não, mas __ne__. mas isso é porque __ne__tem implementação padrão que delega para __eq__. Portanto, não há nada a ser observado aqui.
Jan Hudec
deve definir pelo menos uma operação de pedido: <> <=> = .... eq não é necessário como pedido total se! a <b e b <a então a = b
Xanlantos
9

Isso é coberto pelo PEP 207 - Comparações ricas

Além disso, __cmp__desaparece no python 3.0. (Observe que não está presente em http://docs.python.org/3.0/reference/datamodel.html, mas ESTÁ em http://docs.python.org/2.7/reference/datamodel.html )

Christopher
fonte
O PEP está preocupado apenas com o motivo pelo qual as comparações ricas são necessárias, da maneira como os usuários do NumPy desejam que A <B retorne uma sequência.
Eu não tinha percebido que definitivamente ia embora, isso me deixa triste. (Mas obrigado por apontar isso.)
O PEP também discute "por que" eles são preferidos. Essencialmente, tudo se resume à eficiência: 1. Não há necessidade de implementar operações que não fazem sentido para o seu objeto (como coleções não ordenadas). 2. Algumas coleções têm operações muito eficientes em alguns tipos de comparações. Comparações ricas permitem que o intérprete tire vantagem disso se você defini-las.
Christopher
1
Re 1, se não fizerem sentido, não implemente cmp . Em relação ao 2, ter ambas as opções pode permitir que você otimize conforme necessário, enquanto ainda faz prototipagem e testes rapidamente. Nenhum dos dois me disse por que ele foi removido. (Essencialmente, isso se resume à eficiência do desenvolvedor para mim.) É possível que as comparações ricas sejam menos eficientes com o fallback cmp em vigor? Isso não faria sentido para mim.
1
@R. Pate, como tento explicar em minha resposta, não há perda real na generalidade (uma vez que um mixin, decorador ou metaclasse, permite que você defina facilmente tudo em termos de apenas <se desejar) e, portanto, para todas as implementações Python para transportar código redundante voltando para cmp para sempre - apenas para permitir que os usuários Python expressem coisas de duas maneiras equivalentes - seria executado 100% contra a natureza do Python.
Alex Martelli
2

(Editado em 17/06/17 para levar os comentários em consideração.)

Eu tentei a resposta mixin comparável acima. Tive problemas com "Nenhum". Aqui está uma versão modificada que lida com comparações de igualdade com "Nenhum". (Não vi razão para me preocupar com as comparações de desigualdade com Nenhum por falta de semântica):


class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other and not other<self

    def __ne__(self, other):
        return not __eq__(self, other)

    def __gt__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)): 
            return NotImplemented
        else:
            return not other<self    
Gabriel Ferrer
fonte
Como você acha que selfpoderia ser o solteirão Nonede NoneTypee, ao mesmo tempo implementar o seu ComparableMixin? E, de fato, esta receita é ruim para Python 3.
Antti Haapala
3
selfvai não ser None, de modo a que ramo pode ir completamente. Não use type(other) == type(None); simplesmente use other is None. Em vez de especial-invólucro None, de teste, se o outro tipo é um exemplo do tipo de self, e devolver o NotImplementedSingleton se não: if not isinstance(other, type(self)): return NotImplemented. Faça isso para todos os métodos. Python, então, pode dar ao outro operando uma chance de fornecer uma resposta.
Martijn Pieters
1

Inspirado pelas respostas ComparableMixine pelas KeyedMixinrespostas de Alex Martelli , eu vim com o seguinte mixin. Ele permite que você implemente um único _compare_to()método, que usa comparações baseadas em chave semelhantes a KeyedMixin, mas permite que sua classe escolha a chave de comparação mais eficiente com base no tipo de other. (Observe que este mixin não ajuda muito para objetos que podem ser testados quanto à igualdade, mas não à ordem).

class ComparableMixin(object):
    """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
        """return keys to compare self to other.

        if self and other are comparable, this function 
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
        """
        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

    def __ne__(self, other):
        return not self == other

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented
Eli Collins
fonte