20

Understand "better" as a quicker, elegant and readable.

I have two strings (a and b) that could be null or not. And I want concatenate them separated by a hyphen only if both are not null:

a - b

a (if b is null)

b (where a is null)

Shankar Cabus
  • 9,302
  • 7
  • 33
  • 43
  • 8
    What do you want if they are both null? – unholysampler Mar 12 '13 at 12:21
  • Ok, this I know. But there are many ways to do this (as you can see in the answers below). – Shankar Cabus Mar 12 '13 at 12:38
  • What a downvotefest (on both Q and A) for a pretty straightforward question :/ – Junuxx Mar 12 '13 at 12:40
  • 5
    @ShankarCabus: But all of these implementations do something when they are both null, and not all of them do the same thing. If you want an "elegant" way to do this operation, the elegance should cover all the cases and not require an extra if block before it. – unholysampler Mar 12 '13 at 12:40
  • 1
    (a+'-'+b).strip('-') ...but it fails miserably if a or b =='-' – andsoa Mar 12 '13 at 13:20
  • 1
    @andsoa -- It also "fails miserably" if a and b are anything but ''. The end goal was to have a '-' between a and b, and you're stripping that out. – Hoopdady Mar 12 '13 at 18:23
  • 2
    @Hoopdady: Incorrect, string stripping functions almost universally only apply to leading and trailing characters, otherwise it would just be a .replace('-', '') – Phoshi Mar 12 '13 at 22:28
  • @Phoshi -- Good point. Forgot about that. – Hoopdady Mar 13 '13 at 03:11

10 Answers10

47
# Concatenates a and b with ' - ' or Coalesces them if one is None
'-'.join([x for x in (a,b) if x])

Edit
Here are the results of this algorithm (Note that None will work the same as ''):

>>> '-'.join([x for x in ('foo','bar') if x])
'foo-bar'
>>> '-'.join([x for x in ('foo','') if x])
'foo'
>>> '-'.join([x for x in ('','bar') if x])
'bar'
>>> '-'.join([x for x in ('','') if x])
''

*Also note that Rafael's assessment, in his post below, only showed a difference of .0002 secs over a 1000 iterations of the filter method, it can be reasoned that such a small difference can be due to inconsistencies in available system resources at the time of running the script. I ran his timeit implementation over several iteration and found that either algorithm will be faster about 50% of the time, neither by a wide margin. Thus showing they are basically equivalent.

BenMorel
  • 34,448
  • 50
  • 182
  • 322
Hoopdady
  • 2,296
  • 3
  • 25
  • 40
  • 1
    Wow, someone has some very strong opinions. – Hoopdady Mar 12 '13 at 12:26
  • 5
    as I said, sorry for the strong words, but I've encountered a lot of similar *uncommented* code in large projects, where you spend plenty of time figuring out 'what the heck is going here'? – Zaur Nasibov Mar 12 '13 at 12:28
  • 8
    Well I would agree that a CS101 student would probably not get this, but this isn't a difficult concept for those a little ways into python. I happen to think its pretty elegant, though not necessarily the most efficient. But that's the beauty of SO, the people will decide. :-) – Hoopdady Mar 12 '13 at 12:31
  • 5
    @Hoopdady: I agree with BasicWolf. Given the context of this question, I can follow what is happening. But if it just shows up on one line in a file, I would have to think before I knew what it did. When I see BasicWolf's answer, I know what it is doing immediately. – unholysampler Mar 12 '13 at 12:35
  • 3
    @BasicWolf -- As far as I see it, this is one of the most simple ways to handle the "only if both are not null" criteria (although, I would use a `tuple` in the inner list `(a,b)` rather than `[a,b]`) – mgilson Mar 12 '13 at 12:35
  • 2
    @mgilson, you see, there is also a context of `join()` here, which I am against: when I personally see `join` I expect either a bigger-than-two or variable(!) amount of items to be joined. I would never use `join()` to join two strings explicitly. – Zaur Nasibov Mar 12 '13 at 12:38
  • 4
    @Hoopdady -- I'd also remove the parens around `'-'`. It's pretty idiomatic to leave them off -- In fact, I've seen it written with parenthesis so rarely that the parenthesis actually made me take a second look at this. – mgilson Mar 12 '13 at 12:38
  • 4
    @mgilson You're pretty idiomatic... fine I'll do it. Done. – Hoopdady Mar 12 '13 at 12:40
  • 3
    @Hoopdady you can also leave out the list comprehension, and pass a generator expression into `join` directly: `'-'.join(x for x in (a,b) if x)`. This should run (slightly) faster and use (slightly) less memory, because you're not building an intermediate list and then throwing it away. It also looks nice on the page. – Benjamin Hodgson Mar 12 '13 at 15:22
  • @poorsod I tried it, and it actually ran slower... Not sure why. – Hoopdady Mar 12 '13 at 16:07
  • @Hoopdady - By George, you're right. It was about three times slower for me. I wonder why that is. – Benjamin Hodgson Mar 12 '13 at 16:37
  • Maybe because join takes a list which is given natively by list-comp, but without it, its a generator, and there is some overhead in converting it to a list. – Hoopdady Mar 12 '13 at 17:24
  • 2
    @Hoopdady Correct, I just ran across an explanation a few days ago: `join` goes over the list twice because it pre-calculates the memory required before actually doing the join. – Izkata Mar 12 '13 at 17:50
  • 1) Talk about nit-picking, I'd write `for x in [a, b]`. The line that separates tuples from lists in Python is a bit blurry (because tuples take so many methods you'd expect only in lists), but conceptually this makes more sense as a "list of strings" than a "tuple of strings". 2) It's been explained a zillion times why "join" is a method of string and not from lists/iterables/whatever, but yet, it's a pitty we can't write things like `strings.compact.join("-")` like they do in, well, other languages. – tokland Mar 12 '13 at 17:58
  • @tokland -- Ha, well you and mgilson can fight that one out. – Hoopdady Mar 12 '13 at 18:06
