Skip to content
On this page

Customizing Classes

When you see variables or function names in the form of __xxx__, such as __slots__, be aware that these have special purposes in Python.

We already know how to use __slots__, and we also understand that the __len__() method allows a class to work with the len() function.

Besides these, Python's classes have many such special-purpose functions that help us customize classes.

str

Let's first define a Student class and print an instance:

python
>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>

This prints something like <__main__.Student object at 0x109afb190>, which isn't very readable.

How can we make it print nicely? Simply define the __str__() method to return a readable string:

python
>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...     def __str__(self):
...         return 'Student object (name: %s)' % self.name
...
>>> print(Student('Michael'))
Student object (name: Michael)

Now, the instance is printed not only nicely but also clearly shows important internal data.

However, observant users might notice that directly typing the variable without print still doesn't look good:

python
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>

This is because directly displaying the variable doesn't call __str__(), but instead calls __repr__(). The difference is that __str__() returns a string meant for users, while __repr__() returns a string meant for developers, primarily for debugging purposes.

To fix this, you can also define the __repr__() method. However, since the code for __str__() and __repr__() is usually the same, there's a shortcut:

python
class Student(object):
    def __init__(self, name):
        self.name = name
    def __str__(self):
        return 'Student object (name=%s)' % self.name
    __repr__ = __str__

iter

If a class wants to be used in a for ... in loop, similar to list or tuple, it must implement the __iter__() method. This method should return an iterator object, and Python's for loop will repeatedly call the iterator's __next__() method to get the next value until a StopIteration exception is raised to exit the loop.

Let's take the Fibonacci sequence as an example and write a Fib class that can be used in a for loop:

python
class Fib(object):
    def __init__(self):
        self.a, self.b = 0, 1  # Initialize two counters a and b

    def __iter__(self):
        return self  # The instance itself is the iterator, so return itself

    def __next__(self):
        self.a, self.b = self.b, self.a + self.b  # Calculate the next value
        if self.a > 100000:  # Condition to exit the loop
            raise StopIteration()
        return self.a  # Return the next value

Now, try using a Fib instance in a for loop:

python
>>> for n in Fib():
...     print(n)
...
1
1
2
3
5
...
46368
75025

getitem

Although a Fib instance can be used in a for loop and resembles a list in some ways, it doesn't support indexing like a list. For example, trying to get the 5th element:

python
>>> Fib()[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing

To make it behave like a list and allow element access by index, you need to implement the __getitem__() method:

python
class Fib(object):
    def __getitem__(self, n):
        a, b = 1, 1
        for x in range(n):
            a, b = b, a + b
        return a

Now, you can access any element of the sequence by its index:

python
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101

However, lists have a magical slicing method:

python
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]

Attempting this on Fib results in an error. The reason is that the __getitem__() method may receive an int or a slice object, so you need to handle both cases:

python
class Fib(object):
    def __getitem__(self, n):
        if isinstance(n, int):  # n is an index
            a, b = 1, 1
            for x in range(n):
                a, b = b, a + b
            return a
        if isinstance(n, slice):  # n is a slice
            start = n.start
            stop = n.stop
            if start is None:
                start = 0
            a, b = 1, 1
            L = []
            for x in range(stop):
                if x >= start:
                    L.append(a)
                a, b = b, a + b
            return L

Now, try slicing the Fib sequence:

python
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

However, it doesn't handle the step parameter:

python
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

It also doesn't handle negative indices, so correctly implementing __getitem__() requires more work.

Additionally, if you treat the object as a dict, the __getitem__() parameter might be a key object, such as a str.

Correspondingly, there's the __setitem__() method to assign values to the collection when treating the object like a list or dict, and the __delitem__() method to delete an element.

In summary, by implementing these methods, your custom classes can behave just like Python's built-in list, tuple, and dict, all thanks to the dynamic nature of the language's "duck typing," which doesn't require enforcing any specific interface inheritance.

