Hlavní navigace

Použití nástroje Radon pro zjištění cyklomatické složitosti zdrojových kódů

26. 4. 2018
Doba čtení: 23 minut

Sdílet

Dnes si popíšeme vlastnosti nástroje Radon, který je určen pro zjišťování metrik zdrojových kódů napsaných v Pythonu. Mezi metriky zjišťované tímto nástrojem patří především cyklomatická složitost (CC) a index udržovatelnosti (MI).

Obsah

1. Metriky používané při vývoji a analýze aplikací

2. Cyklomatická složitost (Cyclomatic Complexity)

3. Nástroj Radon

4. Výpočet skóre cyklomatické složitosti jednotlivých programových bloků

5. Instalace nástroje Radon

6. Zjištění cyklomatické složitosti zdrojového kódu nástrojem Radon

7. Čtení výstupu produkovaného nástrojem Radon

8. Cyklomatická složitost základních programových konstrukcí

9. Jak probíhal výpočet?

10. Získání informací o cyklomatické složitosti u funkcí s vnořenými smyčkami a podmínkami

11. Cyklomatická složitost nechvalně proslulých zdrojových kódů

12. A na závěr … cyklomatická složitost nejhoršího kódu a nejnečitelnějšího kódu

13. Cyklomatická složitost != měřítko čitelného kódu

14. Úprava nástroje Radon pro vytvoření HTML stránky s výsledky

15. Zjištění indexu udržovatelnosti (Maintainability Index)

16. Výpočet indexu udržovatelnosti

17. Repositář s demonstračními příklady

18. Odkazy na Internetu

1. Metriky používané při vývoji a analýze aplikací

Při vývoji nových aplikací popř. při analýze již existujících aplikací je možné sledovat různé metriky popisující kvalitu celé aplikace. Kromě klasických metrik, které jsou zaměřeny spíše na výsledný produkt a na jeho chování při běhu (výkonnost, doba odezvy, střední doba selhání, …) či na procesy související s vývojem a údržbou produktu (cena, produktivita, …) se v některých případech používají i metriky, které se nějakým způsobem snaží popsat složitost zdrojových kódů a tím pádem i pracnost opravy chyb či přidávání nových vlastností. Z hlediska implementace je jednou z nejjednodušších metrik tohoto typu takzvaná cyklomatická složitost neboli cyclomatic complexity. Zjednodušeně řešeno se jedná o číslo, kterým se snažíme vyjádřit složitost programu nebo jednotlivých logických bloků, typicky funkcí, tříd a metod (popř. celých balíků). Obecně platí, že čím větší je cyklomatická složitost daného bloku (například funkce), tím více jednotkových testů je zapotřebí vytvořit a tím složitější jsou případné další úpravy kódu (či jen jeho prosté pochopení).

Ve vybrané části programu se cyklomatická složitost počítá pomocí grafu toku řízení toho programu: uzly grafu odpovídají neoddělitelným skupinám v programu (například tělu cyklu, podmínky). Orientované hrany odpovídají tomu, v jakém pořadí se skupiny příkazů budou provádět. Cyklomatickou složitost je možné aplikovat individuálně na vybrané funkce, moduly, metody nebo třídy [1].

2. Cyklomatická složitost (Cyclomatic Complexity)

Implementace měření cyklomatické složitosti je relativně snadná díky tomu, že se pouze zjišťuje počet možných cest ve zdrojovém kódu, tj. provádí se statická analýza, která nevyžaduje měření prováděné v běžící aplikaci. Typicky se nástroje pro měření cyklomatické složitosti zaměřují na zjišťování počtu rozhodovacích konstrukcí (if-then, if-then-else, switch), programových smyček (while, repeat, for, generátorové notace seznamu), konstrukcí pro zpracování výjimek (try-catch-finally/try-except-finally) a někdy se složitost zvyšuje i s každou booleovskou operací v podmínce nebo programové smyčce popř. pro příkaz assert (ten totiž také rozvětvuje běh programu). Tyto informace lze velmi snadno zjistit z abstraktního syntaktického stromu (AST – Abstract Syntax Tree), což je i případ nástroje nazvaného Radon, jehož možnosti budou popsány v navazujících kapitolách.

3. Nástroj Radon

Jedním z užitečných a přitom velmi jednoduše použitelných nástrojů určených pro měření cyklomatické složitosti a současně i indexu udržovatelnosti zdrojových kódů psaných v Pythonu je aplikace nazvaná Radon. Jedná se o nástroj ovládaný z příkazové řádky, který je založený na nepřímé analýze kódu s využitím abstraktního syntaktického stromu (AST). To znamená, že pokud tento nástroj spustíme nad nějakým projektem, je nejdříve provedena lexikální analýza zdrojových kódů, následně syntaktická analýza, jejím výsledkem je AST a cyklomatická složitost je zjišťována při průchodu tímto stromem. Nástroj Radon rozděluje celý zdrojový kód na bloky – funkce, třídy a metody – a počítá pro každý blok celočíselné skóre. Taktéž rozděluje bloky do šesti kategorií A-F na základě dosaženého skóre, takže je možné velmi snadno zjistit, které bloky vyžadují úpravy (refaktoring).

