2012-12-06 10 views
6

Desidero confrontare una coppia di dizionari e utilizzare il confronto in virgola mobile "fuzzy" o meglio ancora utilizzare numpy.allclose() per farlo. Tuttavia, l'utilizzo del valore predefinito == o != in Python per dict non lo fa.Confronto di dic di Python con valori in virgola mobile inclusi

Mi chiedevo se ci fosse un modo per cambiare l'operazione di confronto in virgola mobile (probabilmente usando un gestore di contesto per una pulizia sicura).

Credo che un esempio possa essere d'aiuto. Ho un dict profondamente annidato che contiene tutti i tipi di valori. Alcuni di questi valori sono valori a virgola mobile. So che ci sono un sacco di insidie ​​per 'il confronto' valori in virgola mobile, ecc

d1 = {'a': {'b': 1.123456}} 
d2 = {'a': {'b': 1.1234578}} 

Vorrei utilizzare != a confrontare questi due dicts e lo hanno restituire True se le uniche differenze sono numeri in virgola mobile all'interno di un certo intervallo. Ad esempio, non contare i valori diversi se sono chiudi (non sono sicuro della precisione che voglio ancora).

Suppongo che potrei ricorsivamente passare attraverso le dicts me stesso e basta usare manualmente numpy.allclose() per valori a virgola mobile e ripiegare per l'uguaglianza normale test per tutti gli altri tipi, ecc, tuttavia, questo è un po 'complicato e soggetto a errori. Penso che questa sarebbe una soluzione accettabile, e mi piacerebbe vederne una simile. Spero comunque che ci sia qualcosa di più elegante.

L'elegante soluzione nella mia testa sarebbe simile a quanto segue. Tuttavia, non so se qualcosa di simile è ancora possibile:

with hacked_float_compare: 
    result = d1 != d2 

Così, all'interno di questo contesto, direttore mi sarebbe la sostituzione del confronto in virgola mobile (solo per float() valori standard con entrambi i miei confronto o numpy.allclose().

Ancora una volta, non sono sicuro che ciò sia possibile perché la patch delle scimmie float() non può essere eseguita poiché è scritta in C. Vorrei anche evitare di dover modificare ogni valore in virgola mobile nei Dits sul mio classe float che ha un __eq__(). Forse questo è il migliore w però?

+0

Un'opzione sarebbe quella di creare un wrapper per float ed override '__eq__' lì. – NullUserException

+0

Ma avresti bisogno di creare tutti i tuoi float con 'fuzzyfloat (0.5)', ecc. – alexis

+0

Giusto. So che questo approccio funziona, ma non volevo usare un oggetto/classe speciale se potessi evitarlo. In questo caso ho solo bisogno che il confronto sia "sfocato". Questo è il motivo per cui speravo di utilizzare un gestore di contesto e di andare in una "modalità" diversa per un periodo di tempo limitato. –

risposta

5

Evitare tipi derivati ​​di sottoclasse. Te ne pentirai quando scoprirai che i tuoi oggetti hanno cambiato tipo per qualche motivo sconosciuto. Utilizzare invece la delega. Per esempio:

import operator as op 


class FuzzyDict(object): 
    def __init__(self, iterable=(), float_eq=op.eq): 
     self._float_eq = float_eq 
     self._dict = dict(iterable) 

    def __getitem__(self, key): 
     return self._dict[key] 

    def __setitem__(self, key, val): 
     self._dict[key] = val 

    def __iter__(self): 
     return iter(self._dict) 

    def __len__(self): 
     return len(self._dict) 

    def __contains__(self, key): 
     return key in self._dict 

    def __eq__(self, other): 
     def compare(a, b): 
      if isinstance(a, float) and isinstance(b, float): 
       return self._float_eq(a, b) 
      else: 
       return a == b 
     try: 
      if len(self) != len(other): 
       return False 
      for key in self: 
       if not compare(self[key], other[key]): 
        return False 
      return True 
     except Exception: 
      return False 

    def __getattr__(self, attr): 
     # free features borrowed from dict 
     attr_val = getattr(self._dict, attr) 
     if callable(attr_val): 
      def wrapper(*args, **kwargs): 
       result = attr_val(*args, **kwargs) 
       if isinstance(result, dict): 
        return FuzzyDict(result, self._float_eq) 
       return result 
      return wrapper 
     return attr_val 

