Changeset 894 for trunk


Ignore:
Timestamp:
Sep 23, 2016, 10:04:06 AM (3 years ago)
Author:
cito
Message:

Cache the namedtuple classes used for query result rows

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/docs/contents/changelog.rst

    r893 r894  
    22=========
    33
    4 Version 5.0.3 (2016-09-21)
    5 --------------------------
    6 - It is now possible to use a custom array cast method for the pgdb module,
    7   e.g. pgdb.set_typecast('anyarray', lambda v, basecast: v) will cause arrays
    8   to be always returned as strings instead of lists.
     4Version 5.0.3 (2016-09-23)
     5--------------------------
     6- It is now possible to use a custom array cast function by changing
     7  the type caster for the 'anyarray' type.  For instance, by calling
     8  set_typecast('anyarray', lambda v, c: v) you can have arrays returned
     9  as strings instead of lists.  Note that in the pg module, you can also
     10  call set_array(False) in order to return arrays as strings.
     11- The namedtuple classes used for the rows of query results are now cached
     12  and reused internally, since creating namedtuples classes in Python is a
     13  somewhat expensive operation.  By default the cache has a size of 1024
     14  entries, but this can be changed with the set_row_factory_size() function.
     15  In certain cases this change can notably improve the performance.
    916
    1017Version 5.0.2 (2016-09-13)
  • trunk/pg.py

    r886 r894  
    5757except NameError:  # Python >= 3.0
    5858    basestring = (str, bytes)
     59
     60try:
     61    from functools import lru_cache
     62except ImportError:  # Python < 3.2
     63    from functools import update_wrapper
     64    try:
     65        from _thread import RLock
     66    except ImportError:
     67        class RLock:  # for builds without threads
     68            def __enter__(self): pass
     69
     70            def __exit__(self, exctype, excinst, exctb): pass
     71
     72    def lru_cache(maxsize=128):
     73        """Simplified functools.lru_cache decorator for one argument."""
     74
     75        def decorator(function):
     76            sentinel = object()
     77            cache = {}
     78            get = cache.get
     79            lock = RLock()
     80            root = []
     81            root_full = [root, False]
     82            root[:] = [root, root, None, None]
     83
     84            if maxsize == 0:
     85
     86                def wrapper(arg):
     87                    res = function(arg)
     88                    return res
     89
     90            elif maxsize is None:
     91
     92                def wrapper(arg):
     93                    res = get(arg, sentinel)
     94                    if res is not sentinel:
     95                        return res
     96                    res = function(arg)
     97                    cache[arg] = res
     98                    return res
     99
     100            else:
     101
     102                def wrapper(arg):
     103                    with lock:
     104                        link = get(arg)
     105                        if link is not None:
     106                            root = root_full[0]
     107                            prev, next, _arg, res = link
     108                            prev[1] = next
     109                            next[0] = prev
     110                            last = root[0]
     111                            last[1] = root[0] = link
     112                            link[0] = last
     113                            link[1] = root
     114                            return res
     115                    res = function(arg)
     116                    with lock:
     117                        root, full = root_full
     118                        if arg in cache:
     119                            pass
     120                        elif full:
     121                            oldroot = root
     122                            oldroot[2] = arg
     123                            oldroot[3] = res
     124                            root = root_full[0] = oldroot[1]
     125                            oldarg = root[2]
     126                            oldres = root[3]  # keep reference
     127                            root[2] = root[3] = None
     128                            del cache[oldarg]
     129                            cache[arg] = oldroot
     130                        else:
     131                            last = root[0]
     132                            link = [last, root, arg, res]
     133                            last[1] = root[0] = cache[arg] = link
     134                            if len(cache) >= maxsize:
     135                                root_full[1] = True
     136                    return res
     137
     138            wrapper.__wrapped__ = function
     139            return update_wrapper(wrapper, function)
     140
     141        return decorator
    59142
    60143
     
    10221105    def create_array_cast(self, basecast):
    10231106        """Create an array typecast for the given base cast."""
     1107        cast_array = self['anyarray']
    10241108        def cast(v):
    10251109            return cast_array(v, basecast)
     
    10281112    def create_record_cast(self, name, fields, casts):
    10291113        """Create a named record typecast for the given fields and casts."""
     1114        cast_record = self['record']
    10301115        record = namedtuple(name, fields)
    10311116        def cast(v):
     
    11771262
    11781263
     1264# The result rows for database operations are returned as named tuples
     1265# by default. Since creating namedtuple classes is a somewhat expensive
     1266# operation, we cache up to 1024 of these classes by default.
     1267
     1268@lru_cache(maxsize=1024)
     1269def _row_factory(names):
     1270    """Get a namedtuple factory for row results with the given names."""
     1271    try:
     1272        try:
     1273            return namedtuple('Row', names, rename=True)._make
     1274        except TypeError:  # Python 2.6 and 3.0 do not support rename
     1275            names = [v if v.isalnum() else 'column_%d' % (n,)
     1276                     for n, v in enumerate(names)]
     1277            return namedtuple('Row', names)._make
     1278    except ValueError:  # there is still a problem with the field names
     1279        names = ['column_%d' % (n,) for n in range(len(names))]
     1280        return namedtuple('Row', names)._make
     1281
     1282
     1283def set_row_factory_size(maxsize):
     1284    """Change the size of the namedtuple factory cache.
     1285
     1286    If maxsize is set to None, the cache can grow without bound.
     1287    """
     1288    global _row_factory
     1289    _row_factory = lru_cache(maxsize)(_row_factory.__wrapped__)
     1290
     1291
    11791292def _namedresult(q):
    11801293    """Get query result as named tuples."""
    1181     row = namedtuple('Row', q.listfields())
    1182     return [row(*r) for r in q.getresult()]
     1294    row = _row_factory(q.listfields())
     1295    return [row(r) for r in q.getresult()]
    11831296
    11841297
     
    11891302        """Create query from given result rows and field names."""
    11901303        self.result = result
    1191         self.fields = fields
     1304        self.fields = tuple(fields)
    11921305
    11931306    def listfields(self):
  • trunk/pgdb.py

    r893 r894  
    7575from uuid import UUID as Uuid
    7676from math import isnan, isinf
    77 from collections import namedtuple
     77from collections import namedtuple, Iterable
    7878from functools import partial
    7979from re import compile as regex
     
    9595    basestring = (str, bytes)
    9696
    97 from collections import Iterable
     97try:
     98    from functools import lru_cache
     99except ImportError:  # Python < 3.2
     100    from functools import update_wrapper
     101    try:
     102        from _thread import RLock
     103    except ImportError:
     104        class RLock:  # for builds without threads
     105            def __enter__(self): pass
     106
     107            def __exit__(self, exctype, excinst, exctb): pass
     108
     109    def lru_cache(maxsize=128):
     110        """Simplified functools.lru_cache decorator for one argument."""
     111
     112        def decorator(function):
     113            sentinel = object()
     114            cache = {}
     115            get = cache.get
     116            lock = RLock()
     117            root = []
     118            root_full = [root, False]
     119            root[:] = [root, root, None, None]
     120
     121            if maxsize == 0:
     122
     123                def wrapper(arg):
     124                    res = function(arg)
     125                    return res
     126
     127            elif maxsize is None:
     128
     129                def wrapper(arg):
     130                    res = get(arg, sentinel)
     131                    if res is not sentinel:
     132                        return res
     133                    res = function(arg)
     134                    cache[arg] = res
     135                    return res
     136
     137            else:
     138
     139                def wrapper(arg):
     140                    with lock:
     141                        link = get(arg)
     142                        if link is not None:
     143                            root = root_full[0]
     144                            prev, next, _arg, res = link
     145                            prev[1] = next
     146                            next[0] = prev
     147                            last = root[0]
     148                            last[1] = root[0] = link
     149                            link[0] = last
     150                            link[1] = root
     151                            return res
     152                    res = function(arg)
     153                    with lock:
     154                        root, full = root_full
     155                        if arg in cache:
     156                            pass
     157                        elif full:
     158                            oldroot = root
     159                            oldroot[2] = arg
     160                            oldroot[3] = res
     161                            root = root_full[0] = oldroot[1]
     162                            oldarg = root[2]
     163                            oldres = root[3]  # keep reference
     164                            root[2] = root[3] = None
     165                            del cache[oldarg]
     166                            cache[arg] = oldroot
     167                        else:
     168                            last = root[0]
     169                            link = [last, root, arg, res]
     170                            last[1] = root[0] = cache[arg] = link
     171                            if len(cache) >= maxsize:
     172                                root_full[1] = True
     173                    return res
     174
     175            wrapper.__wrapped__ = function
     176            return update_wrapper(wrapper, function)
     177
     178        return decorator
    98179
    99180
     
    721802
    722803
    723 ### Error messages
     804### Error Messages
    724805
    725806def _db_error(msg, cls=DatabaseError):
     
    733814    """Return OperationalError."""
    734815    return _db_error(msg, OperationalError)
     816
     817
     818### Row Tuples
     819
     820# The result rows for database operations are returned as named tuples
     821# by default. Since creating namedtuple classes is a somewhat expensive
     822# operation, we cache up to 1024 of these classes by default.
     823
     824@lru_cache(maxsize=1024)
     825def _row_factory(names):
     826    """Get a namedtuple factory for row results with the given names."""
     827    try:
     828        try:
     829            return namedtuple('Row', names, rename=True)._make
     830        except TypeError:  # Python 2.6 and 3.0 do not support rename
     831            names = [v if v.isalnum() else 'column_%d' % (n,)
     832                     for n, v in enumerate(names)]
     833            return namedtuple('Row', names)._make
     834    except ValueError:  # there is still a problem with the field names
     835        names = ['column_%d' % (n,) for n in range(len(names))]
     836        return namedtuple('Row', names)._make
     837
     838
     839def set_row_factory_size(maxsize):
     840    """Change the size of the namedtuple factory cache.
     841
     842    If maxsize is set to None, the cache can grow without bound.
     843    """
     844    global _row_factory
     845    _row_factory = lru_cache(maxsize)(_row_factory.__wrapped__)
    735846
    736847
     
    13091420        different row factories whenever the column description changes.
    13101421        """
    1311         colnames = self.colnames
    1312         if colnames:
    1313             try:
    1314                 try:
    1315                     return namedtuple('Row', colnames, rename=True)._make
    1316                 except TypeError:  # Python 2.6 and 3.0 do not support rename
    1317                     colnames = [v if v.isalnum() else 'column_%d' % (n,)
    1318                              for n, v in enumerate(colnames)]
    1319                     return namedtuple('Row', colnames)._make
    1320             except ValueError:  # there is still a problem with the field names
    1321                 colnames = ['column_%d' % (n,) for n in range(len(colnames))]
    1322                 return namedtuple('Row', colnames)._make
     1422        names = self.colnames
     1423        if names:
     1424            return _row_factory(tuple(names))
     1425
    13231426
    13241427
  • trunk/tests/test_classic_connection.py

    r876 r894  
    492492            ' 0 as "A long name with Blanks"')
    493493        r = self.c.query(q).listfields()
     494        self.assertIsInstance(r, tuple)
    494495        result = ('a', 'b', 'c', 'c', 'b', 'a',
    495496            'lowercase', 'uppercase', 'mixedcase', 'MixedCase',
     
    18041805        self.assertIs(r, namedresult)
    18051806
     1807    def testSetRowFactorySize(self):
     1808        try:
     1809            from functools import lru_cache
     1810        except ImportError:  # Python < 3.2
     1811            lru_cache = None
     1812        queries = ['select 1 as a, 2 as b, 3 as c', 'select 123 as abc']
     1813        query = self.c.query
     1814        for maxsize in (None, 0, 1, 2, 3, 10, 1024):
     1815            pg.set_row_factory_size(maxsize)
     1816            for i in range(3):
     1817                for q in queries:
     1818                    r = query(q).namedresult()[0]
     1819                    if q.endswith('abc'):
     1820                        self.assertEqual(r, (123,))
     1821                        self.assertEqual(r._fields, ('abc',))
     1822                    else:
     1823                        self.assertEqual(r, (1, 2, 3))
     1824                        self.assertEqual(r._fields, ('a', 'b', 'c'))
     1825            if lru_cache:
     1826                info = pg._row_factory.cache_info()
     1827                self.assertEqual(info.maxsize, maxsize)
     1828                self.assertEqual(info.hits + info.misses, 6)
     1829                self.assertEqual(info.hits,
     1830                    0 if maxsize is not None and maxsize < 2 else 4)
     1831
    18061832
    18071833class TestStandaloneEscapeFunctions(unittest.TestCase):
  • trunk/tests/test_dbapi20.py

    r893 r894  
    10561056        self.assertEqual(row, (value, 'hello'))
    10571057
    1058 
    10591058    def test_json(self):
    10601059        inval = {"employees":
     
    12571256        self.assertEqual(row, data)
    12581257
     1258    def test_set_row_factory_size(self):
     1259        try:
     1260            from functools import lru_cache
     1261        except ImportError:  # Python < 3.2
     1262            lru_cache = None
     1263        queries = ['select 1 as a, 2 as b, 3 as c', 'select 123 as abc']
     1264        con = self._connect()
     1265        cur = con.cursor()
     1266        for maxsize in (None, 0, 1, 2, 3, 10, 1024):
     1267            pgdb.set_row_factory_size(maxsize)
     1268            for i in range(3):
     1269                for q in queries:
     1270                    cur.execute(q)
     1271                    r = cur.fetchone()
     1272                    if q.endswith('abc'):
     1273                        self.assertEqual(r, (123,))
     1274                        self.assertEqual(r._fields, ('abc',))
     1275                    else:
     1276                        self.assertEqual(r, (1, 2, 3))
     1277                        self.assertEqual(r._fields, ('a', 'b', 'c'))
     1278            if lru_cache:
     1279                info = pgdb._row_factory.cache_info()
     1280                self.assertEqual(info.maxsize, maxsize)
     1281                self.assertEqual(info.hits + info.misses, 6)
     1282                self.assertEqual(info.hits,
     1283                    0 if maxsize is not None and maxsize < 2 else 4)
     1284
    12591285    def test_memory_leaks(self):
    12601286        ids = set()
Note: See TracChangeset for help on using the changeset viewer.