What's the best practice for handling single-value tuples in Python?

前端 未结 9 1725
长发绾君心
长发绾君心 2021-02-19 05:44

I am using a 3rd party library function which reads a set of keywords from a file, and is supposed to return a tuple of values. It does this correctly as long as there are at le

相关标签:
9条回答
  • 2021-02-19 06:01

    There's always monkeypatching!

    # 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
    

    NOTE: This code might burn down your house and sleep with your wife.

    0 讨论(0)
  • 2021-02-19 06:02

    for your first problem you could check if the return value is tuple using

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

    the second thing i like to use tuple([1]). think it is a matter of taste. could probably also write a wrapper, for example def tuple1(s): return tuple([s])

    0 讨论(0)
  • 2021-02-19 06:03

    There is an important thing to watch out for when using the tuple() constructor method instead of the default type definition for creating your single-string tuples. Here is a Nose2/Unittest script you can use to play with the problem:

    #!/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',
            ])
    

    You will notice that the (nearly) identical code calling tuple(string1,) shows as type tuple, but the length will be the same as the string length and all members will be single characters.

    This will cause the assertions on lines #137, #147, #104 and #115 to fail, even though they are seemingly identical to the ones that pass.

    (note: I have a PUDB breakpoint in the code at line #124, it's an excellent debug tool, but you can remove it if you prefer. Otherwise simply pip install pudb to use it.)

    0 讨论(0)
  • 2021-02-19 06:11

    The () have nothing to do with tuples in python, the tuple syntax uses ,. The ()-s are optional.

    E.g.:

    >>> a=1, 2, 3
    >>> type(a)
    <class 'tuple'>
    >>> a=1,
    >>> type(a)
    <class 'tuple'>
    >>> a=(1)
    >>> type(a)
    <class 'int'>
    

    I guess this is the root of the problem.

    0 讨论(0)
  • 2021-02-19 06:14

    You need to somehow test for the type, if it's a string or a tuple. I'd do it like this:

    keywords = library.get_keywords()
    if not isinstance(keywords, tuple):
        keywords = (keywords,) # Note the comma
    for keyword in keywords:
        do_your_thang(keyword)
    
    0 讨论(0)
  • 2021-02-19 06:14

    Rather than checking for a length of 1, I'd use the isinstance built-in instead.

    >>> isinstance('a_str', tuple)
    False
    >>> isinstance(('str1', 'str2', 'str3'), tuple)
    True
    
    0 讨论(0)
提交回复
热议问题