What better way to concatenate string in python?
Asked Answered
W

10

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)

Willson answered 12/3, 2013 at 12:16 Comment(8)
What do you want if they are both null?Falk
Ok, this I know. But there are many ways to do this (as you can see in the answers below).Willson
What a downvotefest (on both Q and A) for a pretty straightforward question :/Hydrochloride
@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.Falk
(a+'-'+b).strip('-') ...but it fails miserably if a or b =='-'Feoff
@Feoff -- 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.Explosion
@Hoopdady: Incorrect, string stripping functions almost universally only apply to leading and trailing characters, otherwise it would just be a .replace('-', '')Tybalt
@Tybalt -- Good point. Forgot about that.Explosion
E
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.

Explosion answered 12/3, 2013 at 12:21 Comment(15)
Wow, someone has some very strong opinions.Explosion
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'?Kilometer
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. :-)Explosion
@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.Falk
@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])Catchweight
@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.Kilometer
@Explosion -- 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.Catchweight
@Catchweight You're pretty idiomatic... fine I'll do it. Done.Explosion
@Explosion 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.Ravage
@poorsod I tried it, and it actually ran slower... Not sure why.Explosion
@Explosion - By George, you're right. It was about three times slower for me. I wonder why that is.Ravage
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.Explosion
@Explosion 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.Phore
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.Dementia
@Dementia -- Ha, well you and mgilson can fight that one out.Explosion
R
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.

Recommendatory answered 12/3, 2013 at 12:21 Comment(4)
+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).Nelrsa
+1, I'd prefer seeing this in production code, as it states clearly the intentShawanda
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?Lindsley
@Supr, you're right, that does work and is a much slicker way to handle it.Recommendatory
N
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.

Neale answered 12/3, 2013 at 12:45 Comment(3)
Great solution! The most elegant for now.Willson
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.Catchweight
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.Coursing
C
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)
Claw answered 12/3, 2013 at 12:19 Comment(2)
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.Catchweight
" - ".join((a, b)) if a and b else a or bAnechoic
A
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)
Alleviation answered 12/3, 2013 at 18:28 Comment(0)
E
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 :)

Eberhard answered 12/3, 2013 at 13:42 Comment(6)
Is there something I'm missing. I've tried to copy and paste your code and it errors everytimeExplosion
Also when I run my own time test, my way is slightly faster.Explosion
@Hoopdady, thanks! Something goes wrong with identations in setup declaration and I've missed a ) in your code, fixed now!Eberhard
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.Explosion
It always depends :) I just run my code a few times and in all results, the answers of @Neale + suggestion of mgilson gave me a better performance compared to yours, Hoopdady. But the difference is almost nothing, it is in microseconds precisionEberhard
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.Explosion
S
1

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

Slowly answered 14/4, 2020 at 10:14 Comment(0)
L
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
Landlord answered 12/3, 2013 at 12:21 Comment(5)
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.Alleviation
@Carl: Surely you can do return a if b else cInformal
@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.Alleviation
@carl: All you're seeing is return (a if b else c)Informal
Makes perfect sense. I could've sworn it didn't work for me when I tried it. It must have been something else. +1Alleviation
S
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 ;)

Siderostat answered 12/3, 2013 at 16:28 Comment(0)
T
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'
Tundra answered 12/3, 2013 at 16:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.