Welcome to Software Development on Codidact!
Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.
How is this code "dividing" by a string?
I found a strange expression in some Python code:
from library import HOME_DIRECTORY
file = HOME_DIRECTORY / "file.json"
It seems to be dividing a string by another string in order to do something with files.
However, I can't make this work in the REPL with my own strings:
>>> "one" / "two"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for /: 'str' and 'str'
What is the code actually doing? Even if it didn't cause an error, I can't guess what dividing strings would mean, or why it would be useful.
3 answers
Python allows for operators to be overloaded (the subject of a separate future Q&A). The /
operator doesn't strictly mean division in a mathematical sense; it means whatever the operands define it to mean - but we may call the operation "division" regardless. (Specifically, we say "true division" to refer to the operation implemented by /
- this is to distinguish it from "floor division", implemented by //
.)
In this case, the left-hand side is a type that implements a __truediv__
method (which implements the /
operator); so Python translates the operator into a call to that method: HOME_DIRECTORY.__truediv__("file.json")
. Strings don't implement this, but whatever HOME_DIRECTORY
is, does.
The semantics, of course, depend on the type of HOME_DIRECTORY
. However, based on the context, we can infer that HOME_DIRECTORY
is an instance of the standard library pathlib.Path
type. The pathlib
library is designed specifically to support this sort of operation, to make it easier to work with file paths. In this case, the code simply joins up the path, creating a new path that represents "the file file.json
, within the HOME_DIRECTORY
".
Thus, HOME_DIRECTORY.__truediv__("file.json")
is roughly equivalent to os.path.join(HOME_DIRECTORY, "file.json")
- except that it starts and ends with a pathlib.Path
. The os
module also defines a PathLike
protocol implemented by pathlib.Path
, which allows you to use a pathlib.Path
in most places that you'd use a path string.
Of course, the reason we use "division" for this operation is purely mnemonic: /
is the default path separator (the one that Linux uses natively, and one which the Windows runtime will translate automatically). So given x
and y
path components, x / y
intuitively makes sense as a way to put them together into a longer path.
0 comment threads
The following users marked this post as Works for me:
User | Comment | Date |
---|---|---|
Andrew Ray | (no comment) | Jul 12, 2024 at 19:14 |
As already said by another answer, you're not "dividing a string by another string". I'd just like to complement by providing more details about how this works.
If you try to divide a string by another, such as x = 'a' / 'b'
, you'll get an error. Therefore, in your code, HOME_DIRECTORY
is certainly not a string. But how can you "divide" it by a string?
Well, that's because in Python operators can be overloaded. In fact, when you do x / y
, you're actually calling type(x).__truediv__(x, y)
. This behaviour is described in the language's Data Model, and in this same link you can also find all the special methods and the respective arithmetic operators (such as __add__
for +
, __sub__
for -
, and so on).
For the division operator (/
), the special method is __truediv__
, and that's why this code works:
x = 10
print(type(x).__truediv__(x, 2)) # 5.0
print(type(x).__truediv__(x, 2) == x / 2) # True
And this doesn't work for strings because they don't define such method:
print(int.__truediv__) # <slot wrapper '__truediv__' of 'int' objects>
print(str.__truediv__) # AttributeError: type object 'str' has no attribute '__truediv__'
x = 'a'
print(type(x).__truediv__(x, 'b')) # AttributeError: type object 'str' has no attribute '__truediv__'
That's how we know that HOME_DIRECTORY
is not a string. And based on the code, it's probably an instance of pathlib.Path
. If you look at the source code, you'll see that this class defines the __truediv__
method.
Not only that, but the method accepts either Path
instances or strings as the second operand, and it internally converts all to a single Path
object. That's why you can "divide" a Path
by a string:
from pathlib import Path
p = Path('folder')
print(p / 'file.json') # folder/file.json
print(type(p).__truediv__(p, 'file.json')) # folder/file.json
# both are the same (calling the division operator is the same as calling __truediv__ method)
print(type(p).__truediv__(p, 'file.json') == p / 'file.json') # True
If you think of /
in terms of arithmetic division, of course it won't make sense to "divide" something by a string. But I believe this was made because the slash (/
) is used as a path separator in *nix systems, and allowing to write code such as folder/file
would sound more "natural" and "intuitive", as we're dealing with Path
instances (AKA "objects that represent filesystem paths"). So I could write folder/file
just as I'd do in a command line.
In this case, you should stretch your mental model and forget about "divison". The /
is just an operator that internally calls a specific method. The fact that numbers define it as the division operator is just a detail (a very convenient one, though, as it's the "expected" behaviour). But it's not required that this operator always means "division" in every case/context. Operator overloading allows you to redefine operations to have any meaning you want - whether it makes sense or not (for Path
objects, it does, IMO).
Take, for instance, the +
operator. For numbers, it performs addition, but for strings, it concatenates them ('1' + '2'
results in '12'
). That's because strings define the __add__
method, whose behaviour is different to int
and float
. Does it make sense to "add strings"? Maybe it does for lots of people, because that's a feature that many languages have and we're so used to it that it just became "normal", but in a strict sense, it wouldn't make sense to perform arithmetic operations with text. With operator overloading, on the other hand, we can extend and change the behaviour and meaning of any operator, to suit our needs (such as "For strings, +
means concatenation instead of addition, because I said so").
As a side note, you can do it with your own classes:
class TrueDivTest:
def __truediv__(self, other):
return f'divided by {type(other).__name__}'
x = TrueDivTest()
print(x / 1) # divided by int
print(x / 'abc') # divided by str
print(x / []) # divided by list
print(x / x) # divided by TrueDivTest
Note how the second operand can be of any type. In a real case scenario, of course, I could perform a different action depending on the type, and/or restrict to specific types (and give an error if the type is not accepted), and so on. You're free to do anything with any operator, and give them any meaning you want. You're not restricted by the arithmetic definitions.
2 comment threads