Análisis de valores booleanos con argparse

616

Me gustaría utilizar argparse para analizar argumentos de línea de comandos booleanos escritos como "--foo True" o "--foo False". Por ejemplo:

my_program --my_boolean_flag False

Sin embargo, el siguiente código de prueba no hace lo que me gustaría:

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)

Tristemente, parsed_args.my_boolevalúa a True. Este es el caso incluso cuando cambio cmd_linea ser ["--my_bool", ""], lo cual es sorprendente, ya que bool("")evalúa False.

¿Cómo puedo obtener argparse a analizar "False", "F"y su minúscula variantes que se vayan False?

Súper eléctrico
fuente
40
Aquí hay una interpretación de la respuesta de @mgilson parser.add_argument('--feature', dest='feature', default=False, action='store_true') . Esta solución le garantizará que siempre obtenga un booltipo con valor Trueo False. (Esta solución tiene una restricción: su opción debe tener un valor predeterminado).
Trevor Boyd Smith
77
Aquí hay una interpretación de la respuesta de @ Maximparser.add_argument('--feature', dest='feature', type=lambda x:bool(distutils.util.strtobool(x))) . Cuando se utiliza la opción, esta solución garantizará un booltipo con valor de Trueo False. Cuando no se utiliza la opción, obtendrá None. ( distutils.util.strtobool(x)es de otra pregunta de stackoverflow )
Trevor Boyd Smith
8
¿ parser.add_argument('--my_bool', action='store_true', default=False)
Qué tal

Respuestas:

276

Otra solución más usando las sugerencias anteriores, pero con el error de análisis "correcto" 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.')

Esto es muy útil para hacer cambios con valores predeterminados; por ejemplo

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

me permite usar:

script --nice
script --nice <bool>

y aún usa un valor predeterminado (específico para la configuración del usuario). Una desventaja (indirectamente relacionada) con ese enfoque es que los 'nargs' pueden atrapar un argumento posicional: vea esta pregunta relacionada y este informe de error de argparse .

Máxima
fuente
44
nargs = '?' significa cero o un argumento. docs.python.org/3/library/argparse.html#nargs
Maxim
1
Me encanta esto, pero mi equivalente de default = NICE me está dando un error, por lo que debo hacer otra cosa.
Michael Mathews
2
@MarcelloRomani str2bool no es un tipo en el sentido de Python, es la función definida anteriormente, debe incluirla en alguna parte.
Maxim
44
el código de str2bool(v)podría ser reemplazado por bool(distutils.util.strtobool(v)). Fuente: stackoverflow.com/a/18472142/2436175
Antonio
44
Quizás valga la pena mencionar que de esta manera no se puede verificar si el argumento está configurado if args.nice:porque si el argumento está configurado como False, nunca pasará la condición. Si esto es correcto, entonces quizás es mejor a la lista de volver de str2boolla función y la lista de canciones como constparámetro, como este [True], [False].
Corrígeme
889

Creo que una forma más canónica de hacer esto es a través de:

command --feature

y

command --no-feature

argparse admite esta versión muy bien:

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

Por supuesto, si realmente quieres la --arg <True|False>versión, puedes pasarla ast.literal_evalcomo "tipo" o como una función definida por el usuario ...

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
fuente
96
Todavía creo que type=booldebería funcionar fuera de la caja (¡considere los argumentos posicionales!) Incluso cuando especificas adicionalmente choices=[False,True], terminas con "Falso" y "Verdadero" considerado Verdadero (¿debido a un lanzamiento de cadena a bool?). Tal vez un problema relacionado
delfín
41
Bien, creo que no hay justificación para que esto no funcione como se esperaba. Y esto es extremadamente engañoso, ya que no hay controles de seguridad ni mensajes de error.
Delfín
69
@mgilson: lo que encuentro engañoso es que puede establecer type = bool, no recibe ningún mensaje de error y, sin embargo, para los argumentos de cadena "False" y "True", obtiene True en su variable supuestamente booleana (debido a cómo tipo de fundición funciona en python). Por lo tanto, type = bool debería ser claramente incompatible (emitir alguna advertencia, error, etc.), o debería funcionar de una manera que sea útil e intuitivamente esperada.
Delfín
14
@dolphin - respectivamente, no estoy de acuerdo. Creo que el comportamiento es exactamente como debería ser y es consistente con el zen de Python "Los casos especiales no son lo suficientemente especiales como para romper las reglas". Sin embargo, si siente esto fuertemente al respecto, ¿por qué no aparece en una de las diversas listas de correo de Python ? Allí, puede tener la oportunidad de convencer a alguien que tiene el poder de hacer algo al respecto. Incluso si pudieras convencerme, solo habrás logrado convencerme y el comportamiento aún no cambiará ya que no soy un desarrollador :)
mgilson
15
¿Estamos discutiendo sobre lo que bool()debería hacer la función Python , o qué argumento debería aceptar type=fn? Todos los argparsecheques fnson invocables. Espera fntomar un argumento de cadena y devolver un valor. El comportamiento de fnes responsabilidad del programador, no argparse's.
hpaulj
235

