W Pythonie, używając argparse, zezwalaj tylko na dodatnie liczby całkowite

164

Tytuł właściwie podsumowuje to, co chciałbym, żeby się wydarzyło.

Oto, co mam i chociaż program nie wysadza na niepozytywną liczbę całkowitą, chcę, aby użytkownik był informowany, że niepozytywna liczba całkowita jest w zasadzie nonsensem.

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-g", "--games", type=int, default=162,
                    help="The number of games to simulate")
args = parser.parse_args()

A wynik:

python simulate_many.py -g 20
Setting up...
Playing games...
....................

Wyjście z ujemnym:

python simulate_many.py -g -2
Setting up...
Playing games...

Oczywiście mógłbym po prostu dodać jeśli, aby określić, że if args.gamesjest ujemne, ale byłem ciekawy, czy istnieje sposób, aby uwięzić to wargparse poziomie, aby skorzystać z automatycznego drukowania użycia.

Idealnie byłoby wydrukować coś podobnego do tego:

python simulate_many.py -g a
usage: simulate_many.py [-h] [-g GAMES] [-d] [-l LEAGUE]
simulate_many.py: error: argument -g/--games: invalid int value: 'a'

Tak jak to:

python simulate_many.py -g -2
usage: simulate_many.py [-h] [-g GAMES] [-d] [-l LEAGUE]
simulate_many.py: error: argument -g/--games: invalid positive int value: '-2'

Na razie to robię i chyba jestem szczęśliwy:

if args.games <= 0:
    parser.print_help()
    print "-g/--games: must be positive."
    sys.exit(1)
jgritty
źródło

Odpowiedzi:

244

Powinno to być możliwe przy użyciu type. Nadal będziesz musiał zdefiniować faktyczną metodę, która zadecyduje o tym za Ciebie:

def check_positive(value):
    ivalue = int(value)
    if ivalue <= 0:
        raise argparse.ArgumentTypeError("%s is an invalid positive int value" % value)
    return ivalue

parser = argparse.ArgumentParser(...)
parser.add_argument('foo', type=check_positive)

Jest to w zasadzie tylko dostosowany przykład z perfect_squarefunkcji w docs na argparse.

Yuushi
źródło
1
Czy Twoja funkcja może mieć wiele wartości? Jak to działa?
Tom
2
Jeśli konwersja na intnie powiedzie się, czy nadal będzie możliwe do odczytania dane wyjściowe? A może powinieneś try raiseręcznie wykonać konwersję?
NOhs
4
@MrZ To da coś takiego error: argument foo: invalid check_positive value: 'foo=<whatever>'. Mógłbyś po prostu dodać try:... except ValueError:wokół niego, który ponownie wywołuje wyjątek z lepszym komunikatem o błędzie.
Yuushi
59

type byłaby zalecaną opcją do obsługi warunków / kontroli, jak w odpowiedzi Yuushi.

W twoim konkretnym przypadku możesz również użyć choicesparametru, jeśli znany jest również twój górny limit:

parser.add_argument('foo', type=int, choices=xrange(5, 10))

Uwaga: użyj rangezamiast xrangedla python 3.x

aneroid
źródło
3
Wyobrażam sobie, że byłoby to dość nieefektywne, ponieważ generowałbyś zakres, a następnie przechodząc przez niego weryfikowałbyś swoje dane wejściowe. Szybki ifjest znacznie szybszy.
TravisThomas
2
@ trav1th Rzeczywiście może być, ale jest to przykład użycia z dokumentacji. Powiedziałem również w mojej odpowiedzi, że odpowiedź Yuushiego jest tą, do której należy się udać. Dobrze jest dać opcje. A w przypadku argparse dzieje się to raz na wykonanie, używa generatora ( xrange) i nie wymaga dodatkowego kodu. Ten kompromis jest dostępny. Każdy decyduje, którą drogą iść.
aneroid
16
Aby wyjaśnić punkt jgritty w odpowiedzi autora Bena, choice = xrange (0,1000) spowoduje, że cała lista liczb całkowitych od 1 do 999 włącznie zostanie zapisana na konsoli za każdym razem, gdy użyjesz --help lub jeśli nieprawidłowy argument to opatrzony. W większości przypadków nie jest to dobry wybór.
biomiker
9