Poznámka: skóre každého bloku je přirozené číslo, ovšem celková cyklomatická složitost se spočítá jako průměr skóre všech bloků a tudíž se jedná o reálné číslo větší než 1 (shora neomezeno, prakticky se však přes 50 nedostanete).

4. Výpočet skóre cyklomatické složitosti jednotlivých programových bloků

Výchozí skóre každého bloku je nastaveno na jedničku a při procházení AST se skóre zvyšuje podle toho, jakou programovou konstrukci Radon našel. Všechny důležité jazykové konstrukce Pythonu jsou vypsány v následující tabulce i s vysvětlením, kdy a proč je skóre započítáno:

Konstrukce Změna skóre Význam
if +1 zvyšuje se počet cest, kterými lze kódem projít
elif +1 zvyšuje se počet cest, kterými lze kódem projít
else +0 nemá žádný význam z hlediska složitosti (je započteno u if)
for +1 na začátku programové smyčky je rozhodovací konstrukce
while +1 na začátku programové smyčky je rozhodovací konstrukce
except +1 zvyšuje se počet cest, kterými lze kódem projít
finally +0 žádný význam z hlediska složitosti (provede se vždy)
with +1 zhruba odpovídá bloku try/except
assert +1 svým chováním odpovídá rozhodovací konstrukci if
generátorová notace seznamu +1 svým chováním odpovídá smyčce for
pravdivostní operátor +1 každý binární operátor přidává další bod, v němž se rozhoduje o dalším chování
Poznámka: díky jednoduché syntaxi Pythonu nebylo nutné počítat skóre u bloků typu switch-case, což je již složitější a mnoho vývojářů a testerů nesouhlasí s tím, aby každý blok case zvyšoval skóre o jedničku, protože mnoho rozhodovacích konstrukcí switch-case je velmi přehledných.

5. Instalace nástroje Radon

Instalace nástroje Radon je snadná, protože se jedná o projekt naprogramovaný pouze v Pythonu, tj. obejdeme se bez nutnosti překladu nativních knihoven atd. Samotná instalace bude provedena do domácího adresáře právě přihlášeného uživatele, což je zajištěno přepínačem –user. Kromě vlastního zdrojového kódu knihovny se ještě nainstaluje skript nazvaný radon, a to většinou do adresáře ~/.local/bin/:

$ pip3 install --user radon

Průběh instalace může vypadat následovně (povšimněte si instalace závislých modulů a knihoven):

Downloading/unpacking radon
  Downloading radon-2.2.0-py2.py3-none-any.whl (46kB): 46kB downloaded
Downloading/unpacking colorama>=0.3,<0.4 (from radon)
  Downloading colorama-0.3.9-py2.py3-none-any.whl
Downloading/unpacking mando>=0.6,<0.7 (from radon)
  Downloading mando-0.6.4-py2.py3-none-any.whl
Downloading/unpacking flake8-polyfill (from radon)
  Downloading flake8_polyfill-1.0.2-py2.py3-none-any.whl
Requirement already satisfied (use --upgrade to upgrade): six in /home/tester/.local/lib/python3.4/site-packages (from mando>=0.6,<0.7->radon)
Downloading/unpacking flake8 (from flake8-polyfill->radon)
  Downloading flake8-3.5.0-py2.py3-none-any.whl (69kB): 69kB downloaded
Downloading/unpacking mccabe>=0.6.0,<0.7.0 (from flake8->flake8-polyfill->radon)
  Downloading mccabe-0.6.1-py2.py3-none-any.whl
Downloading/unpacking pyflakes>=1.5.0,<1.7.0 (from flake8->flake8-polyfill->radon)
  Downloading pyflakes-1.6.0-py2.py3-none-any.whl (227kB): 227kB downloaded
Downloading/unpacking pycodestyle>=2.0.0,<2.4.0 (from flake8->flake8-polyfill->radon)
  Downloading pycodestyle-2.3.1-py2.py3-none-any.whl (45kB): 45kB downloaded
Installing collected packages: radon, colorama, mando, flake8-polyfill, flake8, mccabe, pyflakes, pycodestyle
Successfully installed radon colorama mando flake8-polyfill flake8 mccabe pyflakes pycodestyle
Cleaning up...

To, zda je radon nainstalován korektně, lze zjistit snadno:

$ whereis radon
 
radon: /home/tester/.local/bin/radon

V případě, že předchozí příkaz nezobrazí žádný výstup (pouze „radon:“), je to většinou způsobeno tím, že adresář „.local/bin“ není přidán do proměnné prostředí PATH. V případě, že je binární soubor radon skutečně spustitelný, zjistíme přímočaře následujícím způsobem:

$ radon
 
usage: radon [-h] [-v] {cc,raw,mi} ...
 
positional arguments:
  {cc,raw,mi}
    cc           Analyze the given Python modules and compute Cyclomatic
                 Complexity (CC).
    raw          Analyze the given Python modules and compute raw metrics.
    mi           Analyze the given Python modules and compute the
                 Maintainability Index.
 
optional arguments:
  -h, --help     show this help message and exit
  -v, --version  show program's version number and exit

6. Zjištění cyklomatické složitosti zdrojového kódu nástrojem Radon

