-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrefactoring.html
479 lines (403 loc) · 33.4 KB
/
refactoring.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
<!DOCTYPE html>
<meta charset=utf-8>
<title>Refactoring - Immersione in Python 3</title>
<!--[if IE]><script src=j/html5.js></script><![endif]-->
<link rel=stylesheet href=dip3.css>
<style>
body{counter-reset:h1 10}
</style>
<link rel=stylesheet media='only screen and (max-device-width: 480px)' href=mobile.css>
<link rel=stylesheet media=print href=print.css>
<meta name=viewport content='initial-scale=1.0'>
<form action=http://www.google.com/cse><div><input type=hidden name=cx value=014021643941856155761:l5eihuescdw><input type=hidden name=ie value=UTF-8> <input type=search name=q size=25 placeholder="powered by Google™"> <input type=submit name=root value=Search></div></form>
<p>Voi siete qui: <a href=index.html>Inizio</a> <span class=u>‣</span> <a href=indice.html#refactoring>Immersione in Python 3</a> <span class=u>‣</span>
<p id=level>Livello di difficoltà: <span class=u title=avanzato>♦♦♦♦♢</span>
<h1>Refactoring</h1>
<blockquote class=q>
<p><span class=u>❝</span> Dopo aver suonato una quantità immensa di note e ancora altre note, è la semplicità che emerge come l’autentico sigillo dell’arte. <span class=u>❞</span><br>— <a href=http://en.wikiquote.org/wiki/Fr%C3%A9d%C3%A9ric_Chopin>Frédéric Chopin</a>
</blockquote>
<p id=toc>
<h2 id=divingin>Immersione!</h2>
<p class=f>Nonostante facciate del vostro meglio per scrivere <a href=test-di-unità.html>test di unità</a> completi, i bug capitano. Cosa intendo quando parlo di “bug”? Un bug è un test che non avete ancora scritto.
<pre class=screen><samp class=p>>>> </samp><kbd class=pp>import roman7</kbd>
<a><samp class=p>>>> </samp><kbd class=pp>roman7.from_roman('')</kbd> <span class=u>①</span></a>
<samp class=pp>0</samp></pre>
<ol>
<li>Questo è un bug. Una stringa vuota dovrebbe sollevare un’eccezione di tipo <code>InvalidRomanNumeralError</code>, esattamente come ogni altra sequenza di caratteri che non rappresenta un numero romano valido.
</ol>
<p>Dopo aver riprodotto il bug, e prima di correggerlo, dovreste scrivere un test che fallisce, mettendo così il bug in evidenza.
<pre class=pp><code>class FromRomanBadInput(unittest.TestCase):
.
.
.
def testBlank(self):
'''from_roman dovrebbe fallire con una stringa vuota'''
<a> self.assertRaises(roman6.InvalidRomanNumeralError, roman6.from_roman, '') <span class=u>①</span></a></code></pre>
<ol>
<li>Qui le cose sono piuttosto semplici. Chiamate <code>from_roman()</code> con una stringa vuota e vi assicurate che sollevi un’eccezione di tipo <code>InvalidRomanNumeralError</code>. La parte difficile è stata trovare il bug; ora che lo conoscete, scrivere un test per verificarlo è la parte facile.
</ol>
<p>Dato che il vostro codice ha un bug e ora avete un test che verifica quel bug, il test fallirà:
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest8.py -v</kbd>
<samp>from_roman dovrebbe fallire con una stringa vuota ... FAIL
from_roman dovrebbe fallire con antecedenti malformati ... ok
from_roman dovrebbe fallire con coppie di cifre ripetute ... ok
from_roman dovrebbe fallire con cifre ripetute troppe volte ... ok
from_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
from_roman(to_roman(n))==n per tutti gli n ... ok
to_roman dovrebbe fallire con numeri negativi ... ok
to_roman dovrebbe fallire con numeri non interi ... ok
to_roman dovrebbe fallire con numeri grandi ... ok
to_roman dovrebbe fallire con il numero 0 ... ok
======================================================================
FAIL: from_roman dovrebbe fallire con una stringa vuota
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest8.py", line 117, in test_blank
self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, '')
<mark>AssertionError: InvalidRomanNumeralError not raised by from_roman</mark>
----------------------------------------------------------------------
Ran 11 tests in 0.171s
FAILED (failures=1)</samp></pre>
<p><em>Adesso</em> potete correggere il bug.
<pre class=pp><code>def from_roman(s):
'''converte un numero romano in un intero'''
<a> if not s: <span class=u>①</span></a>
raise InvalidRomanNumeralError('La stringa in ingresso non può essere vuota')
if not re.search(romanNumeralPattern, s):
<a> raise InvalidRomanNumeralError('Numero romano non valido: {}'.format(s)) <span class=u>②</span></a>
result = 0
index = 0
for numeral, integer in romanNumeralMap:
while s[index:index+len(numeral)] == numeral:
result += integer
index += len(numeral)
return result</code></pre>
<ol>
<li>Sono necessarie solo due righe di codice: un controllo esplicito per una stringa vuota e un’istruzione di <code>raise</code>.
<li>Non penso di aver ancora menzionato questa particolarità da nessuna parte in questo libro, quindi vi serva come ultima lezione sulla <a href=stringhe.html#formatting-strings>formattazione di stringhe</a>. A partire da Python 3.1, potete omettere i numeri quando usate gli indici posizionali in una specifica di formato. Questo vuol dire che, invece di usare la specifica di formato <code>{0}</code> per fare riferimento al primo parametro del metodo <code>format()</code>, potete semplicemente usare <code>{}</code>, che Python completerà con l’indice posizionale corretto. Questo funziona con qualsiasi numero di argomenti: la prima <code>{}</code> è <code>{0}</code>, la seconda <code>{}</code> è <code>{1}</code>, e così via.
</ol>
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest8.py -v</kbd>
<a><samp>from_roman dovrebbe fallire con una stringa vuota ... ok</samp> <span class=u>①</span></a>
<samp>from_roman dovrebbe fallire con antecedenti malformati ... ok
from_roman dovrebbe fallire con coppie di cifre ripetute ... ok
from_roman dovrebbe fallire con cifre ripetute troppe volte ... ok
from_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
from_roman(to_roman(n))==n per tutti gli n ... ok
to_roman dovrebbe fallire con numeri negativi ... ok
to_roman dovrebbe fallire con numeri non interi ... ok
to_roman dovrebbe fallire con numeri grandi ... ok
to_roman dovrebbe fallire con il numero 0 ... ok
----------------------------------------------------------------------
Ran 11 tests in 0.156s
</samp>
<a><samp>OK</samp> <span class=u>②</span></a></pre>
<ol>
<li>Il test per la stringa vuota ora ha successo, quindi il bug è stato corretto.
<li>Tutti gli altri test hanno ancora successo, il che significa che la correzione del bug non ha introdotto nuovi errori. Potete smettere di programmare.
</ol>
<p>Programmare in questo modo non rende più facile la correzione dei bug. Per i bug più semplici (come questo) bastano test semplici; bug complessi richiederanno test complessi. In un processo di sviluppo guidato dai test, potrebbe <em>sembrare</em> che ci voglia più tempo per correggere un bug, dato che prima avete bisogno di esprimere la sostanza del bug sotto forma di codice (per scrivere il test) e poi dovete correggere il bug vero e proprio. Successivamente, se il test non ha immediatamente successo, avete bisogno di capire se la correzione era sbagliata o se è il test stesso ad avere un bug. Comunque, nel lungo periodo, lavorare spostandovi continuamente avanti e indietro tra il codice del test e il codice che state collaudando vi ricompenserà, perché rende molto più probabile che i bug vengano corretti al primo tentativo. In più, visto che potete facilmente rieseguire <em>tutti</em> i test insieme a quello nuovo, avete molte meno probabilità di guastare il vecchio codice quando state correggendo quello nuovo. Il test di unità di oggi è il test di regressione di domani.
<p class=a>⁂
<h2 id=changing-requirements>Gestire il cambiamento dei requisiti</h2>
<p>Nonostante facciate del vostro meglio per tenere i vostri committenti con i piedi ben piantati per terra in modo da ottenere requisiti precisi sotto la minaccia di orribili torture a base di forbici e cera calda, i requisiti cambieranno. La maggior parte dei committenti non sa cosa vuole fino a quando non la vede, e anche se lo sapesse non sarebbe così brava a descrivere ciò che vuole in maniera sufficientemente precisa da risultare utile. E anche se lo fosse, vorrebbe comunque di più nella versione successiva. Quindi siate preparati ad aggiornare i vostri test man mano che i requisiti cambiano.
<p>Supponete per esempio di volere espandere l’intervallo dei numeri romani gestito dalle funzioni di conversione. Normalmente nessun carattere in un numero romano può essere ripetuto più di tre volte di seguito. Ma i Romani erano disposti a fare uno strappo a quella regola per poter rappresentare <code>4000</code> con 4 <code>M</code> di seguito. Se fate questa modifica sarete in grado di allargare l’intervallo di numeri convertibili da <code>1..3999</code> a <code>1..4999</code>. Ma prima è necessario fare alcuni cambiamenti ai vostri test.
<p class=d>[<a href=esempi/roman8.py>scarica <code>roman8.py</code></a>]
<pre class=pp><code>class KnownValues(unittest.TestCase):
known_values = ( (1, 'I'),
.
.
.
(3999, 'MMMCMXCIX'),
<a> (4000, 'MMMM'), <span class=u>①</span></a>
(4500, 'MMMMD'),
(4888, 'MMMMDCCCLXXXVIII'),
(4999, 'MMMMCMXCIX') )
class ToRomanBadInput(unittest.TestCase):
def test_too_large(self):
'''to_roman dovrebbe fallire con numeri grandi'''
<a> self.assertRaises(roman8.OutOfRangeError, roman8.to_roman, 5000) <span class=u>②</span></a>
.
.
.
class FromRomanBadInput(unittest.TestCase):
def test_too_many_repeated_numerals(self):
'''from_roman dovrebbe fallire con cifre ripetute troppe volte'''
<a> for s in ('MMMMM', 'DD', 'CCCC', 'LL', 'XXXX', 'VV', 'IIII'): <span class=u>③</span></a>
self.assertRaises(roman8.InvalidRomanNumeralError, roman8.from_roman, s)
.
.
.
class RoundtripCheck(unittest.TestCase):
def test_roundtrip(self):
'''from_roman(to_roman(n))==n per tutti gli n'''
<a> for integer in range(1, 5000): <span class=u>④</span></a>
numeral = roman8.to_roman(integer)
result = roman8.from_roman(numeral)
self.assertEqual(integer, result)</code></pre>
<ol>
<li>I valori noti già esistenti non cambiano (sono ancora tutti valori ragionevoli da collaudare), ma avete bisogno di aggiungerne alcuni nell’intervallo delle quattro migliaia. Qui ho incluso <code>4000</code> (il più corto), <code>4500</code> (il secondo più corto), <code>4888</code> (il più lungo) e <code>4999</code> (il più grande).
<li>La definizione di “numero grande” è cambiata. Questo test invocava <code>to_roman()</code> con <code>4000</code> e si aspettava un errore, ma ora che i valori <code>4000-4999</code> sono validi avete bisogno di alzare la soglia a <code>5000</code>.
<li>Anche la definizione di “cifre ripetute troppe volte” è cambiata. Questo test invocava <code>from_roman()</code> con <code>'MMMM'</code> e si aspettava un errore, ma ora che <code>MMMM</code> è considerato un numero romano valido avete bisogno di alzare il valore limite a <code>'MMMMM'</code>.
<li>Il test di consistenza controlla ogni numero nell’intervallo, da <code>1</code> fino a <code>3999</code>. Dato che ora l’intervallo si è allargato, anche questo ciclo <code>for</code> deve essere aggiornato per arrivare fino a <code>4999</code>.
</ol>
<p>Ora i vostri test sono aggiornati rispetto ai nuovi requisiti, ma il vostro codice non lo è, quindi quando li eseguite aspettatevi diversi fallimenti.
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman dovrebbe fallire con una stringa vuota ... ok
from_roman dovrebbe fallire con antecedenti malformati ... ok
from_roman dovrebbe fallire con ingressi diversi da stringhe ... ok
from_roman dovrebbe fallire con coppie di cifre ripetute ... ok
from_roman dovrebbe fallire con cifre ripetute troppe volte ... ok
<a>from_roman dovrebbe dare un risultato noto con un ingresso noto ... ERROR <span class=u>①</span></a>
<a>to_roman dovrebbe dare un risultato noto con un ingresso noto ... ERROR <span class=u>②</span></a>
<a>from_roman(to_roman(n))==n per tutti gli n ... ERROR <span class=u>③</span></a>
to_roman dovrebbe fallire con numeri negativi ... ok
to_roman dovrebbe fallire con numeri non interi ... ok
to_roman dovrebbe fallire con numeri grandi ... ok
to_roman dovrebbe fallire con il numero 0 ... ok
======================================================================
ERROR: from_roman dovrebbe dare un risultato noto con un ingresso noto
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 82, in test_from_roman_known_values
result = roman9.from_roman(numeral)
File "C:\home\diveintopython3\esempi\roman9.py", line 60, in from_roman
raise InvalidRomanNumeralError('Numero romano non valido: {0}'.format(s))
<mark>roman9.InvalidRomanNumeralError: Numero romano non valido: MMMM</mark>
======================================================================
ERROR: to_roman dovrebbe dare un risultato noto con un ingresso noto
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 76, in test_to_roman_known_values
result = roman9.to_roman(integer)
File "C:\home\diveintopython3\esempi\roman9.py", line 42, in to_roman
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 3999)")
<mark>roman9.OutOfRangeError: numero fuori dall'intervallo (deve essere tra 1 e 3999)</mark>
======================================================================
ERROR: from_roman(to_roman(n))==n per tutti gli n
----------------------------------------------------------------------
Traceback (most recent call last):
File "romantest9.py", line 131, in testSanity
numeral = roman9.to_roman(integer)
File "C:\home\diveintopython3\esempi\roman9.py", line 42, in to_roman
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 3999)")
<mark>roman9.OutOfRangeError: numero fuori dall'intervallo (deve essere tra 1 e 3999)</mark>
----------------------------------------------------------------------
Ran 12 tests in 0.171s
FAILED (errors=3)</samp></pre>
<ol>
<li>Il test sui valori noti per <code>from_roman()</code> fallirà non appena arriva a <code>'MMMM'</code>, perché <code>from_roman()</code> pensa ancora che questo sia un numero romano non valido.
<li>Il test sui valori noti per <code>to_roman()</code> fallirà non appena arriva a <code>4000</code>, perché <code>to_roman()</code> pensa ancora che questo numero sia fuori dall’intervallo di validità.
<li>Anche il controllo di consistenza fallirà non appena arriva a <code>4000</code>, perché <code>to_roman()</code> pensa ancora che questo numero sia fuori dall’intervallo di validità.
</ol>
<p>Ora che i vostri test falliscono a causa dei nuovi requisiti, potete pensare a correggere il codice per riallinearlo con i test. (Quando cominciate a utilizzare i test di unità per la prima volta, potrebbe sembrarvi strano che il codice sotto collaudo non sia mai “avanti” rispetto ai test. Mentre il codice è indietro avete ancora del lavoro da fare, e appena lo avete rimesso in pari con i test potete smettere di programmare. Dopo averci fatto l’abitudine, vi chiederete come facevate a programmare senza i test.)
<p class=d>[<a href=esempi/roman9.py>scarica <code>roman9.py</code></a>]
<pre class=pp><code>roman_numeral_pattern = re.compile('''
^ # inizio della stringa
<a> M{0,4} # migliaia - da 0 a 4 M <span class=u>①</span></a>
(CM|CD|D?C{0,3}) # centinaia - 900 (CM), 400 (CD), 0-300 (da 0 a 3 C),
# o 500-800 (D, seguita da 0 fino a 3 C)
(XC|XL|L?X{0,3}) # decine - 90 (XC), 40 (XL), 0-30 (da 0 a 3 X),
# o 50-80 (L, seguita da 0 fino a 3 X)
(IX|IV|V?I{0,3}) # unità - 9 (IX), 4 (IV), 0-3 (da 0 a 3 I),
# o 5-8 (V, seguita da 0 fino a 3 I)
$ # fine della stringa
''', re.VERBOSE)
def to_roman(n):
'''converte un intero in un numero romano'''
<a> if not (0 < n < 5000): <span class=u>②</span></a>
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 4999)")
if not isinstance(n, int):
raise NotIntegerError('numeri non interi non possono essere convertiti')
result = ''
for numeral, integer in roman_numeral_map:
while n >= integer:
result += numeral
n -= integer
return result
def from_roman(s):
.
.
.</code></pre>
<ol>
<li>Non avete bisogno di fare alcun cambiamento alla funzione <code>from_roman()</code>. L’unico cambiamento è a <var>roman_numeral_pattern</var>. Se guardate da vicino la prima sezione dell’espressione regolare, noterete che ho cambiato il massimo numero di caratteri <code>M</code> opzionali da <code>3</code> a <code>4</code>. Questa modifica renderà legali i numeri romani equivalenti fino a <code>4999</code> anziché <code>3999</code>. La funzione <code>from_roman()</code> è in effetti completamente generica: controlla semplicemente i caratteri ripetuti nei numeri romani e li somma, senza preoccuparsi di quante volte si ripetano. In precedenza la funzione non poteva gestire <code>'MMMM'</code> solo perché l’avevate esplicitamente fermata utilizzando la corrispondenza con il pattern dell’espressione regolare.
<li>La funzione <code>to_roman()</code> ha bisogno solo di un piccolo cambiamento nel controllo sull’intervallo. Laddove controllavate che fosse <code>0 < n < 4000</code>, ora controllate che sia <code>0 < n < 5000</code>. Anche il messaggio di errore che sollevate tramite <code>raise</code> va modificato per riflettere il nuovo intervallo accettabile (<code>tra 1 e 4999</code> anziché <code>tra 1 e 3999</code>). Non avete bisogno di fare alcun cambiamento al resto della funzione, perché gestisce già i nuovi casi. (Aggiunge tranquillamente <code>'M'</code> per ogni migliaia che trova; ricevuto in ingresso <code>4000</code>, restituirà in uscita <code>'MMMM'</code>. In precedenza la funzione non poteva farlo solo perché l’avevate esplicitamente fermata tramite il controllo sull’intervallo.)
</ol>
<p>Potreste non essere convinti che questi due piccoli cambiamenti siano tutto ciò di cui avete bisogno. Ehi, non siete obbligati a fidarvi della mia parola: controllate voi stessi.
<pre class='nd screen'>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest9.py -v</kbd>
<samp>from_roman dovrebbe fallire con una stringa vuota ... ok
from_roman dovrebbe fallire con antecedenti malformati ... ok
from_roman dovrebbe fallire con ingressi diversi da stringhe ... ok
from_roman dovrebbe fallire con coppie di cifre ripetute ... ok
from_roman dovrebbe fallire con cifre ripetute troppe volte ... ok
from_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
from_roman(to_roman(n))==n per tutti gli n ... ok
to_roman dovrebbe fallire con numeri negativi ... ok
to_roman dovrebbe fallire con numeri non interi ... ok
to_roman dovrebbe fallire con numeri grandi ... ok
to_roman dovrebbe fallire con il numero 0 ... ok
----------------------------------------------------------------------
Ran 12 tests in 0.203s
<a>OK <span class=u>①</span></a></samp></pre>
<ol>
<li>Tutti i test hanno successo. Potete smettere di programmare.
</ol>
<p>Un insieme completo di test di unità ci permette di non dover mai fare affidamento su un programmatore che dice: “Fidati di me.”
<p class=a>⁂
<h2 id=refactoring>Refactoring</h2>
<p>La cosa migliore di un collaudo di unità completo non è la sensazione che provate quando tutti i vostri test hanno finalmente successo, e nemmeno la sensazione che provate quando qualcun altro vi accusa di aver guastato il suo codice e voi potete effettivamente <em>dimostrare</em> di non averlo fatto. La cosa migliore dei test di unità è che vi danno la libertà di applicare sistematicamente il refactoring.
<p>Il refactoring è il procedimento tramite il quale si modifica codice già funzionante allo scopo di farlo funzionare meglio. Di solito, “meglio” significa “più velocemente”, ma può anche voler dire “usando meno memoria”, oppure “usando meno spazio su disco”, o semplicemente “in maniera più elegante”. Qualsiasi cosa voglia dire per voi, per il vostro progetto, nel vostro ambiente, il refactoring è importante per la salute a lungo termine di qualsiasi programma.
<p>Nel nostro caso, “meglio” significa sia “più velocemente” che “in maniera più facile da mantenere”. Nello specifico, la funzione <code>from_roman()</code> è lenta e più complessa di quanto vorrei, a causa di quella grossa e disgustosa espressione regolare che usate per validare i numeri romani. Ora, potreste pensare: “Certo, l’espressione regolare è lunga e intricata, ma in quale altro modo si suppone che io controlli che una stringa arbitraria sia un numero romano valido?”
<p>Risposta: di numeri romani validi ce ne sono solo 5000; perché non costruite semplicemente una tabella di ricerca? Questa idea diventa ancora migliore nel momento in cui realizzate che <em>non avete per niente bisogno di usare un’espressione regolare</em>. Mentre costruite la tabella di ricerca per convertire i numeri interi in numeri romani potete costruire la tabella di ricerca inversa per convertire i numeri romani in interi. Nel momento in cui avete bisogno di controllare se una stringa arbitraria è un numero romano valido avrete già raccolto tutti i numeri romani validi. La “verifica” si riduce a un singolo accesso a un dizionario.
<p>E la cosa migliore di tutte è che avete già un insieme completo di test di unità. Potete cambiare metà del codice nel modulo ma i test di unità rimarranno gli stessi. Questo significa che potete dimostrare — a voi stessi e agli altri — che il nuovo codice funziona tanto bene quanto quello originale.
<p class=d>[<a href=esempi/roman10.py>scarica <code>roman10.py</code></a>]
<pre class=pp><code>class OutOfRangeError(ValueError): pass
class NotIntegerError(ValueError): pass
class InvalidRomanNumeralError(ValueError): pass
roman_numeral_map = (('M', 1000),
('CM', 900),
('D', 500),
('CD', 400),
('C', 100),
('XC', 90),
('L', 50),
('XL', 40),
('X', 10),
('IX', 9),
('V', 5),
('IV', 4),
('I', 1))
to_roman_table = [ None ]
from_roman_table = {}
def to_roman(n):
'''converte un intero in un numero romano'''
if not (0 < n < 5000):
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 4999)")
if int(n) != n:
raise NotIntegerError('numeri non interi non possono essere convertiti')
return to_roman_table[n]
def from_roman(s):
'''converte un numero romano in un intero'''
if not isinstance(s, str):
raise InvalidRomanNumeralError("L'ingresso deve essere una stringa")
if not s:
raise InvalidRomanNumeralError('La stringa in ingresso non può essere vuota')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Numero romano non valido: {0}'.format(s))
return from_roman_table[s]
def build_lookup_tables():
def to_roman(n):
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result
for integer in range(1, 5000):
roman_numeral = to_roman(integer)
to_roman_table.append(roman_numeral)
from_roman_table[roman_numeral] = integer
build_lookup_tables()</code></pre>
<p>Dividiamo il programma in frammenti più comprensibili. Teoricamente, la riga più importante è l’ultima:
<pre class='nd pp'><code>build_lookup_tables()</code></pre>
<p>Noterete che è una chiamata di funzione, ma non è preceduta da alcuna istruzione <code>if</code>. Questo non è un blocco <code>if __name__ == '__main__'</code>; la funzione viene invocata <em>quando il modulo è importato</em>. (È importante capire che i moduli sono importati solamente una volta e poi vengono salvati in memoria. Se importate un modulo già importato non succede nulla. Quindi questo codice verrà invocato solo la prima volta che importate questo modulo.)
<p>Cosa fa la funzione <code>build_lookup_tables()</code>? Sono contento che lo abbiate chiesto.
<pre class=pp><code>to_roman_table = [ None ]
from_roman_table = {}
.
.
.
def build_lookup_tables():
<a> def to_roman(n): <span class=u>①</span></a>
result = ''
for numeral, integer in roman_numeral_map:
if n >= integer:
result = numeral
n -= integer
break
if n > 0:
result += to_roman_table[n]
return result
for integer in range(1, 5000):
<a> roman_numeral = to_roman(integer) <span class=u>②</span></a>
<a> to_roman_table.append(roman_numeral) <span class=u>③</span></a>
from_roman_table[roman_numeral] = integer</code></pre>
<ol>
<li>Questa è una tecnica di programmazione ingegnosa… forse troppo ingegnosa. La funzione <code>to_roman()</code> è definita sopra; accede alla tabella di ricerca e ne restituisce i valori. Ma la funzione <code>build_lookup_tables()</code> ridefinisce la funzione <code>to_roman()</code> per fare effettivamente qualcosa di più (come accadeva nell’esempio precedente, prima che aggiungeste una tabella di ricerca). Nell’ambito della funzione <code>build_lookup_tables()</code>, una chiamata a <code>to_roman()</code> invocherà questa versione ridefinita. Una volta usciti dalla funzione <code>build_lookup_tables()</code>, la versione ridefinita scompare — essa è definita solo nell’ambito locale della funzione <code>build_lookup_tables()</code>.
<li>Questa riga di codice chiamerà la funzione <code>to_roman()</code> ridefinita, che calcola effettivamente il numero romano.
<li>Una volta che avete ottenuto il risultato (dalla funzione <code>to_roman()</code> ridefinita), potete aggiungere l’intero e il suo numero romano equivalente a entrambe le tabelle di ricerca.
</ol>
<p>Una volta che le tabelle di ricerca sono popolate, il resto del codice è sia semplice che veloce.
<pre class=pp><code>def to_roman(n):
'''converte un intero in un numero romano'''
if not (0 < n < 5000):
raise OutOfRangeError("numero fuori dall'intervallo (deve essere tra 1 e 4999)")
if int(n) != n:
raise NotIntegerError('numeri non interi non possono essere convertiti')
<a> return to_roman_table[n] <span class=u>①</span></a>
def from_roman(s):
'''converte un numero romano in un intero'''
if not isinstance(s, str):
raise InvalidRomanNumeralError("L'ingresso deve essere una stringa")
if not s:
raise InvalidRomanNumeralError('La stringa in ingresso non può essere vuota')
if s not in from_roman_table:
raise InvalidRomanNumeralError('Numero romano non valido: {0}'.format(s))
<a> return from_roman_table[s] <span class=u>②</span></a></code></pre>
<ol>
<li>Dopo aver fatto gli stessi controlli di prima sugli estremi, la funzione <code>to_roman()</code> si limita a trovare il valore appropriato nella tabella di ricerca e a restituirlo.
<li>Similmente, la funzione <code>from_roman()</code> si riduce a qualche controllo sugli estremi e a una riga di codice. Niente più espressioni regolari. Niente più cicli. Conversione di complessità O(1) da numeri interi in numeri romani e viceversa.
</ol>
<p>Ma funziona? Certo che sì, sì che funziona. E posso dimostrarlo.
<pre class=screen>
<samp class=p>you@localhost:~/diveintopython3/esempi$ </samp><kbd>python3 romantest10.py -v</kbd>
<samp>from_roman dovrebbe fallire con una stringa vuota ... ok
from_roman dovrebbe fallire con antecedenti malformati ... ok
from_roman dovrebbe fallire con ingressi diversi da stringhe ... ok
from_roman dovrebbe fallire con coppie di cifre ripetute ... ok
from_roman dovrebbe fallire con cifre ripetute troppe volte ... ok
from_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
to_roman dovrebbe dare un risultato noto con un ingresso noto ... ok
from_roman(to_roman(n))==n per tutti gli n ... ok
to_roman dovrebbe fallire con numeri negativi ... ok
to_roman dovrebbe fallire con numeri non interi ... ok
to_roman dovrebbe fallire con numeri grandi ... ok
to_roman dovrebbe fallire con il numero 0 ... ok
----------------------------------------------------------------------
<a>Ran 12 tests in 0.031s <span class=u>①</span></a>
OK</samp></pre>
<ol>
<li>Non che lo abbiate chiesto, ma è anche veloce! Tipo, almeno 10× più veloce. Naturalmente, questo non è un confronto onesto, perché questa versione impiega più tempo a essere importata (quando costruisce le tabelle di ricerca). Ma dato che viene importata una volta sola, il costo di inizializzazione è ammortizzato su tutte le chiamate delle funzioni <code>to_roman()</code> e <code>from_roman()</code>. E dato che i test effettuano diverse migliaia di chiamate di funzione (il solo test di consistenza ne fa 10.000), questo risparmio si accumula molto velocemente!
</ol>
<p>Qual è la morale della storia?
<ul>
<li>La semplicità è una virtù.
<li>Specialmente quando sono coinvolte espressioni regolari.
<li>I test di unità possono darvi la sicurezza necessaria per effettuare il refactoring su larga scala.
</ul>
<p class=a>⁂
<h2 id=summary>Riepilogo</h2>
<p>Il collaudo di unità è un potente concetto che, se adeguatamente implementato, può sia ridurre i costi di manutenzione sia aumentare la flessibilità di ogni progetto a lungo termine. È anche importante capire che il collaudo di unità non è una panacea, un Risolutore Magico di Problemi, o la cosiddetta pallottola d’argento. Scrivere buoni test di unità è difficile e mantenerli aggiornati richiede disciplina (specialmente quando i committenti vi urlano dietro per farvi correggere bug critici). I test di unità non sostituiscono altre forme di collaudo, compresi i test funzionali, i test di integrazione e i test di accettazione per l’utente. Ma sono convenienti, e funzionano, e una volta che li avete visti funzionare vi chiederete come avete fatto a lavorare senza fino a quel momento.
<p>Questi pochi capitoli hanno coperto diversi argomenti, molti dei quali non erano nemmeno specifici per Python. Esistono framework per il collaudo di unità in molti linguaggi, e tutti vi richiedono di applicare una serie di pratiche basate sugli stessi concetti elementari:
<ul>
<li>progettate test che siano specifici, automatizzati e indipendenti;
<li>scrivete i test <em>prima</em> del codice che devono collaudare;
<li>scrivete test che utilizzino dati di ingresso accettabili e verifichino i risultati corretti;
<li>scrivete test che utilizzino dati di ingresso non accettabili e controllino le corrette risposte di fallimento;
<li>scrivete e aggiornate i test per riflettere nuovi requisiti;
<li>applicate sistematicamente il refactoring per migliorare prestazioni, scalabilità, leggibilità, manutenibilità, o qualsiasi altra -ilità che vi manca.
</ul>
<p class=v><a href=test-di-unità.html rel=prev title='indietro a “Test di unità”'><span class=u>☜</span></a> <a href=file.html rel=next title='avanti a “File”'><span class=u>☞</span></a>
<p class=c>© 2001–10 <a href=informazioni-sul-libro.html>Mark Pilgrim</a><br>
© 2009–10 <a href=informazioni-sulla-traduzione.html>Giulio Piancastelli</a> per la traduzione italiana
<script src=j/jquery.js></script>
<script src=j/prettify.js></script>
<script src=j/dip3.js></script>