views:

75

answers:

4

In handling a little SQL formatting I was amazed to find I could chain string formatters:

def get_sql(table, limit=True):
    sql = "select report_date from %s"
    if limit:
        result = "%s limit 1" % sql % table
    else:
        result = sql % table
    return result

Is this legit? Any reason not to do this?

+3  A: 

It's perfectly legit.

The "single argument" form of the string formatter is really a special case - for multiple items a tuple is normally used and that would lead to a more obvious example of why it's ok

result = "%s limit 1" % (sql % (table,),)

This ^ was originally written to encourage the questioner that supporting multiple-formats was a legitimate language feature but, as Nas Banov comments, it does read like I'm trying to explain how it works (not helped by screwing up the code). It doesn't build the string right to left like this suggests it might, but HAS to build it (be associative) left to right. The operator must take a string on the left and return one, but can take a non-string (a tuple) on the right. Since you can't use % on a pair of tuples it can't possibly work in reverse

>>> "%s %f %s" % ( "%d", 0.1, "%d %d" ) % (1,2,3)
'1 0.100000 2 3'

It may, however, lead to complicated/messy code, so personally I would use it very sparingly.

You could work your example like this:

def get_sql(table, limit=True):
    sql = "select report_date from"
    strlimit = ""
    if limit:
        strlimit = "limit 1"
    return "%s %s"%(sql, strlimit)
pycruft
`--`: Wrong explanation, this has nothing to do with multiple arguments and the associativeness is wrong. Try `"%ss limit 1" % '%' % 'BOO'` to see what i mean, this expression works, whenever if we put parenthesis as you suggest, `"%ss limit 1" % ('%' % ('BOO'))` breaks/error. Also, your proposed fix is no better than the original
Nas Banov
I think you've misunderstood. I wasn't suggesting it had anything to do with the singularity or multiplicity of the arguments, just that if you changed from using singles to tuples it would become more clear that it's a legit thing to do. Your counter example is wrong - ('%' % ('BOO')) isn't valid. `"%s limit 1" % '%s' % 'BOO'` versus `"%s limit 1" % ('%s' % ('BOO'))`. Both work. Note that question doesn't contain the `%ss` format your example needs. That isn't a 'proposed fix' - there's nothing wrong with the original code, it's just a suggested change.
pycruft
@pycruft: Seems like you don't get it. (A) Putting the parenthesis the way you did changes the **order of operations**. When you have expression of the kind `A % B % C`, it is executed as `(A % B) % C`, not as `A % (B % C)`, as your first set of parens forces it to. (B) Also, your understanding that putting () around expression makes tuple out of it is wrong - it doesn't. To force tuple you either have to explicitly use constructor `tuple()`, or use a "silly comma", like so `"%s limit 1" % (sql % (table,),)` - that will create tuples but your example will still be wrong because of (A)
Nas Banov
Yes, you're quite right. I only realised after I'm left my desk yesterday that I'd screwed up the tuples which makes what I was saying very unclear. Will fix answer. The order of the operations doesn't speak to why it is/isn't a valid language feature to support tho.
pycruft
+4  A: 

It makes sense that it works because a statement like this:

'some value goes here %s' % value

Actually returns a string. It's probably a bit more logical to view it like this:

result = ("%s limit 1" % sql) % table

There's nothing expressly wrong with doing that, but chaining operators can lead to problems with figuring out where an error came from.

So for instance, this works fine:

>>> sql = 'a value of %s'
>>> x = 'some string %s with stuff'
>>> y = 'VALUE'
>>> x % sql % y
'some string a value of VALUE with stuff'

But if there was a formatting error in there (I realize this example is pathological, but it gets the point across):

>>> sql = 'a value of %d'
>>> x = 'some string %d with stuff'
>>> y = 123    
>>> x % sql % y
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: %d format: a number is required, not str

It's really not clear which %d is causing your error. For that reason, I would split it out and just use one % formatter per line if possible because then the traceback will be able to point you to exactly which line and which formatter had the issue.

For the record, by doing it one formatter per line you'll also make life a lot easier for anyone else who has to read your code and try to figure out what's going on.

Brent Nash
A: 

I would write it this way:

def get_sql(table,limit=True):
    sql = "select report_date from %s"%table
    if limit: sql += " limit 1"
    return sql

PS. The code you posted works like this:

In [49]: "%s limit 1" % sql
Out[49]: 'select report_date from %s limit 1'

In [50]: "%s limit 1" % sql % 'table'
Out[50]: 'select report_date from table limit 1'

Sure you can do it, but I don't think it is particularly clear.

unutbu
LOL, why do you even need to use % formatting? ;-)(is much better than the original though)
Nas Banov
+2  A: 

Why yes, it is possible to chain %string formatting like that, even if this is the first time i see it used (and in horrible way, mind you)!

The reason is that operators of the same type group left to right (have "left associativity" - with the notable exceptions of exponentiation ** and comparisons a<b<c).

So in the same way that

>>> 1 - 2 - 3    # equals to (1-2)-3
-4
>>> 16 / 4 / 2   # equals to (16 /4) /2,  NOT 16 / (4 / 2)
2

so does s1 % s2 % s3 equal to (s1 % s2) % s3

Oh and by the way, it does not matter if s1, s2 and s3 are strings or numbers - the compiler does not know that during compilation and only runtime it will be determined that whether % means "remainder from division" (if s1 is number) or string formatting (if s1 is a string).

Nas Banov