Podívejme se nyní na formát výstupu, který je produkovaný nástrojem Radon při analýze cyklomatické složitosti. Nejprve si naklonujte repositář s demonstračními skripty, protože příkaz radon budeme pouštět v tomto repositáři:

$ git clone https://github.com/tisnik/radon-examples.git
 
Cloning into 'radon-examples'...
remote: Counting objects: 18, done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 18 (delta 4), reused 15 (delta 4), pack-reused 0
Unpacking objects: 100% (18/18), done.
Checking connectivity... done.
 
$ cd radon-examples

Pro jednoduchý modul obsahující pouze funkce (a nikoli třídy s metodami) se cyklomatická složitost získá příkazem radon cc ., kde „cc“ znamená „Cyclomatic Complexity“ a tečka označuje aktuální adresář:

$ radon cc .

Výstup by měl vypadat přibližně takto:

genpassword.py
    F 3:0 genpassword - F
hello_world.py
    F 1:0 hello - A
bubble_sort.py
    F 1:0 bubbleSort - A
control_structures.py
    F 56:0 max2 - A
    F 25:0 if_elif_else - A
    F 34:0 nested_if_else - A
    F 44:0 max1 - A
    F 72:0 fibonacci2 - A
    F 12:0 one_if - A
    F 18:0 one_if_else - A
    F 65:0 fibonacci1 - A
    F 78:0 factorial1 - A
    F 86:0 factorial2 - A
    F 93:0 factorial3 - A
    F 1:0 nothing - A
    F 5:0 simple - A
mandelbrot.py
    F 5:0 calc_mandelbrot - A
v_poradi.py
    F 3:0 vporadi - B
mandelbrot_complex.py
    F 20:0 calc_mandelbrot - A

V případě, že se v analyzovaných zdrojových kódech nachází i třídy s metodami, bude výstup vypadat nepatrně odlišně:

cls/dxf_importer.py
    M 185:4 DxfImporter.process_entity - C
    M 103:4 DxfImporter.process_beginning_section - B
    M 163:4 DxfImporter.process_section_entities - B
    M 88:4 DxfImporter.process_beginning - A
    M 227:4 DxfImporter.store_entity - A
    C 22:0 DxfImporter - A
    M 48:4 DxfImporter.dxf_entry - A
    M 142:4 DxfImporter.process_section_blocks - A
    M 152:4 DxfImporter.process_section_block - A
    M 72:4 DxfImporter.import_dxf - A
    M 128:4 DxfImporter.process_section_header - A
    M 135:4 DxfImporter.process_section_tables - A
    M 179:4 DxfImporter.process_section_objects - A
    M 25:4 DxfImporter.__init__ - A
    M 59:4 DxfImporter.init_import - A
    M 239:4 DxfImporter.store_line - A
    M 242:4 DxfImporter.store_circle - A
    M 245:4 DxfImporter.store_arc - A
    M 249:4 DxfImporter.store_text - A

7. Čtení výstupu produkovaného nástrojem Radon

Výpis informací o cyklomatické složitosti jednoho programového bloku vypadá takto:

control_structures.py
    F 56:0 max2 - A (5)

Vidíme, že se nejdříve zobrazí jméno souboru obsahujícího zdrojový kód. Soubor je zobrazen i s cestou, což zlepšuje orientaci ve složitěji strukturovaných aplikacích. Následně se na dalších řádcích zobrazují informace o nalezených blocích kódu. Nejprve je vypsán typ bloku, což je jeden ze znaků „F“, „M“ a „C“  následujícím významem:

Označení Typ bloku
F funkce
M metoda
C třída

Dále je vypsáno umístění bloku (v rámci zdrojového kódu) a jméno bloku, tj. jméno funkce, metody popř. třídy. Za těmito informacemi se již vypisuje vypočtená kategorie cyklomatické složitosti, což je znak „A“ až „F“. Přitom platí, že bloky kategorie „A“ mají nejnižší složitost (funkce s jednou smyčkou či podmínkou), zatímco bloky kategorie „F“ jsou velmi složité a pravděpodobně budou obsahovat potenciálně nestabilní a těžko pochopitelný kód:

Skóre Kategorie Riziko
1 – 5 A nízké, malá komplexita, jednoduché bloky
6 – 10 B nízké, dobrá struktura programu
11 – 20 C průměrné, obsahuje nepatrně složitější bloky kódu
21 – 30 D vyšší, obsahuje složitější bloky kódu
31 – 40 E vyšší, složité bloky kódu nebo složité podmínky
41+ F vysoké, potenciálně nestabilní kód s rizikem zanesení nových chyb

Kromě kategorie je možné si nechat zobrazit i vypočtené skóre, tj. vlastní cyklomatickou složitost. To se provede následovně:

$ radon cc -s .

Skóre je zobrazeno v závorce za kategorií:

genpassword.py
    F 3:0 genpassword - F (41)
hello_world.py
    F 1:0 hello - A (1)
bubble_sort.py
    F 1:0 bubbleSort - A (4)