Szybkim i brudnym sposobem, jeśli masz przewidywalne maksimum i minimum dla swojego argumentu, jest użycie choiceszakresu

parser.add_argument('foo', type=int, choices=xrange(0, 1000))
ben autor
źródło
24
Wadą jest ohydna wydajność.
jgritty
6
nacisk na brudny , jak sądzę.
autor ben
4
Aby lepiej zrozumieć punkt jgritty, choice = xrange (0,1000) spowoduje, że cała lista liczb całkowitych od 1 do 999 włącznie będzie zapisywana na konsoli za każdym razem, gdy użyjesz opcji --help lub jeśli zostanie podany nieprawidłowy argument. W większości przypadków nie jest to dobry wybór.
biomiker
8

Prostszą alternatywą, zwłaszcza w przypadku tworzenia podklas argparse.ArgumentParser, jest zainicjowanie walidacji z wnętrza parse_argsmetody.

Wewnątrz takiej podklasy:

def parse_args(self, args=None, namespace=None):
    """Parse and validate args."""
    namespace = super().parse_args(args, namespace)
    if namespace.games <= 0:
         raise self.error('The number of games must be a positive integer.')
    return namespace

Ta technika może nie być tak fajna jak niestandardowe wywołanie, ale spełnia swoje zadanie.


O ArgumentParser.error(message):

Ta metoda wyświetla komunikat o użyciu, w tym komunikat o błędzie standardowym, i kończy działanie programu z kodem stanu 2.


Kredyt: odpowiedź jonatana

Acumenus
źródło
Lub przynajmniej zastąpienie print "-g/--games: must be positive."; sys.exit(1)just parser.error("-g/--games: must be positive."). (Użycie jak w odpowiedzi jonatana .)
aneroid
3

Na wypadek, gdyby ktoś (taki jak ja) napotkał to pytanie w wyszukiwarce Google, oto przykład, jak zastosować podejście modułowe, aby zgrabnie rozwiązać bardziej ogólny problem polegający na dopuszczaniu liczb całkowitych argparse tylko w określonym zakresie :

# Custom argparse type representing a bounded int
class IntRange:

    def __init__(self, imin=None, imax=None):
        self.imin = imin
        self.imax = imax

    def __call__(self, arg):
        try:
            value = int(arg)
        except ValueError:
            raise self.exception()
        if (self.imin is not None and value < self.imin) or (self.imax is not None and value > self.imax):
            raise self.exception()
        return value

    def exception(self):
        if self.imin is not None and self.imax is not None:
            return argparse.ArgumentTypeError(f"Must be an integer in the range [{self.imin}, {self.imax}]")
        elif self.imin is not None:
            return argparse.ArgumentTypeError(f"Must be an integer >= {self.imin}")
        elif self.imax is not None:
            return argparse.ArgumentTypeError(f"Must be an integer <= {self.imax}")
        else:
            return argparse.ArgumentTypeError("Must be an integer")

Dzięki temu możesz zrobić coś takiego:

parser = argparse.ArgumentParser(...)
parser.add_argument('foo', type=IntRange(1))     # Must have foo >= 1
parser.add_argument('bar', type=IntRange(1, 7))  # Must have 1 <= bar <= 7

Zmienna foodopuszcza teraz tylko dodatnie liczby całkowite , tak jak zapytał OP.

Zwróć uwagę, że oprócz powyższych formularzy możliwe jest również maksimum z IntRange:

parser.add_argument('other', type=IntRange(imax=10))  # Must have other <= 10
pallgeuer
źródło