"Polymorphic" roughly translates to "multiply-shaped", and means that a single piece of code will work for many different (data/code) structures. Your code example isn't quite polymorphic, since you have one line of code for a
circle and one for a
rectangle; here's how we could make it truly polymorphic:
for shape in [circle1, rectangle1]:
Here the line of code
shape.draw() is polymorphic, since that single line of code works for multiple data structures (a
circle and a
We can understand polymorphism in terms of "first-class functions/methods" (i.e. values which represent methods). Polymorphic code can work in multiple situations by first fetching a method which is appropriate for the current situation, then running that method.
In the above example, these two steps could be written like this:
for shape in [circle1, rectangle1]:
myMethod = Object.lookup(shape, "draw")
I've made up the static methods
Method.invoke, but hopefully you get the idea: we're looking up the
"draw" slot of the
shape object, which will return a value
myMethod representing that method. Since
Object.lookup works the same way for any string and object it is not polymorphic. The static method
Method.invoke will run
myMethod; again, this works the same way for all method values, so it's also not polymorphic.
So why does OOP make such a big deal about polymorphism? There are two main reasons:
Some languages support polymorphism (like
shape.draw()) but do not support first-class methods (like
myMethod). Java and C++ were examples of such languages, although newer versions so support first-class methods (called "lambdas"). In such languages we cannot write code involving things like
myMethod, so the above explanation won't work in practice (although the idea still applies).
Object.lookup(shape, "draw") is dynamic: the lookup is done at run-time, and we have no way to know if it will work (for example, instead of the literal string
"draw", we could instead take the string from a file or from user input). In the polymorphic code, the method name
draw is static: it's always literally there in the code, which might gives us enough information to check whether the lookup will work before we run the code. Again, Java and C++ are examples of languages which will perform these checks (as part of their compilation).
(Languages like Java and C++ have had a large impact on programming, and OOP in particular, so it's not surprising that their features and style crop up a lot when describing OOP concepts like polymorphism. Other languages, like Python, don't do such checks, which leads to different styles like "duck typing" that others mention.)
One way to check whether a lookup will work is called subtype polymorphism, where the method name is associated with some explicit 'contract' (e.g. a "class", "interface", "signature", "abstract type", "existential type", "type class", etc. depending on the language) and we only allow lookups of that method for values which claim to fulfil that contract (e.g. "objects instantiating a class/subclass", "objects inheriting from a prototype", "modules implementing a signature", "types instantiating a type class", etc.).
The Answer to Your Question
Checking for things like subtype polymorphism is conservative:
- Code which passes the check will work. For example, if we require a
Circle object and the
Circle class implements a
draw method, then calling
draw on the given object will succeed in finding an implementation to run.
- Code which would work might not pass the check. For example, if we try giving a
Rectangle object to the code in the last example, the check will fail since it isn't a
Circle. Yet, in this case,
Rectangle has a
draw method, so the code would work (if the failed check didn't stop us).
Your question is describing this second situation. We can think of type-checking as a form of mathematical proof: if we know that
shape is a
Circle, that implies is has a
draw method, so we know it's safe to call
shape.draw(). On the other hand, if we know that
shape is not a
Circle, we don't know whether it has a
draw method of not, so it would be unsafe to allow calling
Some languages (e.g. C++ and Java) will forbid these unsafe situations, so we need to make more complicated proofs to convince them that it's safe (e.g. having
draw from some parent class, or implement the same
Drawable interface, etc.). Other languages (e.g. Python and Smalltalk) will allow these unsafe situations, but then it's up to us to make sure our programs work as expected.
Note that there are other forms of polymorphism too! For example "parametric polymorphism" describes code which doesn't care about some details of a value, and hence those details can have any type. For example, getting the length of a list:
result = 0
result = result + 1
l = l.next
This will work for lists of integers, lists of strings, lists of lists of booleans, etc. and is hence (parametrically) polymorphic.
In OOP the word "polymorphism" tends to mean subtype polymorphism, whilst parametric polymorphism tends to be called "generics". Languages which don't enforce checks, like Python, are more fuzzy when it comes to such classification: we can write Python code which uses (subtype) polymorpism (e.g. using classes), but we can also write Python code which looks like (subtype) polymorphism, without any explicit mechanism to enforce it (AKA "duck typing").
If our language supports first-class methods/functions, then we can write "higher-order functions" which act in a similar way to polymorphic code, further blurring the boundaries. For example:
def runOn(f, x):
for (draw, shape) in [(drawCircle, circle1), (drawRectangle, rectangle1)]: