SonOfRoyHodgson
SonOfRoyHodgson

Reputation: 7

Need a little clarification of string join() method

I'm learning python from the basics and i need a little help regarding a practice example from my study book. Original task is to replace the string "Don't panic!" with "on tap" using slices.

So, this is my original code, which didn't work:

phrase = "Don't panic!"
plist = list(phrase)
print(phrase)
print(plist)
new_phrase = ''.join([plist[1:3], plist[5], plist[4], plist[7], plist[6]])

With the int-r mistake:

TypeError: sequence item 0: expected str instance, list found

The correct answer from the book is:

new_phrase = ''.join(plist[1:3])
new_phrase = new_phrase + ''.join([plist[5], plist[4], plist[7], plist[6]])

And that's what i don't understand, what's the difference between my wrong line and the right one? As i look at this, both sequences have string instances?

Upvotes: -1

Views: 721

Answers (5)

Martijn Pieters
Martijn Pieters

Reputation: 1121744

You are mixing lists and strings inside another list by mixing slicing and indexing. Slicing produces a list, indexing an individual element.

So the working code only puts individual values into a list:

[plist[5], plist[4], plist[7], plist[6]]

while your code also uses slicing:

[plist[1:3], plist[5], plist[4], plist[7], plist[6]]
#     ^^^^^

so now you the first element is a list with 2 elements.

str.join() can only join strings, so all elements must be strings, any other object types (such as a list) lead to errors.

The code in the book works around this by joining the slice result separately:

new_phrase = ''.join(plist[1:3])

This takes the sliced list, passing that to str.join() directly (not as an element of another list). The code in the book then appends the result of a second str.join() call to that on the next line:

new_phrase = new_phrase + ''.join([plist[5], plist[4], plist[7], plist[6]])

Modern Python (3.5 or newer) does have syntax to move elements from a list into a list you are creating with the [...] syntax, by prepending the item with *; that causes all the elements to be taken out and placed in the current list. You can use that here too to take all the sliced strings out of the slice and into the new list:

''.join([*plist[1:3], plist[5], plist[4], plist[7], plist[6]])
#        ^ note the * here, meaning: take all elements from plist[1:3], not plist[1:3] itself.

It'll depend on the age of the book you are using whether or not it'll introduce this syntax.

It's always helpful to look at what is happening with the component elements of Python expressions, in the interactive interpreter. Here is what is happening for your code:

>>> phrase = "Don't panic!"
>>> plist = list(phrase)
>>> plist
['D', 'o', 'n', "'", 't', ' ', 'p', 'a', 'n', 'i', 'c', '!']
>>> [plist[1:3], plist[5], plist[4], plist[7], plist[6]]
[['o', 'n'], ' ', 't', 'a', 'p']

See that ['o', 'n'] element at the start there? That's not a single string value, so str.join() won't accept it:

>>> ''.join([plist[1:3], plist[5], plist[4], plist[7], plist[6]])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: sequence item 0: expected str instance, list found

but if you ignored that part the remainder can be joined:

>>> ''.join([plist[5], plist[4], plist[7], plist[6]])
' tap'

If we use the * syntax, you get a flat list, and ''.join(...) accepts that:

>>> ''.join([*plist[1:3], plist[5], plist[4], plist[7], plist[6]])
'on tap'

Upvotes: 4

Rafael Marques
Rafael Marques

Reputation: 1872

As Madhan pointed at his response, you are trying to use a list as the first index, not a string.

Taken from pydocs:

str.join(iterable) Return a string which is the concatenation of the strings in iterable. A TypeError will be raised if there are any non-string values in iterable, including bytes objects. The separator between elements is the string providing this method.

So you need to provide only strings in your iterable. But if you see your code, you are using a list of multiple type of objects.

The first index is a list, and other indexes are strings.

Here is what is going on:

your_param = [plist[1:3], plist[5], plist[4], plist[7], plist[6]]

type(your_param)
<class 'list'>

for param in your_param:
    type(param)

<class 'list'> # <--- HERE
<class 'str'>
<class 'str'>
<class 'str'>
<class 'str'>

If you want everything in one line, you can do something like:

your_param = [''.join(plist[1:3]), plist[5], plist[4], plist[7], plist[6]]

for param in your_param:
    type(param)

<class 'str'>
<class 'str'>
<class 'str'>
<class 'str'>
<class 'str'>

Upvotes: 0

David Culbreth
David Culbreth

Reputation: 2776

You were so close with your original implementation. I don't know if your book has introduced the * (star) notation yet, but you can pass each item in an iterable with *my_iter

like so: my_function(*my_iter)

or just put it into a tuple: (val_a, val_b, val_c, *val_iter)

quick example:

my_list = [1,2,3,4]
my_tuple = (5,6,7,8)

combined_list = [*my_list, *my_tuple]
print(combined_list)

[1, 2, 3, 4, 5, 6, 7, 8]

Now, back to your question: Using *-notation, we can easily solve your issue with the star so that your list of strings is passed, element by element, into the ''.join():

phrase = "Don't panic!"
plist = list(phrase)
new_phrase = ''.join([*plist[1:3], plist[5], plist[4], plist[7], plist[6]])
print(new_phrase)

on tap

Upvotes: 0

Daniel Pryden
Daniel Pryden

Reputation: 60957

You construct plist using

plist = list(phrase)

This means that plist is a list of str instances, one for each character in the original phrase string:

>>> plist
['D', 'o', 'n', "'", 't', ' ', 'p', 'a', 'n', 'i', 'c', '!']

Slicing a list yields another list, so plist[1:3] is a list:

>>> type(plist)
<class 'list'>
>>> type(plist[0])
<class 'str'>
>>> type(plist[0:1])
<class 'list'>

So the argument you're passing into join contains both str objects and a list object (the list created by slicing plist):

>>> [plist[1:3], plist[5], plist[4], plist[7], plist[6]]
[['o', 'n'], ' ', 't', 'a', 'p']

>>> [type(x) for x in [plist[1:3], plist[5], plist[4], plist[7], plist[6]]]
[<class 'list'>, <class 'str'>, <class 'str'>, <class 'str'>, <class 'str'>]

It doesn't matter that that inner list itself contains str objects, since join doesn't inspect into that list object. As soon as it sees something inside the iterable you passed it that is not a str, it raises the TypeError you see.

Upvotes: 0

Alok Dixit
Alok Dixit

Reputation: 9

Let's discuss the correct answer first. Here you are joining plist[1:3] and making a string object then storing it into new_phrase. Keep in mind this is now a string. Then you are adding this string to another string (which is being made by another join function), hence no error as you can add a string to string.

Coming back to your solution, first of all, plist[1:3] is a list and not a string, so you cannot use it inside the join function as it expects a list and not a list of list.

So you can make plist[1:3] to string

new_phrase = ''.join([''.join(plist[1:3]), plist[5], plist[4], plist[7], plist[6]])

which is equivalent to the correct code as you can see.

Upvotes: 0

Related Questions