¿Cuál es la mejor práctica para manejar tuplas de un solo valor en Python?

Estoy utilizando una función de biblioteca de terceros que lee un conjunto de palabras clave de un archivo y se supone que devuelve una tupla de valores. Lo hace correctamente siempre que haya al menos dos palabras clave. Sin embargo, en el caso de que solo haya una palabra clave, devuelve una cadena en bruto, no una tupla de tamaño uno. Esto es particularmente pernicioso porque cuando bash hacer algo como

for keyword in library.get_keywords(): # Do something with keyword 

, en el caso de la palabra clave única, la itera sobre cada carácter de la cadena en sucesión, que no produce ninguna excepción, en tiempo de ejecución o de otro tipo, pero sin embargo es completamente inútil para mí.

Mi pregunta es doble:

Claramente, este es un error en la biblioteca, que está fuera de mi control. ¿Cómo puedo solucionarlo mejor?

En segundo lugar, en general, si estoy escribiendo una función que devuelve una tupla, ¿cuál es la mejor práctica para garantizar que las tuplas con un elemento se generen correctamente? Por ejemplo, si tengo

 def tuple_maker(values): my_tuple = (values) return my_tuple for val in tuple_maker("a string"): print "Value was", val for val in tuple_maker(["str1", "str2", "str3"]): print "Value was", val 

yo obtengo

 Value was a Value was Value was s Value was t Value was r Value was i Value was n Value was g Value was str1 Value was str2 Value was str3 

¿Cuál es la mejor manera de modificar la función my_tuple para devolver una tupla cuando solo hay un elemento? ¿Necesito explícitamente verificar si el tamaño es 1 y crear la tupla por separado, usando la syntax (value,) ? Esto implica que cualquier función que tenga la posibilidad de devolver una tupla de un solo valor debe hacer esto, lo que parece pirata y repetitivo.

¿Hay alguna solución general elegante a este problema?

Necesitas probar de alguna manera el tipo, si es una cadena o una tupla. Lo haría así:

 keywords = library.get_keywords() if not isinstance(keywords, tuple): keywords = (keywords,) # Note the comma for keyword in keywords: do_your_thang(keyword) 

Para su primer problema, no estoy realmente seguro de si esta es la mejor respuesta, pero creo que necesita verificar si el valor devuelto es una cadena o tupla y actuar en consecuencia.

En cuanto a su segundo problema, cualquier variable puede convertirse en una tupla de un solo valor al colocar una , junto a ella:

 >>> x='abc' >>> x 'abc' >>> tpl=x, >>> tpl ('abc',) 

Poniendo estas dos ideas juntas:

 >>> def make_tuple(k): ... if isinstance(k,tuple): ... return k ... else: ... return k, ... >>> make_tuple('xyz') ('xyz',) >>> make_tuple(('abc','xyz')) ('abc', 'xyz') 

Nota: En mi humilde opinión, en general es una mala idea usar la instancia, o cualquier otra forma de lógica que necesite verificar el tipo de un objeto en tiempo de ejecución. Pero para este problema no veo ninguna manera de evitarlo.

Tu tuple_maker no hace lo que crees que hace. Una definición equivalente de tuple maker a la tuya es

 def tuple_maker(input): return input 

Lo que estás viendo es que tuple_maker("a string") devuelve una cadena, mientras que tuple_maker(["str1","str2","str3"]) devuelve una lista de cadenas; ¡Tampoco devuelve una tupla!

Las tuplas en Python se definen por la presencia de comas, no entre paréntesis. Por lo tanto (1,2) es una tupla que contiene los valores 1 y 2 , mientras que (1,) es una tupla que contiene el valor único 1 .

Para convertir un valor en una tupla, como han señalado otros, use la tuple .

 >>> tuple([1]) (1,) >>> tuple([1,2]) (1,2) 

¡Siempre hay crianza de monos!

 # Store a reference to the real library function really_get_keywords = library.get_keywords # Define out patched version of the function, which uses the real # version above, adjusting its return value as necessary def patched_get_keywords(): """Make sure we always get a tuple of keywords.""" result = really_get_keywords() return result if isinstance(result, tuple) else (result,) # Install the patched version library.get_keywords = patched_get_keywords 

NOTA: Este código puede quemar tu casa y dormir con tu esposa.

En lugar de verificar una longitud de 1, usaría isinstance incorporado en su lugar.

 >>> isinstance('a_str', tuple) False >>> isinstance(('str1', 'str2', 'str3'), tuple) True 

¿Es absolutamente necesario que devuelva las tuplas, o lo hará algún iterable?

 import collections def iterate(keywords): if not isinstance(keywords, collections.Iterable): yield keywords else: for keyword in keywords: yield keyword for keyword in iterate(library.get_keywords()): print keyword 

para su primer problema, puede verificar si el valor de retorno es tupla usando

 type(r) is tuple #alternative isinstance(r, tuple) # one-liner def as_tuple(r): return [ tuple([r]), r ][type(r) is tuple] 

la segunda cosa es que me gusta usar la tuple([1]) . Creo que es una cuestión de gusto. Probablemente también podría escribir un contenedor, por ejemplo def tuple1(s): return tuple([s])