36

How about something simple like:

# if I always need a string even when `a` and `b` are both null,
# I would set `output` to a default beforehand.
# Or actually, as Supr points out, simply do `a or b or 'default'`
if a and b:
    output = '%s - %s' % (a, b)
else:
    output = a or b

Edit: Lots of interesting solutions in this thread. I chose this solution because I was emphasizing readability and quickness, at least in terms of implementation. It's not the most scalable or interesting solution, but for this scope it works, and lets me move on to the next problem very quickly.

toxotes
  • 1,386
  • 14
  • 20
  • 9
    +1 for simplicity - not the simplicity of the code necessarily, but of the mapping between human understanding and code. Yeah, I guess that's about the same as readability (and maintainability, reliability, testability). – LarsH Mar 12 '13 at 15:25
  • 4
    +1, I'd prefer seeing this in production code, as it states clearly the intent – Matsemann Mar 12 '13 at 16:49
  • 2
    I don't know Python very well, but couldn't the null/null case be handled by changing the last line to `output = a or b or 'default'` instead of setting it beforehand? – Supr Mar 13 '13 at 08:57
  • @Supr, you're right, that does work and is a much slicker way to handle it. – toxotes Mar 13 '13 at 11:39
31

Wow, seems like a hot question :p My proposal:

' - '.join(filter(bool, (a, b)))

Which gives:

>>> ' - '.join(filter(bool, ('', '')))
''
>>> ' - '.join(filter(bool, ('1', '')))
'1'
>>> ' - '.join(filter(bool, ('1', '2')))
'1 - 2'
>>> ' - '.join(filter(bool, ('', '2')))
'2'

Obviously, None behaves like '' with this code.

icecrime
  • 74,451
  • 13
  • 99
  • 111
  • 4
    Great solution! The most elegant for now. – Shankar Cabus Mar 12 '13 at 12:50
  • 4
    `filter(None,(a,b))` would also work, but `bool` is probably a little more explicit. For the record, this really isn't any different than the version above which uses a list-comp instead. – mgilson Mar 12 '13 at 12:53
  • 1
    Using `bool` I get a *slight* decrease in performance. Probably because passing `None` allows to call the truth methods more directly, while passing `bool` the `filter` has to do a normal function call. – Bakuriu Mar 12 '13 at 16:29
12

Here is one option:

("%s - %s" if (a and b) else "%s%s") % (a,b)

EDIT: As pointed by mgilson, this code would fail on with None's a better way (but less readable one) would be:

"%s - %s" % (a,b) if (a and b) else (a or b)
zenpoy
  • 19,490
  • 9
  • 60
  • 87
  • Note that you pick up the `a = None` case if you re-write it slightly: `"%s - %s"%(a,b) if (a and b) else (a or b)` -- But that's harder to read I would say. – mgilson Mar 12 '13 at 13:27
  • `" - ".join((a, b)) if a and b else a or b` – Buddy Mar 12 '13 at 21:39
4

I just wanted to offer toxotes' solution rewritten as a one liner using format.

output = "{0} - {1}".format(a, b) if (a and b) else (a or b)
Carl Smith
  • 3,025
  • 24
  • 36
3

There's a lot of answers here :)

The two best answers (performance and clean code in one line) are the answers of @icecrime and @Hoopdady

Both asnwers results equally, the only difference is performance.

cases = [
 (None, 'testB'),
 ('', 'testB'),
 ('testA', 'testB'),
 ('testA', ''),
 ('testA', None),
 (None, None)
]

