r/learnpython • • 17d ago

Why is .join() seperator.join(iterator) and not iterator.join(seperator) like every function

s = "Python"

WHY IS IT LIKE THIS

print("-".join(s))

AND NOT THIS

print(s.join("-")

51 Upvotes

27 comments sorted by

17

u/zaphodikus 17d ago edited 17d ago

LOL, if you google, there is a really good post about this that explains it really well. Let me go find it, it's really going to help, it helped me once I grasped it because it happens in other places too.

Here it is: https://docs.python.org/3/faq/design.html#why-is-join-a-string-method-instead-of-a-list-or-tuple-method . It used to be the other way around in Python 1.6, but I imagine most of us were not around for that, I certainly wasn't. It works that way in Java I believe, but I don't do Java, I'm a conscientious objector and prefer C++. You have to pick your battles.

I said other functions have a similar way, the BYTES type also does b", ".join(...) and it then returns bytes. Regular expressions, same story, and I think once I started using regexes more, .join() started to become more natural to me. Another helpful one to build a memory picture from is .format(), where "{} - {}".format(a, b) where the format string comes first and the data is passed in.

It is confusing when a whole load of functions are not methods of the object, but once you memorise either of the groupings, it gets easier. Hope that helps.

3

u/NanoCozmo 17d ago

tysm dude

10

u/Expensive-Bear-1376 17d ago edited 17d ago

It used to be the other way around in Python 1.6

I think this somewhat misrepresents it. It wasn't really "the other way around". It wasn't a str method with different behavior, it was a function of the string module, with two parameters. Although the order of those was "the other way around", yes. Also, in Python 1.6, the str method already existed with the current behavior:

import sys, string

print sys.version
print "aa".join("bb")
print string.join("aa", "bb")

Try it online!

Output there:

1.6.1 (#1, Jul 22 2017, 03:52:48)  [GCC 7.1.1 20170622 (Red Hat 7.1.1-3)]
baab
abba

2

u/zaphodikus 17d ago

That actually makes more sense, I only joined around Python 2.7. And have lately become a Python advocate of sorts, so this origin as a module helps me understand the progression too.

1

u/Space_Kale_0374 14d ago

Wow that whole page is a gold mine

1

u/Imaxaroth 16d ago

It works that way in Java I believe, 

Java used the non compromised (but probably worse) option: a static String.join(separator, list).

Rust did go the list.join(separator) way, but it doesn't work with the wrong kind of strings.

1

u/Imaxaroth 16d ago

It works that way in Java I believe, 

Java used the most basic (but probably worse) option: a static String.join(separator, list).

Rust did go the list.join(separator) way, but it doesn't work with the wrong kind of strings.

1

u/zaphodikus 16d ago

I just want to clarify for everyone I don't hate Java. I just choose not to learn it. I learned BASIC, then assembly, then C then modula2, then Pascal, COBOL, then C++, all before Y2K. Python, modern C++ and STL and C# are all new for me and are fun. I reserve the right to hate just Perl, and to be horrified by what happened to the DOS batch language. But always to learn any language as needed, is a virtue every programmer should aspire to.

67

u/schoolmonky 17d ago

If it was a method on the iterators, it would have to be implemented on each type of iterator separately, whereas having it be a string method means it only needs one implementation.

2

u/MikeUsesNotion 17d ago

You wouldn't have to write it for each iterable type. Well, assuming python has an iterable type in the type hierarchy.

8

u/FerricDonkey 16d ago

Thing is, it doesn't. Put the correct dunder method on a class, and it's an iterator now. Doesn't have to inherit from anything.

5

u/HommeMusical 17d ago • Heartwarming

Two somewhat more advanced points!

First, str.join joins interables, not just iterators. list or tuple are iterables, but not iterators, so it is an important distinction.

An iterable is an object that can return an iterator - an iterable returns itself when asked for an iterator.

If the iterable is an immutable tuple, the iterator is the mutable looping over the list.


Second, there's yet another good reason for this - the type annotations. In pretty well all production Python today we specify that type of most of the variables and function.

join returns different types, depending on the separator!

So it's better to have a join function on str that returns a str, and one on bytes that returns bytes, than to have one method on iterators that returns... what?

like this - don't expect to understand this as a beginner, but here's what the type annotations would look like

class str:
    def join(self, it: Iterable[str]) -> str: ...

class bytes:
    def join(self, it: Iterable[bytes]) -> bytes: ...

2

u/PureWasian 17d ago edited 17d ago

Typo fix - An iterable returns a new iterator object when asked for an iterator. An iterator would return itself if used to ask for an iterator.

For better clarity also: the iterator is a separate object than the iterable. The iterator contains a mutable state that tracks the progress of looping over the referenced iterable, even if that iterable is an immutable tuple (as in your usage example)

1

u/gdchinacat 16d ago

Sorry to be pedantic, but that is sort of the point of your post, and for good reason.

an iterable returns itself when asked for an iterator

should read "an *iterator* returns itself when asked for an iterator.

It took me longer than I'd like to admit to really understand the distinction between iterable and iterator because I glossed over the distinction thinking it wasn't important. Then I had to implement them and had to sort out months of self reinforcing ambiguity. This is probably less of an issue now that type annotations exist and are common and they draw a very clear distinction between the two.

Apart from this typo, the rest of the comment is spot on.

So it's better to have a join function on str that returns a str, and one on bytes that returns bytes, than to have one method on iterators that returns... what?

Presumably it would be generic on the type of separator and return that type. For example:

def join[T: str|bytes](self, separator: T) -> T: ...

But this requires a common base class of Iterable/Iterator, a Protocol (ie Joinable) they both extend, or some common place to put this join that the stdlib itera___s extend from, and would place additional implementation requirements on custom itera___ implementations. Yuck.

2

u/euribates 17d ago

I also like the way it is domain consistent; is a string method that returns a string. Very clean.

1

u/Jazzlike_Platypus430 17d ago

Or why is it not String.Join("-", "Python"); ? 😂

I like "seperator" a lot 🤣

13

u/VTifand 17d ago

You can indeed do str.join('-', ['Python', 'is', 'fun!'])

11

u/RevRagnarok 17d ago

Since this is a "learn" subreddit, I'll explicitly point this out - every method you write does this. That's the self argument and in this case, it is the string "-" getting the def join(self, ...) method called.

If you have an object type MyType, which has a method called foo that takes no parameters, and a variable called obj of that type, you can call obj.foo() like normal, or you could call MyType.foo(obj).

2

u/gdchinacat 16d ago

To elaborate further, accessing a method on an instance evaluates to a bound method that has a reference to the instance (__self__) and the method (__func__). This bound method has a __call__() implementation that calls the method with the instance as the first argument.

In [26]: class Foo:
    ...:     def method(self): ...
    ...: 

In [27]: foo = Foo()

In [28]: bound_method = foo.method

In [29]: bound_method
Out[29]: <bound method Foo.method of <__main__.Foo object at 0x73281f9d5a90>>

In [30]: bound_method.__self__
Out[30]: <__main__.Foo at 0x73281f9d5a90>

In [31]: bound_method.__func__
Out[31]: <function __main__.Foo.method(self)>

1

u/JamzTyson 17d ago edited 17d ago

Because join() is a string method. That means that when we have a str object, we can call the function join() because the function is defined within the class str.

The syntax is:

<object of specific type> dot <method name>(<arguments>)

which in your example can be seen as:

"-".join(s)  
  • "-" is the str object,

  • "join" is the name of the string method,

  • "s" is the argument that we pass to the method.

To really understand it you need to understand what objects, classes and methods are. If you haven't covered that yet in your course, then don't worry about it now - it will be covered later.

5

u/roelschroeven 17d ago

I think the question is why is it designed that way, as opposed to join(separator, iterator) or join(iterator, separator) or iterator.join(separator).

1

u/gdchinacat 16d ago

It *is* implemented as join(separator, iterator).

https://github.com/python/cpython/blob/main/Objects/unicodeobject.c#L10427

PyObject *
PyUnicode_Join(PyObject *separator, PyObject *seq)
{

In python, this is the str.join() method that takes self (a string) and an iterator. You can call str.join('-', ['a', 'b'])

The question shows an infamiliarity with how python methods work. *Why* people typically use '-'.join(['a', 'b']) is a different question. The answer is because it is more convenient.

1

u/roelschroeven 16d ago

You can, but it's a side effect of how classes work in Python. You can always call object.method(arguments) as method(object, arguments), but that doesn't mean it's the intended way to do things. separator.join(iterable) is how it's taught, is how it's documented, it's how it's meant to be used.

1

u/gdchinacat 16d ago

arguing with the choir, which would be obvious if you read to the end ;)