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ě.

Jak to udělat lépe?

V podobných případech, kdy hrozí riziko souběhu (angl. race condition), je potřeba použít funkci, která neprázdnost souboru zjistí atomicky. K tomu lze využít např. os.stat():

import os
import stat
 
# Returns True if the given regular file exists, is accessible, and non-empty.
# Otherwise, it returns False.
def is_non_empty_file(path):
    try:
        s = os.stat(path)
        return stat.S_ISREG(s.st_mode) and s.st_size > 0
    except OSError:
        return False

Po zavolání os.stat() se na základě výsledku provede kontrola, zda se jedná o regulérní soubor (a tedy nikoliv např. o adresář nebo speciální zařízení) a zda je soubor neprázdný. Výjimku OSError je potřeba odchytávat z toho důvodu, že daný soubor nemusí existovat či nemusí být přístupný. V prvním případě by došlo k vyhození FileNotFoundError, ve druhém pak k vyhození PermissionError. Obě tyto výjimky dědí z OSError, takže nám stačí odchytávat ten. Související možná výhoda je, že OSError nám pokrývá i chyby typu "nepodařilo se načíst data z disku". Pokud bychom na chyby při čtení z disku chtěli reagovat jinak, tak by bylo potřeba funkci upravit.

Komentáře


is_non_empty_file

>>> is_non_empty_file('/tmp')
True

Ahoj, díky moc za postřeh, toto jsem si neuvědomil :). Funkce os.path.getsize() se nedívá na to, o jaký typ cesty se jedná (zda je to adresář, regulérní soubor, či speciální zařízení). K detekci pouze regulérních souborů je tedy potřeba použít jiný způsob. Příspěvek jsem upravil.

Pro úplnost, takto vypadala funkce původně (před úpravou):

# 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

Přidat komentář