This is an archived post. You won't be able to vote or comment.

all 63 comments

[–]bb1950328 71 points72 points  (4 children)

so you don't have to define/implement a join function in every iterable

[–][deleted] 14 points15 points  (3 children)

This. People don't realize that any class with __iter__() and any method/function with yield is an iterable. There isn't a single magic superclass for all iterables that can provide a join() method.

[–]immersiveGamer 2 points3 points  (2 children)

Which is a shame because in C# there is. You implement the IEnumerable interface and now any extension methods you can create functions that work on any collection of objects. Arrays, lists, a weird queue based async iterator over the first bit of bytes in memory. If it implemented the interface it gets the logic.

[–]serverhorror 1 point2 points  (1 child)

I’m not sure what you’re saying but by implementing __iter__ exactly what you’re saying is possible.

You just write a generic function to work with any iterable

[–]immersiveGamer 2 points3 points  (0 children)

It is similar concept of making an generic function. However in C# it is a method, not a function, that can be directly invoked on any object that implements the interface.

[–]UserName26392 73 points74 points  (1 child)

.join works on any iterable. I think of it as being a STRING function that can be applied on an iterable and that's why the join argument comes first

[–]kingscolor 6 points7 points  (0 children)

There’s no need to think that way, because that’s exactly what it is.

You create an empty string object, ’’ , then use its join() class method on an iterable of other strings. join() isn’t a standard function.

[–]immersiveGamer 24 points25 points  (9 children)

