Ricerca degli attributi

Per cercare gli attributi di un oggetto O, come ad esempio O.a, si esegue il seguente algoritmo (supponendo che l’interprete abbia già recuperato l’oggetto O):

  1. ricerca a tra tutti gli attributi di O
  2. se O non è una classe procede al passo 4
  3. effettua una ricerca nelle superclassi di O
  4. posto T=type(O) esegue una ricerca negli attributi di T
  5. esegue una ricerca nelle superclassi di T seguendo l’ordine indicato da MRO, che può essere visualizzato con O.mro().

class N(type):
    a = 0
class M(N):
    pass
class C:
    pass
class B(C):
    pass
class A(B,metaclass=M): # Questo è il modo di specificare un tipo diverso da type
    pass
title: Attenzione!
`A.a` viene trovato senza problemi, perchè `a` in `N`.
Se però si crea una istanza di `A`, quale `i = A()` e si cerca di accedere ad `a`, viene generato un errore perchè `a` non fa parte di `A`.
 
Questo perché l'attributo non è definito in A o in una delle sue superclassi, ma in una metaclass.

Intervenire sul protocollo

La ricerca degli attributi non è implementata completamente nell’interprete, ma può essere modificata dallo sviluppatore tramite alcuni metodi magici:

  • __getattribute__: invocato per accessi in lettura
  • __getattr__: invocato da __getattribute__ quando si verifica una miss (non trova l’attributo)
  • __setattr__: usato per modificare/definire il valore di un attributo
class parent:
	z = "Classe padre"
	
class son(parent):
	y = "Classe figlia"
	def __init__(self, x="")
		self.x = x
 
o = son("ciao")
print(o.x)			# ciao
o.x = "hello"
print(o.x)			# hello

Questo ultimo pezzo di codice, dove vengono usati metodi chiamati implicitamente dall’interprete, si può riscrivere in maniera più esplicita come segue:

print(o.__getattribute__("x"))
o.__setattr__("x", "hello")
print(o.__getattribute__("x"))

Tutti questi metodi sono definiti nella classe object, che è la superclasse di tutti gli oggetti.

Si possono stampare tutti gli attributi presenti in un oggetto tramite uno speciale dizionare (anch’esso un attributo)

pprint(dict(son.__dict__))
pprint(dict(parend.__dict__))
pprint(object.__dict__['__getattribute__'])

Ridefinire i metodi base

Si possono ovviamente ridefinire i metodi appena visti, per modificare qualche aspetto del procedimento standard. Python supporta super().

__getattribute__

class A:
	def __getattribute__(self, x):
		print("eseguo getattribute in A")
		return super().__getattribute__(x)
		
class B(A):
	def __getattribute__(self, x):
		print("eseguo getattribute in B")
		return super().__getattribute__(x)
		
class C(A):
	def __getattribute__(self, x):
		print("eseguo getattribute in C")
		return super().__getattribute__(x)
		
class D(B, C):
	y = "ciao"
	
a = D()
a.y						# stampa tutti i print di __getattribute__ di
						# (in ordine) B, C, A

__getattr__

Questo metodo viene chiamato quando __getattribute__ non ritorna nulla.

class A:
	def __getattr__(self, x):
		print("eseguo getattr in A, perchè non esiste questo attributo")
		return self.__setattr__(x, "no value")
		
class B(A): pass		
class C(A): pass
 
class D(B, C):
	y = "ciao"
	
a = D()
a.s						# viene eseguito __getattr__ di A
a.s						# stampa "no value"

Overload metodi

Quando si chiama un metodo oggetto.f, ovviamente f viene cercata tra gli attributi di oggetto. Quando viene trovata, si controlla che sia codice effettivamente eseguibile, e in tal caso si passano i parametri. Se non è codice eseguibile, __getattributes__ demanda il lookup alla classe object. Se lo trova, agisce come decoratore, e restituisce una funzione che, quando verrà chiamata, conta il numero di parametri, scorre la catena ereditaria per cercare una funzione con la stessa firma, ed esegue il metodo trovato. Se non trova nulla ritorna None, che poi causerà un errore.

Ispezionare un metodo

Per ispezionare il numero di parametri di un metodo, esiste una libreria inspect.

from inspect import signature
 
signature(funzione).parameters