Recomiendo la respuesta de mgilson pero con un grupo mutuamente excluyente
para que no pueda usar --featurey --no-featureal mismo tiempo.

command --feature

y

command --no-feature

pero no

command --feature --no-feature

Guión:

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)

Luego puede usar este asistente si va a configurar muchos de ellos:

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
fuente
55
@CharlieParker add_argumentse llama con dest='feature'. set_defaultsse llama con feature=True. ¿Entender?
fnkr
44
Esta o la respuesta de mgilson debería haber sido la respuesta aceptada, a pesar de que el OP quería --flag False, parte de las respuestas de SO deberían ser sobre QUÉ están tratando de resolver, no solo sobre CÓMO. No debería haber absolutamente ninguna razón para hacer --flag Falseo --other-flag Trueusar un analizador personalizado para convertir la cadena a booleana ... action='store_true'y action='store_false'son las mejores formas de usar banderas booleanas
kevlarr
66
@cowlinator ¿Por qué SO se trata en última instancia de responder "preguntas como se indica"? De acuerdo con sus propias pautas , una respuesta ... can be “don’t do that”, but it should also include “try this instead”que (al menos para mí) implica respuestas debe profundizar cuando sea apropiado. Definitivamente, hay momentos en que algunos de nosotros que publicamos preguntas pueden beneficiarnos de la orientación sobre mejores / mejores prácticas, etc. Responder "como se dice" a menudo no hace eso. Dicho esto, su frustración con las respuestas a menudo suponiendo demasiado (o incorrectamente) es completamente válida.
kevlarr
2
Si uno quiere tener un tercer valor para cuando el usuario no ha especificado la función explícitamente, debe reemplazar la última línea con elparser.set_defaults(feature=None)
Alex Che
2
Si queremos agregar una help=entrada para este argumento, ¿a dónde debería ir? En la add_mutually_exclusive_group()llamada? ¿En una o ambas add_argument()llamadas? ¿En algún otro lugar?
Ken Williams
57

Aquí hay otra variación sin fila / s extra para establecer valores predeterminados. El bool siempre tiene un valor asignado para que pueda usarse en declaraciones lógicas sin comprobaciones previas.

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
fuente
66
Esta respuesta es subestimada, pero maravillosa en su simplicidad. No intentes configurarlo required=Trueo de lo contrario siempre obtendrás un argumento verdadero.
Garren S
1
Por favor, NUNCA utilice operador de igualdad en cosas como bool o NoneType. Deberías usar IS en su lugar
webKnjaZ
2
Esta es una mejor respuesta que la aceptada porque simplemente verifica la presencia de la bandera para establecer el valor booleano, en lugar de requerir una cadena booleana redundante. (Hola, te escuché como booleanos ... ¡así que te di un booleano con tu booleano para configurar tu booleano!)
Siphon
44
Hmm ... la pregunta, como se dijo, parece querer usar "Verdadero" / "Falso" en la línea de comando; sin embargo, con este ejemplo, python3 test.py --do-something Falsefalla error: unrecognized arguments: False, por lo que realmente no responde la pregunta.
sdbbs
38

un trazador de líneas:

parser.add_argument('--is_debug', default=False, type=lambda x: (str(x).lower() == 'true'))
Evalds Urtans
fuente
44
bueno para fan de oneliner, también podría mejorarse un poco:type=lambda x: (str(x).lower() in ['true','1', 'yes'])
Tu Bui el
35

Parece haber cierta confusión en cuanto a qué type=booly type='bool'podría significar. ¿Debería uno (o ambos) significar 'ejecutar la función bool()o' devolver un booleano '? Tal como está type='bool', no significa nada. add_argumentda un 'bool' is not callableerror, igual que si usara type='foobar', otype='int' .