control_structures.py
    F 56:0 max2 - A (5)
    F 25:0 if_elif_else - A (3)
    F 34:0 nested_if_else - A (3)
    F 44:0 max1 - A (3)
    F 72:0 fibonacci2 - A (3)
    F 12:0 one_if - A (2)
    F 18:0 one_if_else - A (2)
    F 65:0 fibonacci1 - A (2)
    F 78:0 factorial1 - A (2)
    F 86:0 factorial2 - A (2)
    F 93:0 factorial3 - A (2)
    F 1:0 nothing - A (1)
    F 5:0 simple - A (1)
mandelbrot.py
    F 5:0 calc_mandelbrot - A (5)
v_poradi.py
    F 3:0 vporadi - B (10)
mandelbrot_complex.py
    F 20:0 calc_mandelbrot - A (5)

Také je možné si nechat spočítat průměrnou cyklomatickou složitost. Ta se vypíše na samotném konci:

$ radon cc -s -a .
 
genpassword.py
    F 3:0 genpassword - F (41)
hello_world.py
    F 1:0 hello - A (1)
bubble_sort.py
    F 1:0 bubbleSort - A (4)
control_structures.py
    F 56:0 max2 - A (5)
    F 25:0 if_elif_else - A (3)
    F 34:0 nested_if_else - A (3)
    F 44:0 max1 - A (3)
    F 72:0 fibonacci2 - A (3)
    F 12:0 one_if - A (2)
    F 18:0 one_if_else - A (2)
    F 65:0 fibonacci1 - A (2)
    F 78:0 factorial1 - A (2)
    F 86:0 factorial2 - A (2)
    F 93:0 factorial3 - A (2)
    F 1:0 nothing - A (1)
    F 5:0 simple - A (1)
mandelbrot.py
    F 5:0 calc_mandelbrot - A (5)
v_poradi.py
    F 3:0 vporadi - B (10)
mandelbrot_complex.py
    F 20:0 calc_mandelbrot - A (5)
 
19 blocks (classes, functions, methods) analyzed.
Average complexity: B (5.105263157894737)

Existují i další varianty, například filtrace jen těch horších hodnot, například kategorií od B výše:

$ ~/.local/bin/radon cc -s -a -n B .
genpassword.py
    F 3:0 genpassword - F (41)
v_poradi.py
    F 3:0 vporadi - B (10)
cls/dxf_importer.py
    M 185:4 DxfImporter.process_entity - C (17)
    M 103:4 DxfImporter.process_beginning_section - B (7)
    M 163:4 DxfImporter.process_section_entities - B (7)

5 blocks (classes, functions, methods) analyzed.
Average complexity: C (16.4)

8. Cyklomatická složitost základních programových konstrukcí

Otestujme si nyní, jak přesně probíhá výpočet cyklomatické složitosti v nástroji Radon. Začneme příkladem s jedinou funkcí, v níž se pouze volá jiná funkce print, což nijak nezvyšuje cyklomatickou složitost. Výsledkem by tedy měla být výchozí hodnota – 1:

def hello():
    print("Hello world!")
 
 
if __name__ == "__main__":
    hello()

Výpočet a výpis kategorie složitosti:

$ radon cc hello_world.py
 
hello_world.py
    F 1:0 hello - A

Výpočet a výpis kategorie složitosti i vypočteného skóre:

$ radon cc -s hello_world.py
 
hello_world.py
    F 1:0 hello - A (1)

Dtto ovšem navíc se zobrazí i průměrná složitost:

$ radon cc -s -a hello_world.py 
 
hello_world.py
    F 1:0 hello - A (1)
 
1 blocks (classes, functions, methods) analyzed.
Average complexity: A (1.0)

Další příklad již obsahuje větší počet funkcí, od těch zcela jednoduchých (se složitostí rovnou jedné) až po funkce se smyčkami popř. s vnořenými podmínkami:

def nothing():
    pass
 
 
def simple():
    print(1)
    print(2)
    print(3)
    print(4)
 
 
def one_if(x):
    if x > 0:
        return 42
    return None
 
 
def one_if_else(x):
    if x > 0:
        return 42
    else:
        return None
 
 
def if_elif_else(x):
    if x > 0:
        return "kladny"
    elif x < 0:
        return "zaporny"
    else:
        return "nula"
 
 
def nested_if_else(x):
    if x > 0:
        return "kladny"
    else:
        if x < 0:
            return "zaporny"
        else:
            return "nula"
 
 
def max1(a, b, c):
    maximum = a
 
    if b > maximum:
        maximum = b
 
    if c > maximum:
        maximum = c
 
    return maximum
 
 
def max2(a, b, c):
    if a > b and a > c:
        return a
    elif b > a and b > c:
        return b
    else:
        return c
 
 
def fibonacci1(i):
    x, y = 1, 1
    for i in range(i - 1):
        x, y = y, x + y
    return x
 
 
def fibonacci2(i):
    if i == 1 or i == 2:
        return 1
    return fibonacci2(i-1) + fibonacci2(i-2)
 
 
def factorial1(n):
    result = 1
    while n >= 1:
        result *= n
        n -= 1
    return result
 
 
def factorial2(n):
    if n == 0:
        return 1
    else:
        return n * factorial2(n-1)
 
 
def factorial3(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

Výsledek:

$ radon cc -s -a control_structures.py 
 
control_structures.py
    F 1:0 nothing - A (1)
    F 5:0 simple - A (1)
    F 12:0 one_if - A (2)
    F 18:0 one_if_else - A (2)
    F 25:0 if_elif_else - A (3)
    F 34:0 nested_if_else - A (3)
    F 44:0 max1 - A (3)
    F 56:0 max2 - A (5)
    F 65:0 fibonacci1 - A (2)
    F 72:0 fibonacci2 - A (3)
    F 78:0 factorial1 - A (2)
    F 86:0 factorial2 - A (2)
    F 93:0 factorial3 - A (2)
 
13 blocks (classes, functions, methods) analyzed.
Average complexity: A (2.3846153846153846)

9. Jak probíhal výpočet?

Podívejme se, jakým způsobem vlastně probíhal výpočet cyklomatické složitosti:

Funkce výchozí skóre podmínka smyčka bool Celkem Poznámka
nothing 1 0 0 0 1 jen pass
simple 1 0 0 0 1 jednoduché příkazy
one_if 1 1 0 0 2 jediná podmínka
one_if_else 1 1 0 0 2 else se nezapočítává
if_elif_else 1 2 0 0 3 elif se započítává jako if
nested_if_else 1 2 0 0 3 else se nezapočítává
max1 1 2 0 0 3 if nejsou vnořeny
max2 1 2 0 2 5 booleovské výrazy zvýšily složitost
fibonacci1 1 0 1 0 2 smyčka
fibonacci2 1 1 0 1 3 podmínka + booleovský výraz
factorial1 1 0 1 0 2 jen smyčka
factorial2 1 1 0 0 2 jen podmínka
factorial3 1 0 1 0 2 jen smyčka
Poznámka: jeden z důvodů, proč je metrika cyklomatické složitosti někdy kritizována, spočívá v tom, že se nikde nezohledňují rekurzivní volání funkcí, i když se zajisté jedná o komplikaci programu. To je ostatně vidět i při pohledu na předchozí tabulku, v níž jsou dvě rekurzivní funkce s velmi malou vypočtenou složitostí.

10. Získání informací o cyklomatické složitosti u funkcí s vnořenými smyčkami a podmínkami

Již bez podrobnějších výpočtů se podívejme na poněkud složitější funkce, které obsahují vnořené programové smyčky a podmínky. Začneme školním příkladem – bublinkovým řazením. To lze v Pythonu implementovat například takto (setříděné prvky „probublávají na konec seznamu, proto můžeme postupně snižovat mezní hodnotu počitadla ve vnitřní smyčce)“:

def bubbleSort(sequence):
    for i in range(len(sequence)-1, 0, -1):
        for j in range(i):
            if sequence[j] > sequence[j+1]:
                sequence[j], sequence[j+1] = sequence[j+1], sequence[j]
 
 
numbers = [1, 2, 3, 4, 5]
bubbleSort(numbers)
print(numbers)
 
numbers = [5, 4, 3, 2, 1]
bubbleSort(numbers)
print(numbers)

Vypočtená cyklomatická složitost je stále relativně nízká – skóre dosahuje hodnoty 4, protože se ve funkci nachází dvě smyčky a jedna podmínka:

$ radon cc -s -a bubble_sort.py 
 
bubble_sort.py
    F 1:0 bubbleSort - A (4)
 
1 blocks (classes, functions, methods) analyzed.
Average complexity: A (4.0)

Složitější je již funkce pro výpočet Mandelbrotovy množiny, a to bez použití komplexních čísel:

import palette_mandmap
from sys import argv, exit
 
 
def calc_mandelbrot(width, height, maxiter, palette):
    print("P3")
    print("{w} {h}".format(w=width, h=height))
    print("255")
 
    cy = -1.5
    for y in range(0, height):
        cx = -2.0
        for x in range(0, width):
            zx = 0.0
            zy = 0.0
            i = 0
            while i < maxiter:
                zx2 = zx * zx
                zy2 = zy * zy
                if zx2 + zy2 > 4.0:
                    break
                zy = 2.0 * zx * zy + cy
                zx = zx2 - zy2 + cx
                i += 1
 
            r = palette[i][0]
            g = palette[i][1]
            b = palette[i][2]
            print("{r} {g} {b}".format(r=r, g=g, b=b))
            cx += 3.0/width
        cy += 3.0/height
 
 
if __name__ == "__main__":
    if len(argv) < 4:
        print("usage: python mandelbrot width height maxiter")
        exit(1)
 
    width = int(argv[1])
    height = int(argv[2])
    maxiter = int(argv[3])
    calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)

Alternativně je možné funkci přepsat takovým způsobem, aby komplexní čísla používala:

import palette_mandmap
from sys import argv, exit
 
 
def calc_mandelbrot(width, height, maxiter, palette):
    print("P3")
    print("{w} {h}".format(w=width, h=height))
    print("255")
 
    c = 0.0 - 1.5J
    for y in range(0, height):
        c = complex(-2.0, c.imag)
        for x in range(0, width):
            z = 0.0 + 0.0J
            i = 0
            while i < maxiter:
                if abs(z) > 4.0:
                    break
                z = z**2 + c
                i += 1
 
            r = palette[i][0]
            g = palette[i][1]
            b = palette[i][2]
            print("{r} {g} {b}".format(r=r, g=g, b=b))
            c += 3.0/width
        c += 3.0J/height
 
 
