An earlier version was like that.
I had a class which would absorb criteria via Python expressions, so:
e = logic.Expression()\ne.attribute == 3
would trigger:
class Expression(object):\n def __eq__(self, other):\n stack.append(operator.eq, other)
Etcetera. Then I would pass around the stack and evaluate it as necessary. But I also (like Criteria) had the issue of not being able to override shortcut And and Or (I used binary & and | instead), and it got a bit ugly. In addition, I was abusing normal syntax--as above, line 2, most manipulations took a comparison and didn't return or assign anything; it wasn't very natural.
So, after seeing a brilliant post by Raymond Hettinger on c.l.p., I wrote two things: an EarlyBinder and a LambdaDecompiler class. Passing a lambda (a Python anonymous function) into an Expression constructor now does early binding via a bytecode Visitor, grabbing globals and cell objects from the closure and dereferencing them (making them constants), then making a new function object. At that point, I'm free to pass it around, even make a string out of it and store it in a DB, because all variables are either arguments to the function, or constants. Flexibility of function is provided through a single mechanism; you can write:
lambda x, **kw: x.Size > kw['Size']
..where kw is keyword arguments--a dictionary (mapping; hash to you Perlers).
The LambdaDecompiler also is a bytecode Visitor, and produces the Python source code from bytecode--that's how I get a string to store. But, having that decompiler core, I subclass it and write SQLDecompilers as well. So the above decompiles into SQL Server (ADO) as:
"WHERE [Size] > %(Size)s" % kwargs
...which, when evaluated with
kwargs = {'Size': 3}, yields
WHERE [Size] > 3. This gets me the speed boost you mentioned (avoiding 'SQL fragments').
But it was also important for me to have an expression which I could then pass a fully-formed class and test it, because:
1) I have an in-memory object cache, and I need to filter it by the same logic, and
2) The match between Python logic and SQL is imperfect. For example, == is case-sensitive in Python but -insensitive in ADO SQL. It's fastest just to create the class object and run it through the lambda than try to wrangle SQL Server into making the search case-sensitive, which is set database-wide, per column, *and* per query IIRC.
The only ugly bit now is that it's CPython-specific, it won't work on Jython (Python which runs on the JVM), because the bytecode is different. Meh. One thing I really like is that the application code (what a developer using my framework would write) doesn't mention SQL at all. So you can use exactly the same code for huge DB tables, or 10-item in-memory lists, and leave those decisions about the best storage mechanism up to the deployers. They can decide that a list of 10 small items should stay in memory, and the huge table shouldn't. Or vice-versa.