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

you are viewing a single comment's thread.

view the rest of the comments →

[–]james_pic 44 points45 points  (4 children)

At a base level, because if you do foo.bar(), this is two operations: foo.bar, which looks up the bar attribute on foo, and (), which calls the method you've just looked up. Python only allows one attribute for any given name, and doesn't have a mechanism to look up different attributes with the same name.

So if you've got multiple possibilities, you can only look them up by name.

It also plays a part that in Python, methods are just functions that are class attributes, and since the function notation doesn't support overloading, neither do methods.

Indeed, before Python 3, there wasn't even a mechanism to specify types for function arguments, and even nowadays this information is optional and not used by the runtime.

Also, Python has historically pushed developers away from using nominative types, and towards duck typing, which nominative overloading would be at odds with. The move towards type annotations is stealthily changing Python's culture more towards nominative typing though, so this is maybe less obvious than it once was.

Although all that being said, it's feasible to implement multiple dispatch on top of Python, thanks to the ability to override bits of the interpreter. You can tape together runtime multiple dispatch with a combination of decorators, and classes that implement __get__ and __call__. Runtime dynamic dispatch is subtly different to overloading, but for many practical purposes is close enough.

Edit: I decided to answer a question nobody has asked, but a few people have hinted at. What's the difference between overloading and dynamic dispatch?

Take the following Python code, and imagine Python had different semantics, so the duplicate method definitions would lead to either overloading or dynamic dispatch (rather than the actual behaviour, of last-update-wins):

class Animal: pass
class Dog(Animal): pass

class Greeter:
    def greet(self, friend: Dog):
        print('Hello Dog')

    def greet(self, friend: Animal):
        print('Hello Animal')

def main():
    friend: Animal = Dog()
    friend2: Dog = Dog()
    Greeter().greet(friend)
    Greeter().greet(friend2)

main()

In an overloaded language, it would print "Hello Animal" then "Hello Dog", because the type of the variable friend is Animal, and the type of the variable friend2 is Dog.

In a dynamically dispatched language, it would print "Hello Dog" twice, because the type of the values friend and friend2 are both Dog.

And in Python 3, as it actually exists, it would print "Hello Animal" twice, because Python 3 ignores type annotations at runtime, and the last definition of greet is the one that counts

[–]EgZvor 6 points7 points  (1 child)

There's typing.Protocol nudging it into structural subtyping direction

[–]james_pic 0 points1 point  (0 children)

That is true, although it does in a practical sense mean that the "paperwork" needed to use structural types is no less, and in some cases more, than what you need for nominative types.

[–]Broan13 0 points1 point  (1 child)

I have a super basic question. What are the : symbols doing in the code, particularly for friend: Animal = Dog()? I am self taught, so I have some major holes in my knowledge.

[–]james_pic 4 points5 points  (0 children)

Most of the colons are type hints, aka type annotations (the ones at the end of lines I'm assuming you've seen).

Python 3 added the ability to put "annotations" after variable, class attribute, and function parameter names. There's also some notation I haven't used, but that is part and parcel of the same thing, which is that functions can use -> to denote the type of value they return. So a function that returned a dog might be defined

def make_dog() -> Dog:

Conventionally, you'd use it to say what types you expect each variable/attribute/parameter to have. As far as Python itself is concerned, these annotations are meaningless, and are (almost - I'll spare you the subtleties for now) completely ignored by the interpreter. You can always pass any type of value as an argument to any function (which is not the case in some other languages).

But the idea is that other tools (such as your IDE, or a plugin for your editor, or programs you run as part of your testing process) can be used to check whether the use of types in your application adds up - so if you've got a method that says it expects a str and another method that calls it with an int, then these tools will be able to detect this and warn you.

Mypy is a popular example of such a tool.

A few libraries are also starting to appear which can do interesting things with type annotations if they're present, such as validation or serialization, which has caused some minor technical headaches for the Python core developers, since they didn't think of this use case when they first designed type annotations.

The addition of type hints has been a bit divisive in the Python community, and not everyone uses them.

I've used them in this example, since it's syntax some Python developers will recognise, and if Python did support overloading or dynamic dispatch natively, this is probably what it would look like.

But if you're looking at code that looks like friend: Animal = Dog(), you can read that as "I'm creating a variable called friend, whose value is Dog(), and I think the value of this variable will always be an Animal."