GLoad
Lo script che presenteremo come esempio, si chiama gload ed è un
piccolo programma in grado di testare l'affidabilità di un server
web.
GLoad è in grado di inviare un numero X di richieste http
quasi contemporanee. Python è un linguaggio interpretato, e non possiede un
meccanismo affidabile di sincronizzazione dei thread: per questa
ragione, le richieste non possono essere contemporanee e all'aumentare
del numero di thread questo fenomeno è osservabile a occhio nudo.
GLoad è composto da due classi:
- GLoad che è formata dal solo costruttore, e che coordina i vari
client web
- Client che modella un semplicissimo client http
Vediamo come scrivere queste due classi in python.
Definizione delle classi in python
In python le classi sono dichiarate con la direttiva
class
con una struttura del tipo:
class NomeClasseDerivata(Superclasse):
<statement-1>
.
.
.
<statement-N>
All'interno del blocco class è possibile definire i metodi d'istanza.
I metodi sono funzioni, il cui primo parametro è
sempre
l'istanza della classe di cui si invoca il metodo. Alcuni metodi
hanno un particolare significato per python:essi sono riconoscibili
perché il loro nome inizia e termina con i caratteri '__' (doppia
sottolineatura).
Ad esempio, il costruttore si chiama sempre
__init__
Esempio di costruttore
Prendiamo in esame la classe GLoad: questa classe è formata dal solo
costruttore, che prende in input due parametri: l'url da aprire e il
numero di thread da attivare.
__init__ quindi per prima cosa istanzia un singolo client, in modo da
ottenere il contenuto della pagina (che userà per condurre il test).
Si noti che mentre
tlist
è una variabile locale al metodo
__init__
,
self.expectedResult
è
una variabile d'istanza.
Analizziamo ora il codice dell'altra classe:
class Client(Thread):
""" Simulate a Web Client """
def __init__(self,url,silent):
# We must call the father:
Thread.__init__(self)
self.url=url
self.silent=silent
def run(self):
self.error=0
self.text=""
if not self.silent:
print self,"[Running]"
try:
f = urllib.urlopen(self.url)
self.text=f.read()
f.close()
if not self.silent:
print self,"[END]", len(self.text)
except IOError:
self.error=1
if not self.silent:
print self,"[ERR]"
def isOk(self):
if(self.error==1):
return 0
return 1
def isResultMatch(self,textExpected):
return self.text == textExpected
def getResult(self):
return self.text
Uso dell' ereditarietà
La classe Client deriva dalla classe Thread dichiarata nel package
threading i cui simboli sono tutti importati con l'istruzione
from threading import *
.
Come in Java, la classe Client implementa un metodo run() che è
chiamato da python quando il thread è fatto partire.
Il costruttore di Client si preoccupa di chiamare il metodo della
superclasse Thread prima di fare qualsiasi cosa: questo garantisce che
il thread sia inizializzato correttamente.
Il costruttore in python si occupa anche di inizializzare e
dichiarare le variabili d'istanza url e silent.
Il metodo run() non fa altro che richiedere l'url e inserirne il
contenuto nella variabile self.text.
In caso di errore, il blocco try...except si occupa di
gestire la condizione di errore. La sintassi di questo blocco è assai
simile a quella usata in Java.
Per i dettagli si rimanda alla documentazione di python.
Il metodo
isResultMatch(stringa)
è usato dalla classe GLoad per
verificare che la risposta del server web sia corretta.
Questo perché alcuni server, in caso di picchi di carico possono
presentare una pagina di errore 500 o una pagina statica
personalizzata dallo webmaster.
Questi server rispondono comunque al chiamante, senza
errori di I/O.
Altri server invece, possono non rispondere o chiudere la socket dopo
aver accettato la connessione.
Attraverso il metodo
isOk()
è possibile sapere se la
richiesta è andata a buon fine.
Il metodo
getResult()
consente di ottenere il testo
ritornato dalla richiesta HTTP ed è usato dalla classe GLoad
per inizializzare
self.expectedResult
.
Differenze tra Python e gli altri linguaggi ad oggetti
Tutti i metodi in python, se fossero scritti in C++ sarebbero
dichiarati
virtual. Questo è vero anche per il linguaggio Java.
In C++ le funzioni virtual sono dichiarate nella classe padre e
ridefinite nella classe derivata. Poi a runtime le sottoclassi
implementano nel modo più appropriato i metodi virtual
(si parla di
pure polymorphism).
Python supporta una forma "ridotta" di ereditarietà. Se si dichiara
una classe nel seguente modo:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
La regola usata per cercare un riferimento all'attributo di classe è
del tipo da sinistra-a-destra.
Per cui se un attributo non è trovato in DerivedClassName, è cercato
prima in Base1, poi in Base2 e infine in Base3.
Questo significa che, contrariamente a quanto succede in C++ non è
possibile avere un conflitto sullo scope di un metodo!
Infatti in C++, se il metodo pippo() è dichiarato sia in Base1 che in
Base2, il compilatore richiede di risolvere l'ambiguità usando
l'operatore
::
.
Information Hiding
Python non prevede il concetto di identificatore "private";
c'è però una debole convenzione nominale.
Un identificatore che inizia con '__' è considerato privato.
Python si occupa di "rinominare" questi identificatori.
Per cui, per esempio se nella classe PGP dichiariamo __chiavePrivata
la variabile è rinominata in _PGP__chiavePrivata.
Questa regola è una semplice sostituzione e funziona sia per i metodi
che per le variabili d'istanza.
Il vantaggio di questo metodo è che si garantisce che queste
variabili non possono essere confuse con altre ereditate dalle
superclassi.
Purtroppo è possibile accedere a _PGP__chiavePrivata: basta
conoscerne il nome.
Questo meccanismo di protezione è tra i più deboli che esistano.
In Smalltalk80, che non ha un meccanismo per dichiarare metodi privati,
è però impossibile accedere alle variabili d'istanza dall'esterno.
Questo perché in programmazione orientata agli oggetti, l'integrità
dello stato dell'oggetto deve essere mantenuta!
Se infatti chiunque può modificare gli attributi di un oggetto
senza controllo è necessario affidarsi alla disciplina dei
programmatori e sperare che non si introducano bug.
Questo è sicuramente uno dei punti più deboli di python.
L'arcano float
Molti si chiederanno perché abbia scritto
print "Success%", ((noerrors/float(load))*100)
invece di
print "Success%", ((noerrors/load)*100)
Purtroppo in python le variabili sono di default INTERE. Siccome
noerrors e load sono intere, l'operatore di divisione NON ritorna un
valore float ma l'arrotonda ad un intero! Siccome per ovvie ragioni il
rapporto di cui sopra è sempre in [0,1] l'espressione risultante è
del tipo (0*100) oppure (1*100).
Affinchè il valore percentuale sia corretto, è necessario
forzare la conversione di una delle due variabili a float, attraverso
la funzione
float()
.
Conclusioni
Python offre un buon supporto alla programmazione ad oggetti.
Si osserva però che tale supporto non è marcato come in altri
linguaggi come C++, Java, Ruby e Smalltalk. Per questa ragione, pur
essendo possibile scrivere progetti di medie dimensioni in python, si
deve riflettere accuratamente prima di considerarne l'uso in progetti
con un discreto numero di sviluppatori da coordinare.
In quest'ultimo caso infatti, framework e linguaggi con concetti come
interfacce, template, information hiding possono risultare più
appropriati.
Concludendo, python rappresenta una buona alternativa rispetto agli
script di shell o a perl (ove non siano richieste le caratteristiche
peculiari di quest'ultimo).