Analisando Valores Booleanos com Argparse

616

Eu gostaria de usar argparse para analisar argumentos booleanos da linha de comando escritos como "--foo True" ou "--foo True". Por exemplo:

my_program --my_boolean_flag False

No entanto, o código de teste a seguir não faz o que eu gostaria:

import argparse
parser = argparse.ArgumentParser(description="My parser")
parser.add_argument("--my_bool", type=bool)
cmd_line = ["--my_bool", "False"]
parsed_args = parser.parse(cmd_line)

Infelizmente, parsed_args.my_boolavalia como True. Este é o caso mesmo quando eu mudo cmd_linepara ser ["--my_bool", ""], o que é surpreendente, já que é bool("")avaliado em False.

Como posso obter argparse para analisar "False", "F"e sua minúscula variantes para ser False?

SuperElectric
fonte
40
Aqui está uma interpretação de uma linha da resposta de @ mgilsonparser.add_argument('--feature', dest='feature', default=False, action='store_true') . Essa solução garante que você sempre obtenha um booltipo com valor Trueou False. (Esta solução tem uma restrição: a sua opção deve ter um valor padrão.)
Trevor Boyd Smith
7
Aqui está uma interpretação de uma linha da resposta de @ Maximparser.add_argument('--feature', dest='feature', type=lambda x:bool(distutils.util.strtobool(x))) . Quando a opção é usada, esta solução garante um booltipo com valor de Trueou False. Quando a opção não for usada, você receberá None. ( distutils.util.strtobool(x)É de outra questão stackoverflow )
Trevor Boyd Smith
8
como sobre algo comoparser.add_argument('--my_bool', action='store_true', default=False)
AruniRC

Respostas:

276

Outra solução usando as sugestões anteriores, mas com o erro de análise "correto" de argparse:

def str2bool(v):
    if isinstance(v, bool):
       return v
    if v.lower() in ('yes', 'true', 't', 'y', '1'):
        return True
    elif v.lower() in ('no', 'false', 'f', 'n', '0'):
        return False
    else:
        raise argparse.ArgumentTypeError('Boolean value expected.')

Isso é muito útil para fazer trocas com valores padrão; por exemplo

parser.add_argument("--nice", type=str2bool, nargs='?',
                        const=True, default=False,
                        help="Activate nice mode.")

me permite usar:

script --nice
script --nice <bool>

e ainda use um valor padrão (específico para as configurações do usuário). Uma desvantagem (relacionada indiretamente) com essa abordagem é que os 'nargs' podem pegar um argumento posicional - veja esta questão relacionada e este relatório de erro da argparse .

Máxima
fonte
4
nargs = '?' significa zero ou um argumento. docs.python.org/3/library/argparse.html#nargs
Maxim
1
Eu amo isso, mas meu equivalente a default = NICE está me dando um erro, então preciso fazer outra coisa.
Michael Mathews
2
@MarcelloRomani str2bool não é um tipo no sentido de Python, é a função definida acima, você precisa incluí-lo em algum lugar.
Maxim
4
o código de str2bool(v)poderia ser substituído por bool(distutils.util.strtobool(v)). Fonte: stackoverflow.com/a/18472142/2436175
Antonio
4
Talvez valha a pena mencionar que, dessa maneira, você não pode verificar se o argumento está definido com if args.nice:beacuse, se o argumento estiver definido como False, nunca passará a condição. Se isso é certo, então talvez seja melhor a lista retornar de str2boolfunção e set list como constparâmetro, como este [True], [False]. Corrija-me se eu estiver errado
Nutcracker
889

Eu acho que uma maneira mais canônica de fazer isso é via:

command --feature

e

command --no-feature

argparse suporta bem esta versão:

parser.add_argument('--feature', dest='feature', action='store_true')
parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)

Obviamente, se você realmente deseja a --arg <True|False>versão, pode passar ast.literal_evalcomo o "tipo" ou uma função definida pelo usuário ...

def t_or_f(arg):
    ua = str(arg).upper()
    if 'TRUE'.startswith(ua):
       return True
    elif 'FALSE'.startswith(ua):
       return False
    else:
       pass  #error condition maybe?