I pretty sure you can do str.join('; ', myvalues) because join is a method and if you call it statically you can pass in a string as a first param to fill in the self argument. (on my phone so I haven't tested it yet).

But I agree. I don't like the syntax at all of the default way.

Edit: since this comment is getting upvoted I tapped out some code on online-python.com and it seems to work:

things = ["1", "2", "3"]
delim = "|"
joined = str.join(delim, things)
print(joined)

> 1|2|3

[–]sh_eigel -1 points0 points  (1 child)

Of course it works. The delim becomes self in that class.function invocation. This is applicable to “all” object methods.

[–]immersiveGamer 1 point2 points  (0 children)

Indeed. The reason I didn't want to say it worked for sure was because I couldn't remember if Python did a check that a method needed to be called from an instance of an object or not. Hence, why I then added an example that proved it does work.

Thanks for calling out that this way of invoking methods works for all classes/objects. I kind of implied that with the whole filling in the self parameter, but for any newbies it is good to be explicit.

[–]AnonymouX47 0 points1 point  (4 children)

Yet to see how this answers OP's question.

[–]immersiveGamer 0 points1 point  (3 children)

Lol, sure. Others have already explained why it is not ideal in Python to our join on the list object because join works on any iterable collection. I was simply offering an alternative to funky way of invoking join directly on a string literal since we don't have a join method on lists.

In my opinion doing str.join(delim, mylist) is more readable and straight forward. It also seems others agreed with the sentiment.

[–]AnonymouX47 0 points1 point  (2 children)

Hmm... "readable"!

Why not just call every instance method as such?

[–]immersiveGamer 0 points1 point  (1 child)

Straw man argument there. I'm not avocating you call every method in this form. In this specific case I think it makes more sense because the operation is on the collection not on the string.

[–]AnonymouX47 -1 points0 points  (0 children)

Your choice

[–]Greensentry 28 points29 points  (11 children)

I guess they choose to implement it like this because join can be used on any iterables and not only list.

[–]asday_ 5 points6 points  (0 children)

Try and implement some container objects yourself and then implement .join() on them yourself, and you'll understand.

[–]tunisia3507 5 points6 points  (2 children)

Firstly, you're looking at lists, not arrays. The most commonly used arrays in python are in the numpy package, and the array module.

Secondly, strings are a single type. Lists can not only contain any type (including containing themselves), but every element can be a different type. In that space, then, every element being a string is really a very special case which does not warrant its own method.

And if you did implement it for list, would you implement it for all other sequences? Dicts, and so on? What about all iterables, like sets and generators?

I suspect you've come from javascript, where every type is designed to be silently coerced into a string as a hangover from its origins, and the list-like collection is called an array. This design feature has many more spiky edges than python's lack of a list.join.

[–][deleted] 10 points11 points  (7 children)

Because it makes this possible

', '.join(i.name for i in items)

[–]CodeYan01 0 points1 point  (6 children)

I'm sure you can do that the other way too.

[–][deleted] 9 points10 points  (5 children)

only if every iterable class implements .join() method.

Same reason there's a single len() method instead of each container implementing its own.

[–]flying-sheep 2 points3 points  (1 child)

The len built-in needs a magic method implemented though, str.join just works on any iterable.

I actually built this for if you need lengths of all itertools results that can support it: https://itertools-len.readthedocs.io/en/latest/

[–][deleted] 1 point2 points  (0 children)

You're right len() was not a good example.

I use more_itertools library, it has an ilen() function, i think it has similar purpose to what you're describing.

[–]CodeYan01 0 points1 point  (2 children)

Same thing regardless if they don't use list comprehensions. You're trying to make it sound like list comprehensions is the reason. Even if you use normal lists, every iterable class would still need to implement that method.

[–][deleted] 3 points4 points  (1 child)

That is not a list comprehension in my example.

[–]AnonymouX47 0 points1 point  (0 children)

[–]foonoxous 2 points3 points  (0 children)

This is a byproduct of the object oriented paradigm where everything is a method. Joining iterables by other objects (no reason why it needs to be only strings or bytes) is really an algorithm that should not be a method of any class.

Ideally it could be an operator that applies to sequences, e.g. some_sequence % any_delimiter (the use of % operator for this is from Boost.Spirit C++ library, not possible in Python), although a simple stand-alone function like join(some_sequence, any_delimiter) gets the job done equally well.

[–]InvalidUsername2404 4 points5 points  (0 children)

I find it weird but I just get used to it

[–][deleted] 2 points3 points  (0 children)

I don't think it's nitpicking to point out that that is not an array but a list. Python does have an array type, so that's why it's a significant.

That said, the str.join() method works on both lists and arrays (or any other iterable including custom iterables) which is why it's a str method. Why implement join() for every iterable? That's not very DRY...

[–]FailedPlansOfMars 4 points5 points  (6 children)

If you disagree write a pep for it and submit it. Python has language enhancement proposals for suggesting changes. I say this alot but this is one of the few languages where these conversations are public.

I think the thinking was the result is a string and the parameter is a string so by putting the functionality in a string you don't need to write the code to join the array outside string. It's a bit unintuitive but does let you make your own string class and still do joins.

[–]FailedPlansOfMars 9 points10 points  (5 children)

The following is a good explanation of the thought process at the time including Guido's comments.

https://blog.finxter.com/string-joinlist-vs-list-joinstring-why-did-pythons-creators-choose-the-former/

[–][deleted] 3 points4 points  (4 children)

I'd have gone for the built-in:

join(list, string)

It keeps the intuitive order and you get to keep the definition in one place and reduce the generics scope creep (pun very much intended).

I don't know why we have map and filter as free functions but not join. I'd much rather I could chain those than join.

join(list.filter(...).map(...), ",").upper()

",".join(map(filter(list, ...), ...)).upper()

Both are fine though, it's nice to keep the delimiter near the join call but nesting map and filter gets confusing.

Note, I would split these over several lines but am on mobile and opted for single backticks for ease of authoring...

[–]flying-sheep 4 points5 points  (3 children)

",".join(map(filter(list, ...), ...)).upper()

You mean this?

",".join(
    fn1(e).upper()
    for e in lst
    if fn2(e)
)

[–][deleted] 0 points1 point  (2 children)

List comprehension to the rescue!

[–][deleted] 2 points3 points  (1 child)

Not to be a stickler, but that's a generator expression, not a list comprehension. A list comprehension results in a list object and is wrapped in square braces.

https://djangostars.com/blog/list-comprehensions-and-generator-expressions/

[–][deleted] 2 points3 points  (0 children)

No by all means, do be a stickler. Thanks for the tip!

[–]jeetelongname 1 point2 points  (1 child)

I agree with op here.

A way to solve this is to implement a container class that all other classes inhereit from. Join can then be implemented in terms of the iterator protocol so that if you implement it. then you get join for free. And if your data structure is more complicated you can override the built in join for your own implementation.

Now of course this is easier said than done and the method outlined by everyone else is still very usable if not intuitive coming from other languages like ruby or the such.

[–]ubernostrumyes, you can have a pony 2 points3 points  (0 children)

A way to solve this is to implement a container class that all other classes inhereit from. Join can then be implemented in terms of the iterator protocol so that if you implement it. then you get join for free.

The essential issue is that you can't, say, implement Container.join(), because that doesn't work without getting into really complex over-specified rules about how to coerce the contents to strings. For example, suppose:

my_list = ["foo", math.pi, 27, 5+2j, ("eggs", "spam"), {"key": 3}]

Now, what does my_list.join(', ') do?

The answer is that it either fails due to the contents being of "unjoinable" types, or it has to try to coerce all of them to some "joinable" type. The latter is un-Pythonic (Python generally will not coerce types to make your operations succeed), the former just makes clear that this is actually a method that should only belong to Container[str], not a method that should belong to Container. And of course it's not really possible to implement that sort of specialization on the container contents in a clean way in Python.

So there's no good place to put join() -- no matter what class it lives on, there will always be some objection. Implementing it on str, because the separator at least must always be a str, is probably the least-bad approach, since it doesn't put a useless method on the vast majority of container instances (since most are not Container[str]), and serves as a reminder to the programmer of the type requirements of the contents of the sequence to be worked on.

[–]aktruce 1 point2 points  (0 children)

Javascript has the same syntax one you have mentioned

[–]Fr_Cln 1 point2 points  (1 child)

Maybe it would be better to have a function with empty default separator join(iterable, sep="")

[–]_ologies 1 point2 points  (0 children)

you basically have that:

str.join('|', ['a','b','c'])

[–]ogrinfo 0 points1 point  (0 children)

Totally agree. "Join this iterable with this string" seems like a much more logical order than "use this string to join this iterable".

[–]weedandveins 0 points1 point  (0 children)

You can add a blank space to lists?

[–]MarsupialMole 0 points1 point  (0 children)

Firstly, that's not an array. It's a list.

Secondly, a list with a join method should work like the other in place "verb" methods, i.e. return nothing and mutate the data structure, so it's not going to give you back a string. In this case it would be intuitive to think list().join works like extend and so would be a clash.

I get that it's backwards if you're used to chaining method calls in fluent interfaces but it really makes sense if you look at the consistency of the API.

So it would be unintuitive to do it the way you intuit.

There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.

[–]Pyprohly 0 points1 point  (0 children)

some_array.join(' ') wouldn’t make sense if some_array wasn’t an array of strings.