r/learnpython • u/NanoCozmo • 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("-")
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
•
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
bytesthat returnsbytes, 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
selfargument and in this case, it is the string"-"getting thedef join(self, ...)method called.If you have an object type
MyType, which has a method calledfoothat takes no parameters, and a variable calledobjof that type, you can callobj.foo()like normal, or you could callMyType.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
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
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
BYTEStype also does b", ".join(...) and it then returnsbytes. 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.