2015-10-23 8 views
5

Per un auto-progetto, ho voluto fare qualcosa di simile:Il tentativo di replicare stringa funzionalità internato di Python per i non-stringhe

class Species(object): # immutable. 
    def __init__(self, id): 
     # ... (using id to obtain height and other data from file) 
    def height(self): 
     # ... 

class Animal(object): # mutable. 

    def __init__(self, nickname, species_id): 
     self.nickname = nickname 
     self.species = Species(id) 
    def height(self): 
     return self.species.height() 

Come potete vedere, non ho davvero bisogno di più di un'istanza di Species (id) per id, ma ne creerei uno ogni volta che creo un oggetto Animal con quell'id, e probabilmente avrei bisogno di più chiamate, per esempio, Animal(somename, 3).

Per risolvere questo, quello che sto cercando di fare è di fare una classe in modo che per 2 istanze di esso, diciamo A e B, il seguente è sempre vero:

(a == b) == (a is b) 

Questo è qualcosa che Python fa con stringhe letterali e si chiama stage. Esempio:

a = "hello" 
b = "hello" 
print(a is b) 

che stampa produrrà vero (a patto che la stringa è abbastanza breve se stiamo usando direttamente la shell Python).

Posso solo immaginare come CPython faccia questo (probabilmente implica un po 'di magia C), quindi sto facendo la mia versione di esso. Finora ho:

class MyClass(object): 

    myHash = {} # This replicates the intern pool. 

    def __new__(cls, n): # The default new method returns a new instance 
     if n in MyClass.myHash: 
      return MyClass.myHash[n] 

     self = super(MyClass, cls).__new__(cls) 
     self.__init(n) 
     MyClass.myHash[n] = self 

     return self 

    # as pointed out on an answer, it's better to avoid initializating the instance 
    # with __init__, as that one's called even when returning an old instance. 
    def __init(self, n): 
     self.n = n 

a = MyClass(2) 
b = MyClass(2) 

print a is b # <<< True 

Le mie domande sono:

a) è il mio problema nemmeno la pena di risolvere? Dal momento che il mio oggetto Species previsto dovrebbe essere abbastanza leggero e il numero massimo di volte che Animal può essere chiamato, piuttosto limitato (immagina un gioco Pokemon: non più di 1000 istanze, top)

b) Se lo è, è questo un approccio valido per risolvere il mio problema?

c) Se non è valido, potresti approfondire un modo più semplice/più pulito/più Pythonic per risolvere questo problema?

risposta

1

Sì, l'implementazione di un metodo __new__ che restituisce un oggetto memorizzato nella cache è il modo appropriato di creare un numero limitato di istanze. Se non ti aspetti di creare molte istanze, potresti semplicemente implementare lo __eq__ e confrontarlo in base al valore piuttosto che all'identità, ma non fa male a farlo in questo modo.

Si noti che un oggetto immutabile deve generalmente eseguire tutta la sua inizializzazione in __new__, anziché __init__, poiché quest'ultimo viene chiamato dopo che l'oggetto è stato creato. Inoltre, __init__ verrà chiamato su qualsiasi istanza della classe restituita da __new__, quindi con la cache, verrà richiamata ogni volta che viene restituito un oggetto memorizzato nella cache.

Inoltre, il primo argomento di __new__ è l'oggetto di classe non un'istanza, quindi probabilmente dovrà nominarlo cls piuttosto che self (è possibile utilizzare self invece di instance più avanti nel metodo se si desidera però!).

0

Per rendere il più generale possibile, suggerirò un paio di cose. Uno, ereditato da un namedtuple se si desidera l'immutabilità "vera" (normalmente le persone sono piuttosto fuori mano su questo, ma quando si sta facendo internamento, rompere l'invariante immutabile può causare problemi molto più grandi). In secondo luogo, utilizzare i blocchi per consentire il comportamento sicuro del thread.

Perché questo è piuttosto complesso, ho intenzione di fornire una copia modificata del Species codice con commenti spiegarla:

import collections 
import operator 
import threading 

# Inheriting from a namedtuple is a convenient way to get immutability 
class Species(collections.namedtuple('SpeciesBase', 'species_id height ...')): 
    __slots__ =() # Prevent creation of arbitrary values on instances; true immutability of declared values from namedtuple makes true immutable instances 

    # Lock and cache, with underscore prefixes to indicate they're internal details 
    _cache_lock = threading.Lock() 
    _cache = {} 

    def __new__(cls, species_id): # Switching to canonical name cls for class type 
     # Do quick fail fast check that ID is in fact an int/long 
     # If it's int-like, this will force conversion to true int/long 
     # and minimize risk of incompatible hash/equality checks in dict 
     # lookup 
     # I suspect that in CPython, this would actually remove the need 
     # for the _cache_lock due to the GIL protecting you at the 
     # critical stages (because no byte code is executing comparing 
     # or hashing built-in int/long types), but the lock is a good idea 
     # for correctness (avoiding reliance on implementation details) 
     # and should cost little 
     species_id = operator.index(species_id) 

     # Lock when checking/mutating cache to make it thread safe 
     try: 
      with cls._cache_lock: 
       return cls._cache[species_id] 
     except KeyError: 
      pass 

     # Read in data here; not done under lock on assumption this might 
     # be expensive and other Species (that already exist) might be 
     # created/retrieved from cache during this time 
     species_id = ... 
     height = ... 
     # Pass all the values read to the superclass (the namedtuple base) 
     # constructor (which will set them and leave them immutable thereafter) 
     self = super(Species, cls).__new__(cls, species_id, height, ...) 

     with cls._cache_lock: 
      # If someone tried to create the same species and raced 
      # ahead of us, use their version, not ours to ensure uniqueness 
      # If no one raced us, this will put our new object in the cache 
      self = cls._cache.setdefault(species_id, self) 
     return self 

Se si vuole fare internato per le librerie generali (in cui gli utenti potrebbero essere filettati, e non puoi fidarti di loro per non rompere l'invarianza dell'immutabilità), qualcosa di simile a quanto sopra è una struttura di base con cui lavorare. È veloce, minimizza l'opportunità di bancarelle anche se la costruzione è pesante (in cambio di possibilmente ricostruire un oggetto più di una volta e gettare via una sola copia se molti thread tentano di costruirlo per la prima volta contemporaneamente), ecc.

Naturalmente, se la costruzione è a buon mercato e casi sono piccole, poi basta scrivere un __eq__ (e possibilmente __hash__ se è logicamente immutabili) e da fare con esso,

+0

potete vedere una molto simile sorta di codice in esecuzione di Python di i vari ['wrapper's per' functools.lru_cache'] (https://hg.python.org/cpython/file/3.5/Lib/functools.py#l453). Come succede, non si bloccano per le operazioni "atomiche" quando il lavoro svolto è atomico, che supporta il mio commento sopra, ma usano lo stesso "prova a ottenere dalla cache con il blocco, se non è riuscito, rilascia il blocco, eseguire costoso lavoro, riacquisti il ​​blocco e aggiorna la cache se non viene eseguito "il pattern che ho usato sopra per la dimensione della cache è limitato (e i gruppi di operazioni devono essere eseguiti atomicamente). – ShadowRanger

Problemi correlati