No Python, usando argparse, permita apenas números inteiros positivos

164

O título resume muito bem o que eu gostaria que acontecesse.

Aqui está o que eu tenho e, embora o programa não exploda um número inteiro não positivo, quero que o usuário seja informado de que um número inteiro não positivo é basicamente um disparate.

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()

E a saída:

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

Saída com negativo:

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

Agora, obviamente, eu poderia simplesmente adicionar um if para determinar que if args.gamesé negativo, mas fiquei curioso para saber se havia uma maneira de capturá-lo no argparsenível, para tirar proveito da impressão de uso automático.

Idealmente, imprimiria algo semelhante a este:

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'

Igual a:

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'

Por enquanto estou fazendo isso e acho que estou feliz:

if args.games <= 0:
    parser.print_help()
    print "-g/--games: must be positive."
    sys.exit(1)
jgritty
fonte

Respostas:

244

Isso deve ser possível utilizando type. Você ainda precisará definir um método real que decida isso para você:

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)

Este é basicamente apenas um exemplo adaptado da perfect_squarefunção nos documentos em argparse.

Yuushi
fonte
1
Sua função pode ter vários valores? Como isso funciona?
Tom
2
Se a conversão intfalhar, ainda haverá uma saída legível? Ou você deve fazer try raisea conversão manualmente para isso?
NOHS
4
@ MrZ Vai dar algo parecido error: argument foo: invalid check_positive value: 'foo=<whatever>'. Você pode simplesmente adicionar um try:... em except ValueError:volta dele que gera uma exceção com uma mensagem de erro melhor.
Yuushi 13/09
59

type seria a opção recomendada para lidar com condições / verificações, como na resposta de Yuushi.

No seu caso específico, você também pode usar o choicesparâmetro se seu limite superior também for conhecido:

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

Nota: use em rangevez de xrangepara python 3.x

aneróide
fonte
3
Eu imagino que isso seria bastante ineficiente, pois você geraria um intervalo e depois passaria por ele para validar sua entrada. Um rápido ifé muito mais rápido.
TravisThomas
2
@ trav1th De fato pode ser, mas é um exemplo de uso dos documentos. Além disso, eu disse na minha resposta que a resposta de Yuushi é a melhor. É bom dar opções. E no caso do argparse, está acontecendo uma vez por execução, usa um gerador ( xrange) e não requer código adicional. Essa troca está disponível. Cabe a cada um decidir qual caminho seguir.
aneroid
16
Para ser mais claro sobre o argumento de jgritty na resposta do autor, escolhas = xrange (0,1000) resultará na lista inteira de números inteiros de 1 a 999, sendo gravados no console sempre que você usar --help ou se um argumento inválido for forneceu. Não é uma boa escolha na maioria das circunstâncias.
Biomiker # 29/16
9

A maneira rápida e suja, se você tem um máximo previsível e um mínimo para o seu argumento, é usado choicescom um intervalo

parser.add_argument('foo', type=int, choices=xrange(0, 1000))
autor do bem
fonte
24
A desvantagem é a saída hedionda.
jgritty
6
ênfase em sujo , eu acho.
ben autor
4
Para ser mais claro sobre o ponto de jgritty, escolhas = xrange (0,1000) resultará na gravação de toda a lista de números inteiros de 1 a 999, inclusive no seu console, toda vez que você usar --help ou se for fornecido um argumento inválido. Não é uma boa escolha na maioria das circunstâncias.
Biomiker # 29/16
8

Uma alternativa mais simples, especialmente se a subclasse argparse.ArgumentParser, é iniciar a validação de dentro do parse_argsmétodo.

Dentro dessa subclasse:

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

Essa técnica pode não ser tão legal quanto uma chamada personalizada, mas faz o trabalho.


Sobre ArgumentParser.error(message):

Este método imprime uma mensagem de uso incluindo a mensagem para o erro padrão e finaliza o programa com um código de status 2.


Crédito: resposta por jonatan

Acumenus
fonte
Ou, no mínimo, substituindo print "-g/--games: must be positive."; sys.exit(1)por apenas parser.error("-g/--games: must be positive."). (Uso como na resposta de Jonatan .)
aneroid
3

Caso alguém (como eu) se depare com essa pergunta em uma pesquisa no Google, aqui está um exemplo de como usar uma abordagem modular para resolver com perfeição o problema mais geral de permitir números inteiros argparse apenas em um intervalo especificado :

# 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")

Isso permite que você faça algo como:

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

A variável fooagora permite apenas números inteiros positivos , como o OP solicitado.

Observe que, além dos formulários acima, apenas um máximo também é possível com IntRange:

parser.add_argument('other', type=IntRange(imax=10))  # Must have other <= 10
pallgeuer
fonte