mgilson
fonte
96
Eu ainda acho que type=booldeveria funcionar imediatamente (considere argumentos posicionais!). Mesmo quando você especifica adicionalmente choices=[False,True], você acaba com "False" e "True" considerados True (devido a uma conversão de string para bool?). Talvez problema relacionado #
dolphin
41
Certo, acho que não há justificativa para que isso não funcione conforme o esperado. E isso é extremamente enganador, pois não há verificações de segurança nem mensagens de erro.
dolphin
69
@mgilson - O que eu acho enganoso é que você pode definir type = bool, não recebe mensagem de erro e, no entanto, para os argumentos de string "False" e "True", você obtém True na sua variável supostamente booleana (devido a como conversão de tipo funciona em python). Portanto, type = bool não deve ser claramente suportado (emite algum aviso, erro, etc.) ou deve funcionar de maneira útil e esperada intuitivamente.
dolphin
14
@ Dolphin - respectivamente, eu discordo. Eu acho que o comportamento é exatamente do jeito que deveria ser e é consistente com o zen do python "Casos especiais não são especiais o suficiente para quebrar as regras". No entanto, se você sente isso fortemente, por que não trazê-lo para uma das várias listas de discussão em python ? Lá, você pode convencer alguém que tem o poder de fazer algo sobre esse problema. Mesmo se você fosse capaz de me convencer, você terá só conseguiu me convencer e o comportamento ainda não vai mudar desde que eu não sou um dev :)
mgilson
15
Estamos discutindo o que a bool()função Python deve fazer ou o que o argparse deve aceitar type=fn? Todas as argparseverificações é que fné exigível. Ele espera fnreceber um argumento de sequência e retornar um valor. O comportamento de fné de responsabilidade do programador, não argparse's.
Hpaulj # 7/13
235

Eu recomendo a resposta de mgilson, mas com um grupo mutuamente exclusivo,
para que você não possa usar --featuree --no-featureao mesmo tempo.

command --feature

e

command --no-feature

mas não

command --feature --no-feature

Roteiro:

feature_parser = parser.add_mutually_exclusive_group(required=False)
feature_parser.add_argument('--feature', dest='feature', action='store_true')
feature_parser.add_argument('--no-feature', dest='feature', action='store_false')
parser.set_defaults(feature=True)

Você pode usar esse auxiliar se quiser definir muitos deles:

def add_bool_arg(parser, name, default=False):
    group = parser.add_mutually_exclusive_group(required=False)
    group.add_argument('--' + name, dest=name, action='store_true')
    group.add_argument('--no-' + name, dest=name, action='store_false')
    parser.set_defaults(**{name:default})

add_bool_arg(parser, 'useful-feature')
add_bool_arg(parser, 'even-more-useful-feature')
fnkr
fonte
5
@CharlieParker add_argumenté chamado com dest='feature'. set_defaultsé chamado com feature=True. Compreendo?
precisa saber é o seguinte
4
Essa ou a resposta de mgilson deveria ter sido a resposta aceita - mesmo que o OP quisesse --flag False, parte das respostas do SO deveria ser sobre O QUE eles estão tentando resolver, não apenas COMO. Deve haver absolutamente nenhuma razão para fazer --flag Falseou --other-flag Truee, em seguida, usar algum analisador personalizado para converter a string para um boolean .. action='store_true'e action='store_false'são as melhores maneiras de usar bandeiras booleanas
kevlarr
6
@ cowlinator Por que SO é, em última análise, sobre responder "perguntas como declaradas"? De acordo com suas próprias diretrizes , uma resposta ... can be “don’t do that”, but it should also include “try this instead”que (pelo menos para mim) implica respostas deve ser mais profunda quando apropriado. Definitivamente, há momentos em que alguns de nós que postamos perguntas podem se beneficiar de orientações sobre melhores / melhores práticas, etc. Responder "como indicado" geralmente não faz isso. Dito isto, sua frustração com as respostas geralmente assumindo muito (ou incorretamente) é completamente válida.
Kevlarr #
2
Se alguém deseja ter um terceiro valor para quando o usuário não especificou o recurso explicitamente, ele precisa substituir a última linha parser.set_defaults(feature=None)
Alex Che
2
Se queremos adicionar uma help=entrada para esse argumento, para onde deveria ir? Na add_mutually_exclusive_group()ligação? Em uma ou nas duas add_argument()chamadas? Em outro lugar?
Ken Williams
57

Aqui está outra variação sem linhas extras para definir os valores padrão. O bool sempre tem um valor atribuído para que possa ser usado em instruções lógicas sem pré-verificações.

import argparse
parser = argparse.ArgumentParser(description="Parse bool")
parser.add_argument("--do-something", default=False, action="store_true" , help="Flag to do something")
args = parser.parse_args()

if args.do_something:
     print("Do something")
else:
     print("Don't do something")
print("Check that args.do_something=" + str(args.do_something) + " is always a bool")
Schaki
fonte
6
Esta resposta é subestimada, mas maravilhosa em sua simplicidade. Não tente definir, required=Trueou você sempre obterá um argumento True.
precisa
1
Por favor, NUNCA use operador de igualdade em coisas como bool ou NoneType. Em vez disso, você deve usar o IS
webKnjaZ
2
Essa é uma resposta melhor que a aceita, pois simplesmente verifica a presença do sinalizador para definir o valor booleano, em vez de exigir uma sequência booleana redundante. (Yo dawg, eu ouvi você gosta booleans ... então eu dei-lhe um booleano com o seu boolean para definir o seu boolean!)
Siphon
4
Hmm ... a questão, como afirmado, parece querer usar "True" / "False" na própria linha de comando; no entanto, neste exemplo, python3 test.py --do-something Falsefalha com error: unrecognized arguments: False, por isso realmente não responde à pergunta.
Sdbbs 26/11/19
38

