[Python] Anomalia nella classe?

Marco Giusti marco.giusti a gmail.com
Lun 24 Giu 2013 00:46:22 CEST


On Sat, Jun 22, 2013 at 02:25:45PM +0200, Gollum1 wrote:
> Il 22 giugno 2013 14:05, Gollum1 <gollum1.smeagol1 a gmail.com> ha scritto:
> > sto implementando il seguente codice:
> 
> altra domanda:
> 
> in alcuni punti faccio riferimento direttamente agli attributi della
> classe, in quanto li ho resi pubblici e non privati. Potersi fare si
> può fare (e infatti lo faccio), ma a livello sintattico e di buona
> programmazione, va bene così, o sarebbe meglio avere un metodo che
> ritorna il valore dell'attributo e un'altro che lo setti?

Domanda da un milione di euro. Non so' darti una risposta ma posso dirti
alcune problematiche che ho affrontato.

Ante scriptum. Ho paura che questa raggiunga presto lunghezze
indesiderate.

Ti presento subito due problemi dei metodi getter e setter. Il primo è
che rischi di avere decine di linee di codice che non fanno niente:

	class Foo:

		_a = None
		_b = None
		_c = None

		def get_a(self):
			return self._a

		def set_a(self, value):
			self._a = value

		def get_b(self):
			return _b

		...

Di fatto questo codice non fa' niente e trovo che sia tempo perso
scriverlo. Il secondo problema riguarda le proprietà che di solito vanno
a braccetto con gli accessor (getter e setter):

	def calcolo_molto_oneroso_di(value):
		time.sleep(2)
		return value

	class Foo:

		_a = None

		def get_a(self):
			return self._a

		def set_a(self, value):
			self._a = calcolo_molto_oneroso_di(value)

		a = property(get_a, set_a)

	
	class BetterFoo(Foo):

		def set_a(self, value):
			if self._a != value:
				Foo.set_a(self, value)


	foo = BetterFoo()
	foo.a = 3

Se uno crede che set_a di BetterFoo sia invocata si sbaglia perché gli
accessor delle proprietà non sono ridefinibili e quindi devi aggiungere
un'altra riga, alquanto bruttina, come la seguente:

		a = property(Foo.get_a, set_a)

Detto questo, ti presento le problematiche che mi sono trovato a dover
affrontare quando ho fatto il refactoring di una classe che ereditava da
una seconda classe. La classe figlia non aveva molto senso di esistere
ma soprattutto avevo bisogno di controllare l'instanziamento della
classe, già controllavo l'instanziamento della classe genitrice, e così
l'ho convertita in un wrapper. Alcuni attributi della classe genitrice
erano pubblici ed ecco il primo problema:

	>>> class Foo:
	...  a = "bar"
	...  def print_a(self):
	...   print self.a
	...
	>>> class FooW:
	...  def __init__(self, foo):
	...   self.foo = foo
	...  def __getattr__(self, name):
	...   return getattr(self.foo, name)
	...
	>>> f = FooW(Foo())
	>>> f.print_a()
	bar
	>>> f.a = "spam"
	>>> f.print_a()
	bar
	>>> 

'f.a' setta l'attributo 'a' ma nella classe 'FooW' e 'Foo' non ne è a
conoscenza. '__setattr__' può essere di aiuto ma pericoloso:

	>>> class FooW2:
	...  def __init__(self, foo):
	...   self.foo = foo
	...  def __getattr__(self, name):
	...   return getattr(self.foo, name)
	...  def __setattr__(self, name, value):
	...   setattr(self.foo, name, value)
	... 
	>>> f = FooW2(Foo())
	>>> f = FooW2(Foo())
	Traceback (most recent call last):
	  File "<stdin>", line 1, in <module>
	  File "<stdin>", line 3, in __init__
	  File "<stdin>", line 7, in __setattr__
	  File "<stdin>", line 5, in __getattr__
	  ...
	  File "<stdin>", line 5, in __getattr__
	RuntimeError: maximum recursion depth exceeded
	>>> 

Questo perché 'setattr' viene invocato nel costruttore quando ancora
'self.foo' non è definito e quindi anche '__getattr__' viene invocato ed
invoca se stesso fino allo scatenarsi dell'eccezione. Neanche il
prossimo trucco funziona:

	>>> class FooW3:
	...  foo = None
	...  def __init__(self, foo):
	...   self.foo = foo
	...  def __getattr__(self, name):
	...   return getattr(self.foo, name)
	...  def __setattr__(self, name, value):
	...   setattr(self.foo, name, value)
	...
	>>> f = FooW3(Foo())
	Traceback (most recent call last):
	  File "<stdin>", line 1, in <module>
	  File "<stdin>", line 4, in __init__
	  File "<stdin>", line 8, in __setattr__
	AttributeError: 'NoneType' object has no attribute 'foo'
	>>> 

La soluzione è quella, bruttina, di accedere al dizionario dell'oggetto
nel costruttore:

	>>> class FooW4:
	...  def __init__(self, foo):
	...   self.__dict__["foo"] = foo
	...  def __getattr__(self, name):
	...   return getattr(self.foo, name)
	...  def __setattr__(self, name, value):
	...   setattr(self.foo, name, value)
	...
	>>> f = FooW4(Foo())
	>>> f.print_a()
	bar
	>>> f.a = "spam"
	>>> f.print_a()
	spam
	>>> 

Adesso tutto funziona senonché la classe figlia aveva anche degli
attributi e metodi tutti suoi e pubblici:

	>>> class FooW5:
	...  def __init__(self, foo):
	...   self.__dict__["foo"] = foo
	...   self.__dict__["b"] = "manzotin"
	...  def __getattr__(self, name):
	...   return getattr(self.foo, name)
	...  def __setattr__(self, name, value):
	...   setattr(self.foo, name, value)
	...  def print_b(self):
	...   print self.b
	>>> f = FooW5(Foo())
	>>> f.print_b()
	manzotin
	>>> f.b = "simmenthal"
	>>> f.print_b()
	manzotin
	>>> 

Anche qui la soluzione è stata quella di accedere a '__dict__' in
'__setattr__'. Sicuramente avere avuto degli accessor avrebbe aiutato ma
non capita spesso di fare il refactoring di una classe e riscriverla
come wrapper. Quella che secondo me è la vera soluzione è un'interfaccia
stabile e ben definita ed allora l'uso di attributi pubblici non sono
non è sconsigliato ma fa parte dell'interfaccia di programmazione. Nel
seguente, e ultimo, pezzo di codice, 'buildProtocol' non è un metodo
invocato direttamente dal codice scritto dal programmatore (ultimo) ma
dal framework e l'attributo 'protocol' invece _deve_ essere
sovrascritto:

	class Factory:

		protocol = None

		def buildProtocol(self):
			p = self.protocol()
			p.factory = self
			return p

Una classe figlia specializzata può ridefinire 'protocol' nel corpo
della classe, ma se il comportamento della classe base è sufficiente per
gli scopi è possibile ridefinire semplicemente l'attributo:

	class MyFactory:

		protocol = MyProtocol

		...

	factory = MyFactory()
	do_something(factory)

piuttosto che:

	factory = Factory()
	factory.protocol = MyProtocol
	do_something(factory)

Mi scuso perché nonostante la lunghezza non ti ho dato una risposta ma
forse almeno qualche spunto.

ciao
m.


Maggiori informazioni sulla lista Python