getattr

Under normal circumstances, when you call a class's method or access its attribute, if it doesn't exist, an error is raised. For example, defining a Student class:

python
class Student(object):
    def __init__(self):
        self.name = 'Michael'

Accessing the name attribute is fine, but accessing a non-existent score attribute causes an issue:

python
>>> s = Student()
>>> print(s.name)
Michael
>>> print(s.score)
Traceback (most recent call last):
  ...
AttributeError: 'Student' object has no attribute 'score'

The error message clearly states that the score attribute was not found.

To avoid this error, besides adding a score attribute, Python provides another mechanism: defining a __getattr__() method to dynamically return an attribute. Modify the class as follows:

python
class Student(object):
    def __init__(self):
        self.name = 'Michael'

    def __getattr__(self, attr):
        if attr == 'score':
            return 99

When accessing a non-existent attribute like score, Python will attempt to call __getattr__(self, 'score'), giving you the opportunity to return a value:

python
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99

Returning a function is also possible:

python
class Student(object):
    def __getattr__(self, attr):
        if attr == 'age':
            return lambda: 25

But the way to call it changes to:

python
>>> s.age()
25

Note that __getattr__() is only called when the attribute is not found. Existing attributes, like name, won't be searched in __getattr__().

Additionally, note that any arbitrary call like s.abc would return None because our __getattr__() defaults to returning None. To make the class respond only to specific attributes, you should follow the convention of raising an AttributeError:

python
class Student(object):
    def __getattr__(self, attr):
        if attr == 'age':
            return lambda: 25
        raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

This allows you to dynamically handle all attribute and method calls for a class without any special techniques.

What practical use does this dynamic calling feature have? It allows handling completely dynamic situations.

For example:

Many websites now offer REST APIs, such as Sina Weibo and Douban, where API URLs look like:

http://api.server/user/friends
http://api.server/user/timeline/list

If you write an SDK, creating a method for each URL would be tedious, and the SDK would need to be updated whenever the API changes.

By leveraging the fully dynamic __getattr__(), we can create a chainable call structure:

python
class Chain(object):
    def __init__(self, path=''):
        self._path = path

    def __getattr__(self, path):
        return Chain('%s/%s' % (self._path, path))

    def __str__(self):
        return self._path

    __repr__ = __str__

Try it out:

python
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'

This way, regardless of how the API changes, the SDK can handle calls dynamically based on the URL without needing to change as APIs are added.

Some REST APIs include parameters in the URL, such as GitHub's API:

GET /users/:user/repos

When calling, you need to replace :user with the actual username. If we can write a chainable call like:

python
Chain().users('michael').repos

it becomes very convenient to call the API. Interested readers can try implementing this themselves.

call

An object instance can have its own attributes and methods. When you call an instance method, you use instance.method(). But can you call the instance itself directly? In Python, the answer is yes.

Any class that defines a __call__() method can have its instances called directly. Here's an example:

python
class Student(object):
    def __init__(self, name):
        self.name = name

    def __call__(self):
        print('My name is %s.' % self.name)

Calling it works as follows:

python
>>> s = Student('Michael')
>>> s()  # Do not pass the self parameter
My name is Michael.

The __call__() method can also accept parameters. Directly calling an instance is similar to calling a function, so you can treat objects as functions and functions as objects because there's no fundamental difference between the two.

If you treat an object as a function, then functions themselves can be dynamically created at runtime because class instances are created at runtime. This blurs the boundary between objects and functions.

So, how do you determine if a variable is an object or a function? Often, you need to check if an object is callable. Callable objects are those that can be called, such as functions and instances with a __call__() method:

python
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

Using the callable() function, you can determine if an object is "callable."

Summary

Python's classes allow defining many special methods, making it very convenient to create specific classes.

This section introduced some of the most commonly used special methods. There are many more customizable methods available; please refer to Python's official documentation for more details.

Customizing Classes has loaded