oneliner:

parser.add_argument('--is_debug', default=False, type=lambda x: (str(x).lower() == 'true'))
Evalds Urtans
fonte
4
bom para os fãs oneliner, também poderia ser um pouco melhorado:type=lambda x: (str(x).lower() in ['true','1', 'yes'])
Tu Bui
35

Parece haver alguma confusão sobre o que type=boole type='bool'pode significar. Um (ou ambos) deve significar 'executar a função bool()ou' retornar um booleano '? Tal como está type='bool', não significa nada. add_argumentdá um 'bool' is not callableerro, igual ao que você usou type='foobar', ou type='int'.

Mas argparsepossui um registro que permite definir palavras-chave como esta. É usado principalmente para action, por exemplo, `action = 'store_true'. Você pode ver as palavras-chave registradas com:

parser._registries

que exibe um dicionário

{'action': {None: argparse._StoreAction,
  'append': argparse._AppendAction,
  'append_const': argparse._AppendConstAction,
...
 'type': {None: <function argparse.identity>}}

Existem muitas ações definidas, mas apenas um tipo, o padrão argparse.identity,.

Este código define uma palavra-chave 'bool':

def str2bool(v):
  #susendberg's function
  return v.lower() in ("yes", "true", "t", "1")
p = argparse.ArgumentParser()
p.register('type','bool',str2bool) # add type keyword to registries
p.add_argument('-b',type='bool')  # do not use 'type=bool'
# p.add_argument('-b',type=str2bool) # works just as well
p.parse_args('-b false'.split())
Namespace(b=False)

parser.register()não está documentado, mas também não está oculto. Para a maior parte o programador não precisa saber sobre isso, porque typee actionfunção take e de classe valores. Existem muitos exemplos de stackoverflow para definir valores personalizados para ambos.


Caso isso não seja óbvio na discussão anterior, bool()não significa 'analisar uma string'. Na documentação do Python:

bool (x): converte um valor em um booleano, usando o procedimento padrão de teste da verdade.

Compare isso com

int (x): converte um número ou sequência x em um número inteiro.

hpaulj
fonte
3
Ou use: parser.register ('type', 'bool', (lambda x: x.lower () em ("yes", "true", "t", "1")))
Matyas
17

Eu estava procurando o mesmo problema, e imho a solução bonita é:

def str2bool(v):
  return v.lower() in ("yes", "true", "t", "1")

e usando isso para analisar a string como booleana, conforme sugerido acima.

Susundberg
fonte
5
Se você for seguir esse caminho, sugiro distutils.util.strtobool(v).
precisa saber é o seguinte
1
Os distutils.util.strtoboolretornos 1 ou 0, e não um booleano real.
precisa saber é o seguinte
14

Uma maneira bastante semelhante é usar:

feature.add_argument('--feature',action='store_true')

e se você definir o argumento --feature em seu comando

 command --feature

o argumento será True, se você não definir type --feature, o argumento default será sempre False!

dl.meteo
fonte
1
Existe alguma desvantagem nesse método que as outras respostas superam? Essa parece ser de longe a solução mais fácil e sucinta que chega ao que o OP (e nesse caso eu) queria. Eu amo isso.
Simon O'Hanlon
2
Embora simples, ele não responde à pergunta. OP quer um argumento onde você pode especificar--feature False
Astariul
12

Isso funciona para tudo o que eu espero:

add_boolean_argument(parser, 'foo', default=True)
parser.parse_args([])                   # Whatever the default was
parser.parse_args(['--foo'])            # True
parser.parse_args(['--nofoo'])          # False
parser.parse_args(['--foo=true'])       # True
parser.parse_args(['--foo=false'])      # False
parser.parse_args(['--foo', '--nofoo']) # Error

O código:

def _str_to_bool(s):
    """Convert string to bool (in argparse context)."""
    if s.lower() not in ['true', 'false']:
        raise ValueError('Need bool; got %r' % s)
    return {'true': True, 'false': False}[s.lower()]

def add_boolean_argument(parser, name, default=False):                                                                                               
    """Add a boolean argument to an ArgumentParser instance."""
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        '--' + name, nargs='?', default=default, const=True, type=_str_to_bool)
    group.add_argument('--no' + name, dest=name, action='store_false')
Stumpy Joe Pete
fonte
Excelente! Eu estou indo com esta resposta. Apertei o botão _str_to_bool(s)para converter s = s.lower()uma vez, depois testar if s not in {'true', 'false', '1', '0'}e finalmente return s in {'true', '1'}.
precisa saber é o seguinte
6

Uma maneira mais simples seria usar como abaixo.

parser.add_argument('--feature', type=lambda s: s.lower() in ['true', 't', 'yes', '1'])
arunkumarreddy
fonte
5

Mais simples. Não é flexível, mas prefiro a simplicidade.

  parser.add_argument('--boolean_flag',
                      help='This is a boolean flag.',
                      type=eval, 
                      choices=[True, False], 
                      default='True')

EDIT: Se você não confia na entrada, não use eval.

Russell
fonte
Isso parece bastante conveniente. Notei que você tem eval como o tipo. Eu tinha uma pergunta sobre isso: como avaliar deve ser definido ou é necessária uma importação para fazer uso dela?
edesz 28/04
1
evalé uma função interna. docs.python.org/3/library/functions.html#eval Essa pode ser uma função unária que outras abordagens mais flexíveis aproveitam.
Russell
Ei, isso é ótimo. Obrigado!
edesz 29/04
2
isso é fofo, mas é bastante arriscado simplesmente expor-se à natureza, onde os usuários que não têm consciência de que eval é mau apenas copiam e colam em seus scripts.
Arne
@ Arne, bom ponto. Embora, parece que seria muito difícil para um usuário bem-intencionado acidentalmente fazer algo pernicioso.
Russell
3

A maneira mais simples seria usar as opções :

parser = argparse.ArgumentParser()
parser.add_argument('--my-flag',choices=('True','False'))

args = parser.parse_args()
flag = args.my_flag == 'True'
print(flag)

Não passando --my-flag é avaliado como False. A opção required = True pode ser adicionada se você sempre quiser que o usuário especifique explicitamente uma opção.

gerardw
fonte
2

Eu acho que a maneira mais canônica será:

parser.add_argument('--ensure', nargs='*', default=None)

ENSURE = config.ensure is None
Andreas Maertens
fonte
1
class FlagAction(argparse.Action):
    # From http://bugs.python.org/issue8538

    def __init__(self, option_strings, dest, default=None,
                 required=False, help=None, metavar=None,
                 positive_prefixes=['--'], negative_prefixes=['--no-']):
        self.positive_strings = set()
        self.negative_strings = set()
        for string in option_strings:
            assert re.match(r'--[A-z]+', string)
            suffix = string[2:]
            for positive_prefix in positive_prefixes:
                self.positive_strings.add(positive_prefix + suffix)
            for negative_prefix in negative_prefixes:
                self.negative_strings.add(negative_prefix + suffix)
        strings = list(self.positive_strings | self.negative_strings)
        super(FlagAction, self).__init__(option_strings=strings, dest=dest,
                                         nargs=0, const=None, default=default, type=bool, choices=None,
                                         required=required, help=help, metavar=metavar)

    def __call__(self, parser, namespace, values, option_string=None):
        if option_string in self.positive_strings:
            setattr(namespace, self.dest, True)
        else:
            setattr(namespace, self.dest, False)
Robert T. McGibbon
fonte
1

A maneira mais simples e correta é

from distutils import util
arser.add_argument('--feature', dest='feature', type=lambda x:bool(distutils.util.strtobool(x)))

Observe que os valores verdadeiros são y, sim, t, verdadeiro, ativado e 1; valores falsos são n, não, f, falso, desligado e 0. Gera ValueError se val for outra coisa.

Akash Desarda
fonte
0

Rápido e fácil, mas apenas para argumentos 0 ou 1:

parser.add_argument("mybool", default=True,type=lambda x: bool(int(x)))
myargs=parser.parse_args()
print(myargs.mybool)

A saída será "False" após a chamada do terminal:

python myscript.py 0
FEMengineer
fonte
-1

Semelhante ao Akash, mas aqui está outra abordagem que eu usei. Ele usa do strque lambdaporque python lambdasempre me dá um sentimento estranho.

import argparse
from distutils.util import strtobool

parser = argparse.ArgumentParser()
parser.add_argument("--my_bool", type=str, default="False")
args = parser.parse_args()

if bool(strtobool(args.my_bool)) is True:
    print("OK")
Youngjae
fonte
-1

Como uma melhoria na resposta de @Akash Desarda, você pode fazer

import argparse
from distutils.util import strtobool

parser = argparse.ArgumentParser()
parser.add_argument("--foo", 
    type=lambda x:bool(strtobool(x)),
    nargs='?', const=True, default=False)
args = parser.parse_args()
print(args.foo)

E suporta python test.py --foo

(base) [costa@costa-pc code]$ python test.py
False
(base) [costa@costa-pc code]$ python test.py --foo 
True
(base) [costa@costa-pc code]$ python test.py --foo True
True
(base) [costa@costa-pc code]$ python test.py --foo False
False
Costa Huang
fonte