Polymorphism and Magic Methods

The last two lessons covered the "form" of classes and inheritance. This lesson covers the "spirit." Polymorphism lets objects of different classes be called in the same way — you don't need to know their specific class, just that they have the required method. Magic methods make custom classes behave as naturally as built-in types.


1. What is Polymorphism

Polymorphism means "many forms" — the same interface, different implementations:

PYTHON
class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class Duck:
    def speak(self):
        return "Quack!"

# Polymorphism: regardless of class, as long as it has a speak() method
animals = [Dog(), Cat(), Duck()]
for animal in animals:
    print(animal.speak())

Output:

TEXT
Woof!
Meow!
Quack!

Tip: Duck Typing: "If it walks like a duck and quacks like a duck, it's a duck." Python doesn't care about an object's type — only whether it has the method you need. This is one of Python's most flexible designs.

Example: Shape Area Calculation (Difficulty: Star-Star)

PYTHON
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height

class Circle:
    def __init__(self, radius):
        self.radius = radius
    
    def area(self):
        return 3.14159 * self.radius ** 2

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height
    
    def area(self):
        return 0.5 * self.base * self.height

# Polymorphism — regardless of shape, just calculate area
shapes = [Rectangle(3, 4), Circle(5), Triangle(6, 8)]
for shape in shapes:
    print(f"Area: {shape.area():.2f}")
▶ Try it Yourself

Output:

TEXT
Area: 12.00
Area: 78.54
Area: 24.00

2. Magic Methods: Making Classes More "Pythonic"

Magic methods are special methods with double underscore prefixes and suffixes — __init__, __str__, __len__, etc. They make custom classes behave like built-in types.

__str__ and __repr__: Friendlier Printing

PYTHON
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def __str__(self):
        """User-friendly string representation"""
        return f"{self.name} ({self.age} years old)"
    
    def __repr__(self):
        """Developer-friendly string representation"""
        return f"Person('{self.name}', {self.age})"

p = Person("Zhang San", 25)
print(p)                    # Zhang San (25 years old) — calls __str__
print(repr(p))              # Person('Zhang San', 25) — calls __repr__

__len__: Making Objects Support len()

PYTHON
class Team:
    def __init__(self, members=None):
        self.members = members or []
    
    def add(self, member):
        self.members.append(member)
    
    def __len__(self):
        return len(self.members)
    
    def __str__(self):
        return f"Team({', '.join(self.members)})"

team = Team()
team.add("Zhang San")
team.add("Li Si")
team.add("Wang Wu")

print(len(team))            # 3 — calls __len__
print(team)                 # Team(Zhang San, Li Si, Wang Wu)

__getitem__: Making Objects Support Indexing

PYTHON
class Playlist:
    def __init__(self):
        self.songs = []
    
    def add(self, song):
        self.songs.append(song)
    
    def __getitem__(self, index):
        return self.songs[index]
    
    def __len__(self):
        return len(self.songs)

playlist = Playlist()
playlist.add("Sunny Day")
playlist.add("Seven Li Fragrance")
playlist.add("Nocturne")

print(playlist[0])          # Sunny Day — calls __getitem__
print(len(playlist))        # 3

# Supports slicing and iteration
for song in playlist:
    print(song, end=" ")    # Sunny Day Seven Li Fragrance Nocturne

3. Common Magic Methods Reference

PYTHON
class Demo:
    def __init__(self, value):      # Constructor
        self.value = value
    
    def __str__(self):              # str() / print()
        return str(self.value)
    
    def __repr__(self):             # repr() / debugging
        return f"Demo({self.value})"
    
    def __len__(self):              # len()
        return 1
    
    def __bool__(self):             # bool() / if condition
        return self.value != 0
    
    def __eq__(self, other):        # ==
        return self.value == other.value
    
    def __lt__(self, other):        # < (for sorting)
        return self.value < other.value
    
    def __add__(self, other):       # + operator
        return Demo(self.value + other.value)
    
    def __getitem__(self, key):     # obj[key]
        return key

Example: Fraction Class (Difficulty: Star-Star-Star)

PYTHON
class Fraction:
    """Fraction class — supports addition"""
    def __init__(self, numerator, denominator=1):
        if denominator == 0:
            raise ValueError("Denominator cannot be zero")
        self.numerator = numerator
        self.denominator = denominator
    
    def __str__(self):
        if self.denominator == 1:
            return str(self.numerator)
        return f"{self.numerator}/{self.denominator}"
    
    def __add__(self, other):
        new_den = self.denominator * other.denominator
        new_num = self.numerator * other.denominator + other.numerator * self.denominator
        return Fraction(new_num, new_den)
    
    def __eq__(self, other):
        return (self.numerator * other.denominator) == (other.numerator * self.denominator)

f1 = Fraction(1, 3)
f2 = Fraction(1, 6)
print(f1)                           # 1/3
print(f1 + f2)                      # 9/18
print(Fraction(1, 2) == Fraction(2, 4))  # True
▶ Try it Yourself

4. @dataclass: Quickly Defining Data Classes

Writing a class just for storing data, with manual __init__, __repr__, __eq__, is tedious. @dataclass generates them automatically:

PYTHON
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float

@dataclass
class Student:
    name: str
    age: int
    score: float = 0.0    # Default value

# Automatically gets __init__, __repr__, __eq__
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
print(p1)                   # Point(x=3.0, y=4.0)
print(p1 == p2)             # True (compares all fields automatically)

s = Student("Zhang San", 20, 85.5)
print(s)                    # Student(name='Zhang San', age=20, score=85.5)

Tip: When to use @dataclass: 1) A class mainly stores data with few methods. 2) You need boilerplate like __init__, __repr__, __eq__. 3) You don't want to write these manually. For simple cases, it's much more efficient than hand-writing classes.


Common Use Cases


FAQ

Q What's the difference between magic methods and regular methods?
A Magic methods are called by Python's built-in operations — print() calls __str__, len() calls __len__, == calls __eq__. You don't call them directly; just implement them, and Python calls them automatically at the right time.
Q What's the difference between @dataclass and a regular class?
A @dataclass auto-generates the methods you're tired of writing. It's just a code generator — the generated class is identical to a hand-written one. Classes decorated with @dataclass can also have their own methods. If you find yourself repeatedly writing __init__ to store properties, it's time for @dataclass.
Q Python is dynamically typed — does polymorphism come naturally?
A Yes. Python's "duck typing" makes polymorphism possible without inheritance — just having methods with the same name is enough. Other languages (like Java/C++) need interfaces or abstract classes for polymorphism. The benefit is flexibility; the downside is that type-checking can't catch errors as easily.

Summary


Exercises

  1. Beginner (Difficulty: Star): Define a Book class implementing __str__ and __len__ (returning the page count). Create two books and output print(book) and len(book).

  2. Intermediate (Difficulty: Star-Star): Use @dataclass to define a Product class (name, price, quantity). Implement __add__ — merge two products with the same name (sum quantities, average price). Hint: Implement inside the @dataclass class.

  3. Advanced (Difficulty: Star-Star-Star): Define a Deck class (a deck of playing cards) implementing __len__, __getitem__ (supporting indexing and slicing), and shuffle() (using random.shuffle). Then write code to create a deck and deal the first 5 cards as a hand.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