if __name__ == "__main__":
    if len(argv) < 4:
        print("usage: python mandelbrot width height maxiter")
        exit(1)
 
    width = int(argv[1])
    height = int(argv[2])
    maxiter = int(argv[3])
    calc_mandelbrot(width, height, maxiter, palette_mandmap.palette)

Nezávisle na tom, že druhá funkce je nepatrně kratší než funkce první, mají oba výpočty Mandelbrotovy množiny naprosto stejnou cyklomatickou složitost (opět nízkou, jen 5), protože obsahují tři vnořené smyčky a jedinou podmínku:

$ radon cc -s -a mand*
 
mandelbrot_complex.py
    F 20:0 calc_mandelbrot - A (5)
mandelbrot.py
    F 5:0 calc_mandelbrot - A (5)
 
2 blocks (classes, functions, methods) analyzed.
Average complexity: A (5.0)
Tyto funkce ještě není nutné refaktorovat, protože jsou ještě poměrně krátké dělají jednu činnost a jejich cyklomatická složitost stále spadá do kategorie A. Ovšem již se nacházíme na hranici s kategorií B.

11. Cyklomatická složitost nechvalně proslulých zdrojových kódů

Na stránce http://hovnokod.cz/2744 můžeme najít relativně krátkou, ale již docela „zašmodrchanou“ funkci:

def vporadi(x,y,z):
    if (x<y) and (x<z):
        if (y<z):
            print(x,y,z)
        else:
            print(x,z,y)
    if (y<x) and (y<z):
        if (x<z):
            print(y,x,z)
        else:
            print(y,z,x)
    if (z<y) and (z<x):
        if (y<x):
            print(z,y,x)
        else:
            print(z,x,y)

Pomiňme fakt, že andnižší prioritu než relační operátory, takže jsou závorky zbytečné a zaměřme se na výpočet cyklomatické složitosti:

$ radon cc -s -a v_poradi.py 
 
v_poradi.py
    F 3:0 vporadi - B (10)
 
1 blocks (classes, functions, methods) analyzed.
Average complexity: B (10.0)

I takto krátká funkce má cyklomatickou složitost 10, což je na hranici mezi kategorií B a C.

12. A na závěr … cyklomatická složitost nejhoršího kódu a nejnečitelnějšího kódu

I další inspiraci jsem získal na slavném serveru, tentokrát na adrese http://hovnokod.cz/1429. Po nepatrné úpravě (příkaz print → funkce print) vypadá kód neuvěřitelně:

def genpassword(wlc,maxchar,txt,List,verbose):
    word = ""
    i1 = i2 = i3 = i4 = i5 = i6 = i6 = i7 = i8 = i9 = i10 = i11 = i12 = i13 = i14 = i15 = 0
    txtfile = open(txt,'w')
 
    i = 0
    mc = int(maxchar) - 1
    lword = [0]
    for i in range(mc):
        lword += [0]
 
    for i1 in range(len(wlc)):
        for i2 in range(len(wlc)):
            for i3 in range(len(wlc)):
                for i4 in range(len(wlc)):
                    for i5 in range(len(wlc)):
                        for i6 in range(len(wlc)):
                            for i7 in range(len(wlc)):
                                for i8 in range(len(wlc)):
                                    for i9 in range(len(wlc)):
                                        for i10 in range(len(wlc)):
                                            for i11 in range(len(wlc)):
                                                for i12 in range(len(wlc)):
                                                    for i13 in range(len(wlc)):
                                                        for i14 in range(len(wlc)):
                                                            for i15 in range(len(wlc)):
                                                                if int(maxchar) == 1 :
                                                                    word = wlc[i15]
                                                                if int(maxchar) == 2 :
                                                                    word = wlc[i14] + wlc[i15]
                                                                if int(maxchar) == 3 :
                                                                    word = wlc[i13] + wlc[i14] + wlc[i15]
 
                                                                if int(maxchar) == 14 :
                                                                    word = wlc[i1] + wlc[i2] + wlc[i3] + wlc[i4] \
                                                                    + wlc[i5] + wlc[i6] + wlc[i7] + wlc[i8] + wlc[i9] \
                                                                    + wlc[i10] + wlc[i11] + wlc[i12] + wlc[i13] \
                                                                    + wlc[i14] + wlc[i15]
 
                                                                if int(maxchar) == 15 :
                                                                    word = wlc[i1] + wlc[i2] + wlc[i3] + wlc[i4] \
                                                                    + wlc[i5] + wlc[i6] + wlc[i7] + wlc[i8] + wlc[i9] \
                                                                    + wlc[i10] + wlc[i11] + wlc[i12] + wlc[i13] \
                                                                    + wlc[i14] + wlc[i15]
 
                                                                if int(verbose) == 1:
                                                                    print(word)
 
                                                                txtfile.writelines(word + "\n")
 
                                                                i = 0
                                                                end = 0
                                                                if int(List) == 1 :
                                                                    for i in range(len(word)):
                                                                        lword[i] = "9"
                                                                    if str(lword) == str(list(word)):
                                                                        end = 1
 
                                                                if end == 1 : break
                                                            if end == 1 : break
                                                        if end == 1 : break
                                                    if end == 1 : break
                                                if end == 1 : break
                                            if end == 1 : break
                                        if end == 1 : break
                                    if end == 1 : break
                                if end == 1 : break
                            if end == 1 : break
                        if end == 1 : break
                    if end == 1 : break
                if end == 1 : break
            if end == 1 : break
        if end == 1 : break
 
    txtfile.close()