E un esempio di utilizzo:

>>> def float_eq(a, b): 
...  return abs(a - b) < 0.01 
... 
>>> A = FuzzyDict(float_eq=float_eq) 
>>> B = FuzzyDict(float_eq=float_eq) 
>>> A['a'] = 2.345 
>>> A['b'] = 'a string' 
>>> B['a'] = 2.345 
>>> B['b'] = 'a string' 
>>> B['a'] = 2.3445 
>>> A == B 
True 
>>> B['a'] = 234.55 
>>> A == B 
False 
>>> B['a'] = 2.345 
>>> B['b'] = 'a strin' 
>>> A == B 
False 

E funzionano anche in caso di nidificazione:

>>> A['nested'] = FuzzyDict(float_eq=float_eq) 
>>> A['nested']['a'] = 17.32 
>>> B['nested'] = FuzzyDict(float_eq=float_eq) 
>>> B['nested']['a'] = 17.321 
>>> B['b'] = 'a string' # changed before 
>>> A == B 
True 
>>> B['nested']['a'] = 17.34 
>>> A == B 
False 

una sostituzione completa di dict richiederebbe un po 'più di codice e, probabilmente, un po' test per vedere quanto è robusto, ma anche la soluzione di cui sopra fornisce molte delle funzioni dict (ad esempio copy, setdefault, get, update ecc)


Per quanto riguarda il motivo per cui non si dovrebbe sottoclasse un built-in.

Questa soluzione sembra facile e corretta, ma generalmente non lo è. Prima di tutto, anche se puoi creare una sottoclasse di tipi built-in, questo non significa che siano stati scritti per essere usati come sottoclassi, quindi potresti scoprire che per far funzionare qualcosa devi scrivere più codice di quanto pensassi.

Inoltre, probabilmente vorrai utilizzare i metodi incorporati, ma questi metodi restituiranno un'istanza del tipo built-in e non un'istanza della classe, il che significa che devi reimplementare ogni singolo metodo del tipo Inoltre, a volte è necessario implementare altri metodi che non sono stati implementati nel built-in.

Per esempio, sottoclasse list si potrebbe pensare che, dal momento che list implementa solo __iadd__ e __add__ sarai al sicuro reimplementare questi due metodi, ma vi sbagliate! È inoltre necessario implementare __radd__, altrimenti espressioni come:

[1,2,3] + MyList([1,2,3]) 

restituirebbe una normale list e non MyList.

In sintesi, la creazione di sottoclassi di un built-in ha molte più conseguenze di quanto si possa pensare all'inizio e potrebbe introdurre alcuni bug imprevedibili a causa del cambiamento di tipi o comportamenti che non ci si aspettava. Anche il debug diventa più difficile perché non puoi semplicemente stampare le istanze degli oggetti nel log, la rappresentazione sarebbe corretta! Devi davvero verificare la classe di tutti gli oggetti in giro per catturare questi bug sottili.

Nella tua situazione specifica, se si prevede di convertire i dizionari solo all'interno di un singolo metodo, allora si può evitare la maggior parte gli svantaggi di sottoclassi dict, ma a quel punto perché non è sufficiente scrivere una funzione e confrontare i dict s con esso? Questo dovrebbe funzionare bene, tranne se si desidera passare gli dict a una funzione di libreria che esegue il confronto.

+1

Questo sembra abbastanza buono. Tuttavia, nel mio caso penso che la sottoclasse di 'dict' potrebbe essere ok. Voglio solo convertire i dit in locale per fare questo confronto. Quindi, questa nuova classe sarà sempre usata internamente ad un unico metodo. Ha senso e sembra ragionevole? –