Hay una cosa importante a tener en cuenta al usar el método constructor tuple () en lugar de la definición de tipo predeterminada para crear tus tuplas de una sola cadena. Aquí hay un script Nose2 / Unittest que puedes usar para jugar con el problema:

 #!/usr/bin/env python # vim: ts=4 sw=4 sts=4 et from __future__ import print_function # global import unittest import os import sys import logging import pprint import shutil # module-level logger logger = logging.getLogger(__name__) # module-global test-specific imports # where to put test output data for compare. testdatadir = os.path.join('.', 'test', 'test_data') rawdata_dir = os.path.join(os.path.expanduser('~'), 'Downloads') testfiles = ( 'bogus.data', ) purge_results = False output_dir = os.path.join('test_data', 'example_out') def cleanPath(path): '''cleanPath Recursively removes everything below a path :param path: the path to clean ''' for root, dirs, files in os.walk(path): for fn in files: logger.debug('removing {}'.format(fn)) os.unlink(os.path.join(root, fn)) for dn in dirs: # recursive try: logger.debug('recursive del {}'.format(dn)) shutil.rmtree(os.path.join(root, dn)) except Exception: # for now, halt on all. Override with shutil onerror # callback and ignore_errors. raise class TestChangeMe(unittest.TestCase): ''' TestChangeMe ''' testdatadir = None rawdata_dir = None testfiles = None output_dir = output_dir def __init__(self, *args, **kwargs): self.testdatadir = os.path.join(os.path.dirname( os.path.abspath(__file__)), testdatadir) super(TestChangeMe, self).__init__(*args, **kwargs) # check for kwargs # this allows test control by instance self.testdatadir = kwargs.get('testdatadir', testdatadir) self.rawdata_dir = kwargs.get('rawdata_dir', rawdata_dir) self.testfiles = kwargs.get('testfiles', testfiles) self.output_dir = kwargs.get('output_dir', output_dir) def setUp(self): '''setUp pre-test setup called before each test ''' logging.debug('setUp') if not os.path.exists(self.testdatadir): os.mkdir(self.testdatadir) else: self.assertTrue(os.path.isdir(self.testdatadir)) self.assertTrue(os.path.exists(self.testdatadir)) cleanPath(self.output_dir) def tearDown(self): '''tearDown post-test cleanup, if required ''' logging.debug('tearDown') if purge_results: cleanPath(self.output_dir) def tupe_as_arg(self, tuple1, tuple2, tuple3, tuple4): '''test_something_0 auto-run tests sorted by ascending alpha ''' # for testing, recreate strings and lens string1 = 'string number 1' len_s1 = len(string1) string2 = 'string number 2' len_s2 = len(string2) # run the same tests... # should test as type = string self.assertTrue(type(tuple1) == str) self.assertFalse(type(tuple1) == tuple) self.assertEqual(len_s1, len_s2, len(tuple1)) self.assertEqual(len(tuple2), 1) # this will fail # self.assertEqual(len(tuple4), 1) self.assertEqual(len(tuple3), 2) self.assertTrue(type(string1) == str) self.assertTrue(type(string2) == str) self.assertTrue(string1 == tuple1) # should test as type == tuple self.assertTrue(type(tuple2) == tuple) self.assertTrue(type(tuple4) == tuple) self.assertFalse(type(tuple1) == type(tuple2)) self.assertFalse(type(tuple1) == type(tuple4)) # this will fail # self.assertFalse(len(tuple4) == len(tuple1)) self.assertFalse(len(tuple2) == len(tuple1)) def default_test(self): '''testFileDetection Tests all data files for type and compares the results to the current stored results. ''' # test 1 __import__('pudb').set_trace() string1 = 'string number 1' len_s1 = len(string1) string2 = 'string number 2' len_s2 = len(string2) tuple1 = (string1) tuple2 = (string1,) tuple3 = (string1, string2) tuple4 = tuple(string1,) # should test as type = string self.assertTrue(type(tuple1) == str) self.assertFalse(type(tuple1) == tuple) self.assertEqual(len_s1, len_s2, len(tuple1)) self.assertEqual(len(tuple2), 1) # this will fail # self.assertEqual(len(tuple4), 1) self.assertEqual(len(tuple3), 2) self.assertTrue(type(string1) == str) self.assertTrue(type(string2) == str) self.assertTrue(string1 == tuple1) # should test as type == tuple self.assertTrue(type(tuple2) == tuple) self.assertTrue(type(tuple4) == tuple) self.assertFalse(type(tuple1) == type(tuple2)) self.assertFalse(type(tuple1) == type(tuple4)) # this will fail # self.assertFalse(len(tuple4) == len(tuple1)) self.assertFalse(len(tuple2) == len(tuple1)) self.tupe_as_arg(tuple1, tuple2, tuple3, tuple4) # stand-alone test execution if __name__ == '__main__': import nose2 nose2.main( argv=[ 'fake', '--log-capture', 'TestChangeMe.default_test', ]) 

Notará que el código (casi) idéntico que llama tupla (cadena1,) se muestra como tipo tupla, pero la longitud será la misma que la longitud de la cadena y todos los miembros serán caracteres individuales.

Esto hará que las afirmaciones en las líneas # 137, # 147, # 104 y # 115 fracasen, aunque parezcan idénticas a las que pasan.

(Nota: tengo un punto de interrupción de PUDB en el código de la línea 124, es una excelente herramienta de depuración, pero puede eliminarlo si lo prefiere. De lo contrario, simplemente pip install pudb para usarlo).