Cyklomatická složitost je zatím nejvyšší – skóre dosahuje hodnoty 41(!):

$ radon cc -s -a genpassword.py 
 
genpassword.py
    F 3:0 genpassword - F (41)
 
1 blocks (classes, functions, methods) analyzed.
Average complexity: F (41.0)
Není jasné, jestli se jedná o umělý příklad, školní úlohu nebo … produkční kód.

13. Cyklomatická složitost != měřítko čitelného kódu

Poslední příklad byl získán na adrese http://preshing.com/20110926/high-resolution-mandelbrot-in-obfuscated-python/ a opět byl nepatrně upraven tak, aby obsahoval funkci. Příklad je schválně obfuskovaný (i přesto, že je psán v Pythonu):

def x():
    _                                      =   (
                                            255,
                                          lambda
                                   V       ,B,c
                                 :c   and Y(V*V+B,B,  c
                                   -1)if(abs(V)<6)else
                   (              2+c-4*abs(V)**-0.4)/i
                     )  ;v,      x=1500,1000;C=range(v*x
                      );import  struct;P=struct.pack;M,\
                j  ='<QIIHHHH',open('M.bmp','wb').write
    for X in j('BM'+P(M,v*x*3+26,26,12,v,x,1,24))or C:
                i  ,Y=_;j(P('BBB',*(lambda T:(T*80+T**9
                      *i-950*T  **99,T*70-880*T**18+701*
                     T  **9     ,T*i**(1-T**45*2)))(sum(
                   [              Y(0,(A%3/3.+X%v+(X/v+
                                   A/3/3.-x/2)/1j)*2.5
                                 /x   -2.7,i)**2 for  \
                                   A       in C
                                          [:9]])
                                            /9)
                                           )   )
 
x()

Zajímavé je, že tento nečitelný kód má cyklomatickou složitost jen 6! To mj. znamená, že cyklomatická složitost nemusí mít nic společného s čitelností zdrojových kódů:

$ radon cc -s -a mandelbrot_lambda.py 
 
mandelbrot_lambda.py
    F 1:0 x - B (6)
 
1 blocks (classes, functions, methods) analyzed.
Average complexity: B (6.0)
Poznámka: tento kód je skutečně funkční a když několik minut počkáte, získáte BMP obrázek s Mandelbrotovou množinou ve vyšším rozlišení.

14. Úprava nástroje Radon pro vytvoření HTML stránky s výsledky

V této kapitole si ukážeme, jak je možné upravit nástroj Radon takovým způsobem, aby generoval HTML stránky s obarvenými výsledky. Příkladem může být tato stránka: https://fabric8-analytics.github.io/dashboard/f8a-server-backbone.cc.html.

Pokud je Radon spuštěn tak, aby vypočtenou cyklomatickou složitost zobrazil na standardním výstupu, je takový výstup obarven:

Pokud je však provedeno jakékoli přesměrování, barvy se ztratí. Proto upravíme soubor colors.py umístěný v adresáři:

~/.local/lib/python3.6/site-packages

Postačuje zakomentovat volání funkce colorama.init(), popř. upravit konstantu BRIGHT, protože světlá písmena jsou na bílém pozadí špatně čitelná:

'''Module holding constants used to format lines that are printed to the
terminal.
'''
 
import sys
try:
    import colorama
    # colorama.init(strip=(not sys.stdout.isatty()))
    # colorama.init()
    GREEN, YELLOW, RED = (colorama.Fore.GREEN, colorama.Fore.YELLOW,
                          colorama.Fore.RED)
    MAGENTA, CYAN, WHITE = (colorama.Fore.MAGENTA, colorama.Fore.CYAN,
                            colorama.Fore.WHITE)
    BRIGHT, RESET = colorama.Style.BRIGHT, colorama.Style.RESET_ALL
except ImportError:  # pragma: no cover
    # No colorama, so let's fallback to no-color mode
    GREEN = YELLOW = RED = MAGENTA = CYAN = WHITE = BRIGHT = RESET = ''
 
RANKS_COLORS = {'A': GREEN, 'B': GREEN,
                'C': YELLOW, 'D': YELLOW,
                'E': RED, 'F': RED}
 
LETTERS_COLORS = {'F': MAGENTA,
                  'C': CYAN,
                  'M': GREEN}
 
MI_RANKS = {'A': GREEN, 'B': YELLOW, 'C': RED}
TEMPLATE = '{0}{1} {reset}{2}:{3} {4} - {5}{6}{reset}'

Výstup je následně možné přesměrovat do skriptu ansi2html a získaný výstup podruhé přesměrovat do souboru s koncovkou .html:

$ radon cc -s -a . | ansi2html > cc.html

