Jste zde

Ještě jednou a lépe: zjištění neprázdnosti souboru v Pythonu

Podíváme se na to, jak řešení zdánlivě jednoduchého úkolu může vést na chybu v programu.

Původní kód

K sepsání tohoto příspěvku mě inspirovala tato nekorektní odpověď na stackoverflow.com. Mějme za úkol napsat funkci v Pythonu, která zjistí, zda je předaný soubor neprázdný. Rychlým vyhledáním dojdeme k tomu, že v Pythonu se ke zjištění velikosti souboru dá použít os.path.getsize(). První pokus tedy může vypadat takto:

# Returns True if the given file is non-empty, False otherwise.
def is_non_empty_file(path):
    return os.path.getsize(path) > 0

Pokud však daný soubor neexistuje, tak os.path.getsize() vyhodí výjimku FileNotFoundError. Autor oné odpovědi však chtěl, aby jeho funkce nevyhodila výjimku. Vyřešil to následovně:

# Returns True if the given file exists and is non-empty, False otherwise.
def is_non_empty_file(path):
    return os.path.isfile(path) and os.path.getsize(path) > 0

Nejdříve tedy zkontroluje, zda soubor existuje a teprve potom si zjistí jeho velikost.

Proč takto ne?

Představte si následující situaci:

  1. Uživatel zavolá is_non_empty_file(path) na existujícím souboru.
  2. Zavolá se os.path.isfile(path), která vrátí True.
  3. Někdo shodou okolností nyní onen soubor z disku smaže (např. jiný program).
  4. Zavolá se os.path.getsize(path). Jelikož však daný soubor již neexistuje, dojde k vyhození výjimky FileNotFoundError. V popisu funkce is_non_empty_file() však nic o vyhození výjimky není, na což se volající kód může spoléhat. A hle, chyba je na světě.

Dalším problémem je, že po přečtení dokumentace k os.path.getsize() vidíme, že tato funkce obecně může vyhodit OSError, nikoliv jen FileNotFoundError (podtřída OSError). Např. v případě, kdy daný soubor není přístupný.

Jak to udělat lépe?

V podobných případech, kdy hrozí riziku souběhu (angl. race condition), je potřeba použít funkci, která neprázdnost souboru zjistí atomicky. Stačí nám zavolat os.path.getsize() a odchytit OSError:

# Returns True if the given file exists, is accessible, and non-empty.
# Otherwise, it returns False.
def is_non_empty_file(path):
    try:
        return os.path.getsize(path) > 0
    except OSError:
        return False

Samozřejmě, pokud bychom na OSError chtěli reagovat jiným způsobem, tak bychom si museli os.path.getsize() zavolat sami a výjimku odchytit. Cílem funkce is_non_empty_file() bylo od této chyby uživatele odstínit, i přesto, že prakticky dojde k zatajení problémů při čtení souboru.

Přidat komentář