2012-05-30 20 views
5

Quindi esiste un modo semplice per calcolare l'intersezione di due insiemi tramite set.intersection(). Tuttavia, ho il seguente problema:.Intersezione personalizzata Python intersezione

class Person(Object):      
    def __init__(self, name, age):              
     self.name = name                 
     self.age = age                 

l1 = [Person("Foo", 21), Person("Bar", 22)]            
l2 = [Person("Foo", 21), Person("Bar", 24)]            

union_list = list(set(l1).union(l2))           
# [Person("Foo", 21), Person("Bar", 22), Person("Bar", 24)] 

(Object è una base di classe fornito dal mio ORM che implementa funzionalità di base __hash__ e __eq__, che aggiunge, in sostanza ogni membro della classe per l'hash In altre parole, il __hash__ restituito sarà un hash di ogni elemento della classe)

a questo punto, vorrei eseguire un'operazione di impostazione di intersezione da solo .name, da trovare, per esempio, Person('Bar', -1).intersection(union_list) #= [Person("Bar", -1), Person("Bar", 22), Person("Bar", 24)]. (Il tipico .intersection() a questo punto non mi avrebbe dato qualsiasi cosa, non posso ignorare __hash__ o __eq__ sulla classe Person, poiché ciò sarebbe contrario l'unione serie originale (I credo)

Qual è il modo migliore per farlo questo in Python 2.x

EDIT:? si noti che la soluzione non hanno fare affidamento su un set Tuttavia, ho bisogno di trovare i sindacati e poi le intersezioni, così ci si sente come questo è suscettibile di un set. (ma sono disposto ad accettare soluzioni che usano qualunque magia tu ritenga degna, purché risolva il mio problema!)

+0

Non capisco il risultato desiderato. Potresti per favore * spiegare * che cosa dovrebbe contenere il risultato? –

+0

Err crap, che dovrebbe essere .union, not .intersection. Ho aggiornato la domanda: fammi sapere se è più chiara? –

+0

Sono ancora un po 'confuso dal momento che il codice di esempio non ha il risultato richiesto. –

risposta

1

Odio rispondere alle mie stesse domande, quindi rimarrò a contrassegnarlo come la "risposta" ancora per un po '.

scopre il modo per farlo è la seguente:

import types 
p = Person("Bar", -1) 
new_hash_method = lambda obj: hash(obj.name) 
p.__hash__ = types.MethodType(new_hash_method, p) 
for i in xrange(0, len(union_list)): 
    union_list[i].__hash__ = types.MethodType(new_hash_method, union_list[i]) 
set(union_list).intersection(p) 

E 'certamente sporco e si basa su types.MethodType, ma è meno intenso rispetto la soluzione migliore finora proposte (la soluzione di glglgl) come il mio attuale union_list può contenere potenzialmente nell'ordine di migliaia di elementi, quindi questo mi salverà ricreando oggetti ogni volta che eseguo questa procedura di intersezione.

+0

Questo funziona davvero? La documentazione indica che metodi magici come '__hash__' sono cercati nella classe, non nell'istanza. https://docs.python.org/3/reference/datamodel.html#special-lookup –

+0

In realtà, sembra che funzioni per le classi di vecchio stile, ma non per le nuove classi di stile: https://docs.python.org /2/reference/datamodel.html#special-method-lookup-for-old-style-classes –

0

È necessario eseguire l'override di __hash__ e i metodi di confronto se si desidera utilizzare set come questo.

Se non lo fai, allora

Person("Foo", 21) == Person("Foo", 21) 

sarà sempre falso.

Se i tuoi oggetti sono gestiti da un ORM, dovrai verificare come confronta gli oggetti. Solitamente guarda solo l'id dell'oggetto e la comparazione funziona solo se entrambi gli oggetti sono gestiti. Se si prova a confrontare un oggetto ottenuto dall'ORM con un'istanza creata dall'utente prima che sia persistente nel db, è probabile che siano diversi. Ad ogni modo, un ORM non dovrebbe avere problemi con te fornendo la tua logica di comparazione.

Ma se per alcuni motivi non è possibile ignorare __hash__ e __eq__, non è possibile utilizzare i set per l'intersezione e l'unione con gli oggetti originali. Si potrebbe:

  • calcolare l'intersezione/unione te
  • creare una classe wrapper che è paragonabile:

    class Person:      
        def __init__(self, name, age):              
         self.name = name                 
         self.age = age                 
    
    l1 = [Person("Foo", 21), Person("Bar", 22)]            
    l2 = [Person("Foo", 21), Person("Bar", 24)]            
    
    class ComparablePerson: 
        def __init__(self, person): 
         self.person = person 
    
        def __hash__(self): 
         return hash(self.person.name) + 31*hash(self.person.age) 
    
        def __eq__(self, other): 
         return (self.person.name == other.person.name and 
           self.person.age == other.person.age) 
        def __repr__(self): 
         return "<%s - %d>" % (self.person.name, self.person.age) 
    
    c1 = set(ComparablePerson(p) for p in l1) 
    c2 = set(ComparablePerson(p) for p in l2) 
    
    print c1 
    print c2 
    print c1.union(c2) 
    print c2.intersection(c1) 
    
+1

Vedere il mio commento (sulla domanda originale); l'override è già gestito da un ORM. Aggiornerò la domanda per riflettere questo –

0

Questo è goffo, ma ...

set(p for p in union_list for q in l2 if p.name == q.name and p.age != q.age) | (set(p for p in l2 for q in union_list if p.name == q.name and p.age != q.age)) 
# {person(name='Bar', age=22), person(name='Bar', age=24)} 
5

Suoni come

>>> class Person: 
...  def __init__(self, name, age): 
...   self.name = name 
...   self.age = age 
...  def __eq__(self, other): 
...   return self.name == other.name 
...  def __hash__(self): 
...   return hash(self.name) 
...  def __str__(self): 
...   return self.name 
... 
>>> l1 = [Person("Foo", 21), Person("Bar", 22)] 
>>> l2 = [Person("Foo", 21), Person("Bar", 24)] 
>>> union_list = list(set(l1).union(l2)) 
>>> [str(l) for l in union_list] 
['Foo', 'Bar'] 

è quello che vuoi, dal name è la tua chiave unica?

+0

Ah, no, l'ORM che sto usando fornisce già un metodo __eq__ e __hash__ (e, come tale, set.union() produce già risultati "sani"). Sto cercando un modo per fare un'operazione di intersezione che * solo * usi uno dei membri della classe come chiave, e come tale non può sovrascrivere '__hash__' o' __eq__'. –

+0

Vedo, quindi forse la soluzione di Glglgl sarebbe adatta? –

1

Se si desidera che il age irrilevante rispetto alla confrontando, si dovrebbe ignorare __hash__() e __eq__() in Person anche se lo avete nel vostro Object.

Se avete bisogno di questo comportamento solo in questo (e la classica) contesti, è possibile creare un oggetto wrapper che contiene il Person e si comporta in modo diverso, come ad esempio

class PersonWrapper(Object): 
    def __init__(self, person): 
     self.person = person 
    def __eq__(self, other): 
     if hasattr(other, 'person'): 
      return self.person.name == other.person.name 
     else: 
      return self.person.name == other.name 
    def __hash__(self): 
     return hash(self.person.name) 

e poi fare

union_list = list(set(PersonWrapper(i) for i in l1).union(PersonWrapper(i) for i in l2)) 
# [Person("Foo", 21), Person("Bar", 22), Person("Bar", 24)] 

(non testato)

+0

Il problema è che ho bisogno dei metodi '__hash__' e' __eq__' così come sono, altrimenti '.union()' non funzionerà come fa. –

+0

Hmm, interessante. Quindi non c'è modo di farlo senza ricostruire gli oggetti? So che il C++ mi dà la possibilità di passare un comparatore personalizzato; Python non ha la stessa abilità? –

+0

C'è un modo per farlo con funzioni come 'sorted()' dove puoi dare una funzione 'cmp' e una funzione' key', ma non con 'set's, ahimè ... – glglgl

1

ne dite:

d1 = {p.name:p for p in l1} 
d2 = {p.name:p for p in l2} 

intersectnames = set(d1.keys()).intersection(d2.keys) 
intersect = [d1[k] for k in intersectnames] 

Potrebbe essere più veloce di gettare intersectnames al vostro ORM, nel qual caso non sarebbe costruire dizionari, basta raccogliere i nomi nelle liste.