15. Zjištění indexu udržovatelnosti (Maintainability Index)

Pokud nástroj Radon spustíte s příkazem mi, bude se zjišťovat takzvaný index udržovatelnosti (Maintainability Index). Nejprve se podívejme na příklad:

$ radon mi -s .
 
hello_world.py - A (81.86)
control_structures.py - A (38.27)
bubble_sort.py - A (64.62)
 
mandelbrot.py - A (48.27)
mandelbrot_complex.py - A (78.90)
 
v_poradi.py - A (72.20)
genpassword.py - A (39.68)

Vidíme, že se opět vypisuje kategorie (jeden znak) a skóre. Tentokrát však mají obě metriky jiný význam a snaží se popsat míru udržovatelnosti zdrojových kódů. Navíc se skóre a kategorie počítá vždy pro celý zdrojový kód, resp. přesněji řečeno pro jeden modul:

Skóre Kategorie Udržovatelnost
100 – 20 A vysoká
19 – 10 B střední
9 – 0 C nízká

Cílem je tedy získat co nejvyšší skóre, na rozdíl od cyklomatické složitosti.

16. Výpočet indexu udržovatelnosti

Existuje větší množství způsobů výpočtu indexu udržovatelnosti. Nástroj Radon používá tento heuristický a magickými konstantami oplývající vzorec:

root_podpora

kde:

Člen Význam
V Halsteadova míra vypočtená dalším vzorcem V=Nlog2η na základě počtu operátorů a operandů
G cyklomatická složitost
L počet řádků se zdrojovým kódem
C % komentářů ve zdrojovém kódu převedené na radiány (?)
N celkový počet operátorů a operandů (pro výpočet V)
η počet rozdílných operátorů a operandů (pro výpočet V)

17. Repositář s demonstračními příklady

Zdrojové kódy osmi dnes popsaných demonstračních příkladů určených pro interpret Pythonu 3 byly uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/radon-examples. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem prozatím velmi malý, doslova několik kilobajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:

Poznámka: obfuskovaný kód generující Mandelbrotovu množinu naleznete na adrese http://preshing.com/20110926/high-resolution-mandelbrot-in-obfuscated-python/, takže ho nebylo nutné přidávat do repositáře.

18. Odkazy na Internetu

  1. Radon na PyPi
    https://pypi.org/project/radon/
  2. Radon na GitHubu
    https://github.com/rubik/radon
  3. Radon’s documentation!
    http://radon.readthedocs.i­o/en/latest/
  4. Cyclomatic complexity (Wikipedia)
    https://en.wikipedia.org/wi­ki/Cyclomatic_complexity
  5. Cyklomatická složitost (Wikipedia)
    https://cs.wikipedia.org/wi­ki/Cyklomatick%C3%A1_slo%C5%BE­itost
  6. What is Cyclomatic Complexity?
    https://www.tutorialspoin­t.com/software_testing_dic­tionary/cyclomatic_comple­xity.htm
  7. The Halstead metrics
    http://www.virtualmachine­ry.com/sidebar2.htm
  8. Measurement of Halstead Metrics with Testwell CMT++ and CMTJava (Complexity Measures Tool)
    http://www.verifysoft.com/en_hal­stead_metrics.html
  9. Halstead complexity measures (Wikipedia)
    https://en.wikipedia.org/wi­ki/Halstead_complexity_me­asures
  10. Decision-to-decision path
    https://en.wikipedia.org/wiki/Decision-to-decision_path
  11. Essential complexity
    https://en.wikipedia.org/wi­ki/Essential_complexity
  12. Learn Mccabe's Cyclomatic Complexity with Example
    https://www.guru99.com/cyclomatic-complexity.html
  13. ansi2html
    https://github.com/Kronuz/ansi2html
  14. Komplexita
    https://cs.wikipedia.org/wi­ki/Komplexita
  15. Think Twice Before Using the “Maintainability Index”
    https://avandeursen.com/2014/08/29/think-twice-before-using-the-maintainability-index/
  16. Structured testing (kniha)
    https://books.google.cz/bo­oks?id=vtNWAAAAMAAJ
  17. Static program analysis
    https://en.wikipedia.org/wi­ki/Static_program_analysis
  18. Python #2744
    http://hovnokod.cz/2744
  19. Python #1429
    http://hovnokod.cz/1429
  20. Measure complexity of C source
    https://www.gnu.org/softwa­re/complexity/manual/comple­xity.html
  21. Using Complexity Measurements to Improve Software Quality
    https://www.infoq.com/new­s/2014/10/complexity-software-quality
  22. High-Resolution Mandelbrot in Obfuscated Python
    http://preshing.com/20110926/high-resolution-mandelbrot-in-obfuscated-python/
  23. Penrose Tiling in Obfuscated Python
    http://preshing.com/20110822/penrose-tiling-in-obfuscated-python/
  24. Abstract syntax tree
    https://en.wikipedia.org/wi­ki/Abstract_syntax_tree
  25. Parsing
    https://en.wikipedia.org/wiki/Parsing
  26. Lexical analysis
    https://en.wikipedia.org/wi­ki/Lexical_analysis

Byl pro vás článek přínosný?

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.