Pero argparsetiene un registro que le permite definir palabras clave como esta. Se utiliza principalmente para action, por ejemplo, `action = 'store_true'. Puede ver las palabras clave registradas con:

parser._registries

que muestra un diccionario

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

Hay muchas acciones definidas, pero solo un tipo, el predeterminado, argparse.identity .

Este código define una palabra clave '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()no está documentado, pero tampoco está oculto. En su mayor parte el programador no necesita saber al respecto porque typey actionvalores de la función toma y de clase. Hay muchos ejemplos de stackoverflow para definir valores personalizados para ambos.


En caso de que no sea obvio de la discusión anterior, bool()no significa 'analizar una cadena'. De la documentación de Python:

bool (x): convierte un valor a un booleano, utilizando el procedimiento estándar de prueba de verdad.

Contrasta esto con

int (x): Convierte un número o cadena x en un entero.

hpaulj
fuente
3
O use: parser.register ('type', 'bool', (lambda x: x.lower () en ("yes", "true", "t", "1")))
Matyas
17

Estaba buscando el mismo problema, y ​​en mi humilde opinión, la bonita solución es:

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

y usar eso para analizar la cadena a booleano como se sugirió anteriormente.

Susundberg
fuente
55
Si vas a ir por esta ruta, te sugiero distutils.util.strtobool(v).
CivFan
1
Los distutils.util.strtoboolrendimientos de 1 o 0, no un booleano real.
CMCDragonkai
14

Una forma bastante similar es usar:

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

y si establece el argumento --característica en su comando

 command --feature

el argumento será Verdadero, si no establece el tipo --característica, el argumento predeterminado siempre es Falso!

dl.meteo
fuente
1
¿Hay algún inconveniente en este método que las otras respuestas superen? Esta parece ser, con mucho, la solución más fácil y sucinta que llega a lo que quería el OP (y en este caso yo). Me encanta.
Simon O'Hanlon
2
Si bien es simple, no responde la pregunta. OP quiere un argumento donde puede especificar--feature False
Astariul
12

Esto funciona para todo lo que 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

El 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
fuente
¡Excelente! Me voy con esta respuesta. Ajusté mi _str_to_bool(s)para convertir s = s.lower()una vez, luego probar if s not in {'true', 'false', '1', '0'}y finalmente return s in {'true', '1'}.
Jerry101
6

Una forma más sencilla sería usar la siguiente.

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

Más simple No es flexible, pero prefiero la simplicidad.

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

EDITAR: si no confía en la entrada, no la use eval.

Russell
fuente
Esto parece bastante conveniente. Noté que tienes eval como el tipo. Tenía una pregunta sobre esto: ¿cómo debería definirse eval, o hay una importación requerida para poder usarla?
edesz
1
evalEs una función incorporada. docs.python.org/3/library/functions.html#eval Esta puede ser cualquier función unaria que aprovechan otros enfoques más flexibles.
Russell
Oye, eso es genial. ¡Gracias!
edesz
2
eso es lindo, pero bastante arriesgado simplemente ponerlo en la naturaleza donde los usuarios que no son conscientes de que Eva es malvado lo copiarán y pegarán en sus scripts.
Arne
@Arne, buen punto. Sin embargo, parece que sería bastante difícil para un usuario bien intencionado hacer accidentalmente algo pernicioso.
Russell
3

La forma más sencilla sería usar opciones :

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

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

No pasa --mi-flag se evalúa como False. La opción required = True podría agregarse si siempre desea que el usuario especifique explícitamente una opción.

gerardw
fuente
2

Creo que la forma más canónica será:

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

ENSURE = config.ensure is None
Andreas Maertens
fuente
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
fuente
1

La forma más simple y correcta es

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

Tenga en cuenta que los valores verdaderos son y, yes, t, true, on y 1; los valores falsos son n, no, f, falso, apagado y 0. Aumenta ValueError si val es otra cosa.

Akash Desarda
fuente
0

Rápido y fácil, pero solo para los argumentos 0 o 1:

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

El resultado será "Falso" después de llamar desde la terminal:

python myscript.py 0
Ingeniero Fem
fuente
-1

Similar a @Akash pero aquí hay otro enfoque que he usado. Utiliza strque lambdadebido pitón lambdasiempre me da un extranjero sentimientos.

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
fuente
-1

Como una mejora a la respuesta de @Akash Desarda, podrías hacer

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)

Y es compatible 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
fuente