+0

Tuttavia, questa soluzione non restituirà False se il comando 'other' dict ha chiavi che non sono nel primo dict. Quindi questa soluzione cambia la semantica del confronto di dicts più di un semplice confronto in virgola mobile, giusto? –

+0

@ durden2.0 L'ho modificato leggermente da quando l'ho pubblicato per la prima volta, e ho pensato che fosse okay. Prima di controllare per la prima volta 'sort (self) == sort (other)' che viene letto di questa differenza, ma penso che anche quanto sopra sia ok. Perché se il numero di chiavi è diverso, allora viene catturato confrontando le lunghezze, dopodiché controllo ogni tasto in "self", e se non è in "other" viene sollevato un "KeyError" che viene catturato dal 'except Exception' che restituisce correttamente' False', quindi dovrebbe essere a posto. Ad ogni modo, se il cambiamento è garantito essere solo in un metodo forse la sottoclasse di 'dict' va bene. – Bakuriu

1

Per eseguire l'override di un operatore di confronto, è necessario definire una classe derivata che utilizza un operatore diverso. Quindi non puoi farlo nel modo che suggerisci. Che cosa si potrebbe fare è derivare una classe "fuzzy float" (come @null) ha suggerito, o deriva e classe da dict e specificare che utilizza il confronto sfocata su carri:

class fuzzydict(dict): 
    def __eq__(self, other): 
     """Manually compare each element of `self` with `other`. 
      Float values are compared up to reasonable precision.""" 

Dovrete sfornare attraverso il logica di comparazione del dizionario te stesso, e probabilmente non sarà veloce come il confronto integrato, ma sarai in grado di scrivere dict1 == dict2 nel tuo codice. Assicurati di utilizzare fuzzydict anziché dict per tutti i dizionari (nidificati) che potrebbero contenere float.

Devo aggiungere, però, che si sta rischiando indeterminatezza: I dizionari confronteranno uguali ma contenere numeri leggermente diversi, in modo da calcoli subsquent potrebbe dare risultati che non risultano uguali, a seconda di quale dizionario si utilizza. A mio parere, un approccio più sicuro (e più sano) sarebbe quello di arrotondare i float quando li inserirai nel dizionario, in modo che si confrontino in modo strettamente uguale.

+0

Sì, anche questo funzionerà. Tuttavia, non vedo molti vantaggi nel creare la mia classe di dict e inserire il codice di confronto nel dict "__eq__". Questo stesso esatto codice potrebbe essere solo un metodo che prende due dicts. Quindi non devo usare questo nuovo tipo di dict in tutto il mondo o convertire dicts esistenti, ecc. Ancora una volta, questa soluzione andrebbe bene se lo facessi in molti posti. Tuttavia, c'è solo un settore in cui questo confronto è importante. –

+0

Inoltre, fare calcoli con questi numeri in virgola mobile è complicato in ogni caso solo perché la rappresentazione in virgola mobile, ecc. Inoltre, nel mio scenario, non mi importa che questi numeri siano all'interno di un intervallo l'uno dell'altro. Ciò non causerà nessuna strana operazione lungo la strada. È solo un'area di codice molto limitata a cui voglio applicare questo. –

+0

Il confronto dict di Python è ricorsivo. Se si ottiene una classe, python gestirà la ricorsione e si dovrà solo implementare la logica piatta: controllo di chiavi mancanti o extra e confronto dei valori. – alexis

2

Solo per riferimento, penso che nella mia situazione la sottoclasse non fosse il modo migliore. Ho elaborato una soluzione che probabilmente userò here.

Questa non è la risposta accettata poiché era un approccio collaborativo basato su ciò che ho imparato da questo thread. Volevo solo una "soluzione" di cui altri possano trarre beneficio.

Problemi correlati