for case in cases: print '-'.join(filter(bool, case))
'testB'
'testB'
'testA-testB'
'testA'
'testA'

for case in cases: print '-'.join([x for x in case if x])
'testB'
'testB'
'testA-testB'
'testA'
'testA'

So let's do a benchmark :)

import timeit

setup = '''
cases = [
  (None, "testB"),
  ("", "testB"),
  ("testA","testB"),
  ("testA", ""),
  ("testA", None),
  (None, None)
]
'''

print min(timeit.Timer(
  "for case in cases: '-'.join([x for x in case if x])", setup=setup
).repeat(5, 1000))
0.00171494483948

print min(timeit.Timer(
  "for case in cases: '-'.join(filter(bool, case))", setup=setup
).repeat(5, 1000))
0.00283288955688

But, as @mgilson said, using None instead of bool as the function in filter produces the same result and have a quite better performance:

print min(timeit.Timer(
  "for case in cases: '-'.join(filter(None, case))", setup=setup
).repeat(5, 1000))
0.00154685974121

So, the best result is the answer gave by @icecrime with the suggestion from @mgilson:

'-'.join(filter(None, (a,b)))

The performance difference is in milliseconds per 1000 iterations (microseconds per iteration). So these two methods have a quite equals performance, and, for almost any project you could choose any one; In case your project must have a better performance, considering microseconds, you could follow this benchmark :)

Rafael Verger
  • 1,751
  • 13
  • 18
  • 1
    Is there something I'm missing. I've tried to copy and paste your code and it errors everytime – Hoopdady Mar 12 '13 at 16:01
  • Also when I run my own time test, my way is slightly faster. – Hoopdady Mar 12 '13 at 16:14
  • @Hoopdady, thanks! Something goes wrong with identations in `setup` declaration and I've missed a `)` in your code, fixed now! – Rafael Verger Mar 12 '13 at 19:00
  • 1
    In copying your code and running it. I've found that my solution vs Icecrime's solutions are pretty much identical in time. Sometimes when I run your test, mine beats his, and sometimes his beats mine, it really depends on what all is going on in the system at the time. – Hoopdady Mar 12 '13 at 20:02
  • It always depends :) I just run my code a few times and in all results, the answers of @icecrime + suggestion of mgilson gave me a better performance compared to yours, Hoopdady. But the difference is almost nothing, it is in microseconds precision – Rafael Verger Mar 12 '13 at 23:39
  • 1
    I'm just saying that if you run it again, it may not give you better performance. Its such a small difference you have to consider inconsistencies in your available system resources at the time of running the script. I found that to be the case when I ran your script myself. Sometimes mine was faster, sometimes the filter implementation was faster. – Hoopdady Mar 13 '13 at 12:09
1

Do it like this: '-'.join(max(x,'') for x in [a,b] if x is not None)

0

Try this:

def myfunc(a,b):
    if not b:
        return a
    elif not a:
        return b
    else:
        return a+' - '+b

Or

def myfunc(a,b):
    if a and b:
        return a+' - '+b
    else:
        return a or b
pradyunsg
  • 18,287
  • 11
  • 43
  • 96
  • 1
    You don't really need an `else` if the preceding `if` will return from the function. I often wondered if it should be included for readability, but decided against it. It'd be nice if we could do `return a if b else c`. – Carl Smith Mar 12 '13 at 14:33
  • 4
    @Carl: Surely you _can_ do `return a if b else c` – Eric Mar 12 '13 at 18:11
  • @Eric: Sweet. I could've sworn I tried that once and got a syntax error, but I just tried it and it works fine. Thanks. That's such a nice feature. – Carl Smith Mar 12 '13 at 18:19
  • 1
    @carl: All you're seeing is `return (a if b else c)` – Eric Mar 12 '13 at 18:23
  • Makes perfect sense. I could've sworn it didn't work for me when I tried it. It must have been something else. +1 – Carl Smith Mar 12 '13 at 18:30
0

Something pythonian, readable and elegant:

strings = string1, string2

'{0}{1}{2}'.format(
    # output first string if it's not empty
    strings[0] if strings[0] else '',

    # join with hyphen if both strings are not empty    
    '-' if all(strings) else '',

    # output second string if it's not empty
    strings[1] if strings[1] else ''
    )

And fast too ;)

catalesia
  • 3,348
  • 2
  • 13
  • 9
0

I would do it like this:

def show_together(item1=None, item2=None, seperator='-'):
    return '%s%s%s' % (item1,seperator,item2) if item1 and item2 else item1 or item2



>>> show_together(1,1)
'1-1'

>>> show_together(1)
1

>>> show_together()
>>> show_together(4,4,'$')
'4$4'
YardenST
  • 5,119
  • 2
  • 33
  • 54