LW KeyedTuples have instance dicts (ineffective __slots__)

Issue #3420 resolved
Sebastian Bank created an issue
import sqlalchemy
import sqlalchemy.orm

engine = sqlalchemy.create_engine('sqlite://')
session = sqlalchemy.orm.Session(engine)
result = session.query(sqlalchemy.func.now())[0]

result.__dict__  # should raise AttributeError

print('\n'.join('%s\t%s' % (getattr(cls, '__slots__', None), cls)
    for cls in result.__class__.mro()))
# ()    <class 'sqlalchemy.util._collections.result'>
# ()    <class 'sqlalchemy.util._collections._LW'>
# None  <class 'sqlalchemy.util._collections.AbstractKeyedTuple'>
# None  <type 'tuple'>
# None  <type 'object'>

Note that __slots__ only works if every class along the mro spares instance __dict__s. __slots__ = () for sqlalchemy.util._collections.AbstractKeyedTuple shoud fix this.

Comments (10)

  1. Mike Bayer repo owner
    • The "lightweight named tuple" used when a :class:.Query returns rows failed to implement __slots__ correctly such that it still had a __dict__. This is resolved, but in the extremely unlikely case someone was assigning values to the returned tuples, that will no longer work. fixes #3420

    → <<cset 64c1f2e56888>>

  2. Sebastian Bank reporter

    Uh, you really know your users damn well!

    I think Python even raises with new attributes (with and without the change). I was curious if the change affects the benchmarks from the docs (maybe for some that would count as argument for/against __slots__).

  3. Mike Bayer repo owner

    __slots__ doesn't have a huge impact on speed, it is very useful for keeping memory under control though. The amount of memory that was being wasted throughout huge parts of SQLA on dictionaries that were hardly needed is why I put a lot of __slots__ in 1.0.

  4. Rodrigo Menezes

    Awww mannnn. You caught me :)

    I assigned some helper lambdas to the returning rows:

    class MyQuery(Query):
        def __iter__(self):
            return (self.decorate(r) for r in Query.__iter__(q) )
    
        handy_lambda_1 = lambda r: stuff
        handy_lambda_2 = lambda r: more_stuff
    
        def decorate(self, row):
            row.handy_lambda_1 = handy_lambda_1
            row.handy_lambda_2 = handy_lambda_2
            return row
    
    MySession = scoped_session(
        sessionmaker(
            extension=ZopeTransactionExtension(),
            query_cls=MyQuery
        )
    )
    

    Any chance we'll get row_cls as an option to sessionmaker? I found these helper lambdas super useful in certain scenarios.

  5. Log in to comment