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

Od Petr Zemek, 2017-05-14

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.

Obsah tohoto pole je soukromý a nebude veřejně zobrazen.

Filtrované HTML (využíváno)

  • Povolené HTML značky: <a href hreflang> <em> <strong> <cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <table>
  • Zvýraznění syntaxe kódu lze povolit přes následující značky: <code>, <blockcode>, <bash>, <c>, <cpp>, <haskell>, <html>, <java>, <javascript>, <latex>, <perl>, <php>, <python>, <ruby>, <rust>, <sql>, <text>, <vim>, <xml>, <yaml>.
  • Řádky a odstavce se zalomí automaticky.
  • Webové a e-mailové adresy jsou automaticky převedeny na odkazy.
CAPTCHA
7 + 4 =
Vyřešte tento jednoduchý matematický příklad a vložte výsledek. Např. pro 1+3 vložte 4.
Nějak se mi tady rozmohl spam, takže poprosím o ověření.

Don Joe (neověřeno)

7 years 1 month zpět


is_non_empty_file

>>> is_non_empty_file('/tmp')
True

Petr Zemek

7 years 1 month zpět

In reply to by Don Joe (neověřeno)

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