You should pay attention to variables or function names in the form of __xxx__ (such as __slots__), as these have special purposes in Python.
We already know how to use __slots__, and we also know that the __len__() method enables a class to work with the len() function.
In addition to these, there are many other such special-purpose functions in Python classes that help us customize classes.
First, let’s define a Student class and print an instance:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x109afb190>
The output is a messy string like <__main__.Student object at 0x109afb190>, which is not user-friendly.
How can we make the output look better? Simply define the __str__() method to return a nicely formatted string:
>>> 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 printed instance is not only more readable but also clearly shows the important data inside the instance.
However, careful readers will notice that directly typing the variable name (without print) still produces an unreadable output:
>>> s = Student('Michael')
>>> s
<__main__.Student object at 0x109afb310>
This is because displaying a variable directly calls __repr__() instead of __str__(). The difference is that __str__() returns the string seen by the user, while __repr__() returns the string seen by the developer (i.e., __repr__() is for debugging purposes).
The solution is to define __repr__() as well. Since __str__() and __repr__() usually have identical code, there’s a shortcut:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
If a class wants to be used in a for ... in loop (like list or tuple), it must implement an __iter__() method. This method returns an iterator object, and Python’s for loop will continuously call the iterator’s __next__() method to get the next value in the loop until a StopIteration error is raised to exit the loop.
Let’s take the Fibonacci sequence as an example and write a Fib class that works with for loops:
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 self
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, let’s try using the Fib instance in a for loop:
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025
Although a Fib instance works with for loops and looks similar to a list, it cannot be used like a list (e.g., accessing the 5th element):
>>> Fib()[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing
To behave like a list and retrieve elements by index, we need to implement the __getitem__() method:
运行
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
Now we can access any term in the sequence by index:
>>> f = Fib()
>>> f[0]
1
>>> f[1]
1
>>> f[2]
2
>>> f[3]
3
>>> f[10]
89
>>> f[100]
573147844013817084101
However, list has a powerful slicing feature:
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]
This throws an error for Fib. The reason is that the parameter passed to __getitem__() can be either an int (index) or a slice object (slice), so we need to add a check:
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 let’s test slicing with Fib:
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
However, we haven’t handled the step parameter:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
Nor have we handled negative indices. Therefore, implementing __getitem__() correctly requires significant additional work.
Additionally, if treating the object as a dict, the parameter to __getitem__() could be any hashable object (e.g., str) that can act as a key.
The corresponding __setitem__() method allows the object to be treated as a list or dict for assignment operations. Finally, there is the __delitem__() method for deleting elements.
In summary, through the above methods, our custom class behaves identically to Python’s built-in list, tuple, and dict—thanks entirely to the dynamic language feature of duck typing, which does not require forced inheritance of a specific interface.
Under normal circumstances, calling a non-existent method or attribute of a class will raise an error. For example, define the Student class:
class Student(object):
def __init__(self):
self.name = 'Michael'
Calling the name attribute works fine, but calling the non-existent score attribute causes an error:
>>> 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 indicates that the score attribute was not found.
To avoid this error (other than adding a score attribute), Python provides another mechanism: writing a __getattr__() method to dynamically return an attribute. Modify the class as follows:
class Student(object):
def __init__(self):
self.name = 'Michael'
def __getattr__(self, attr):
if attr=='score':
return 99
When calling a non-existent attribute (e.g., score), the Python interpreter attempts to call __getattr__(self, 'score') to retrieve the attribute—giving us the opportunity to return a value for score:
>>> s = Student()
>>> s.name
'Michael'
>>> s.score
99
Returning a function is also fully supported:
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
Only the calling syntax changes:
>>> s.age()
25
Note that __getattr__ is only called when the attribute is not found. Existing attributes (e.g., name) are not looked up in __getattr__.
Additionally, arbitrary calls like s.abc return None because our __getattr__ defaults to returning None. To make the class respond only to specific attributes, we must raise an AttributeError as per conventions:
class Student(object):
def __getattr__(self, attr):
if attr=='age':
return lambda: 25
raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)
This effectively allows dynamic handling of all attribute and method calls for a class without any special tricks.
What practical use does this fully dynamic calling feature have? It enables calls for completely dynamic scenarios.
Many websites now offer REST APIs (e.g., Sina Weibo, Douban). The URLs for calling these APIs look like:
http://api.server/user/friends
http://api.server/user/timeline/list
Writing an SDK that implements a separate method for each API URL would be extremely tedious—and any API changes would require updating the SDK.
Using the fully dynamic __getattr__, we can implement chain calls:
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__
Test it:
>>> Chain().status.user.timeline.list
'/status/user/timeline/list'
This way, no matter how the API changes, the SDK can implement fully dynamic calls based on URLs—without modification as new APIs are added!
Some REST APIs include parameters in the URL (e.g., GitHub’s API):
GET /users/:user/repos
When calling, :user needs to be replaced with the actual username. If we could write chain calls like this:
Chain().users('michael').repos
it would make API calls extremely convenient. Interested readers can try implementing this.
An object instance can have its own attributes and methods. When calling an instance method, we use instance.method(). Can we call the instance directly? In Python, the answer is yes.
Any class can be made callable directly by defining a __call__() method. See the example:
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print('My name is %s.' % self.name)
Call it like this:
>>> s = Student('Michael')
>>> s() # Do not pass the self parameter
My name is Michael.
__call__() can also define parameters. Calling an instance directly is just like calling a function—so you can treat objects as functions and functions as objects, as there is no fundamental distinction between them.
If you treat objects as functions, functions themselves can be dynamically created at runtime (since class instances are created at runtime). This blurs the line between objects and functions.
So how do we determine if a variable is an object or a function? In practice, we often need to check if an object is callable. Callable objects include functions and class instances with a __call__() method:
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
The callable() function lets us check if an object is callable.
Python classes allow defining numerous custom methods, making it easy to create specialized classes.
This section covers the most commonly used custom methods—many more are available; refer to Python’s official documentation for details.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return "Student object (name: %s)" % self.name
__repr__ = __str__
print(Student("Michael"))
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1
def __iter__(self):
return self
def __next__(self):
self.a, self.b = self.b, self.a + self.b
if self.a > 100000:
raise StopIteration()
return self.a
for n in Fib():
print(n)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Fib(object):
def __getitem__(self, n):
if isinstance(n, int):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, 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
f = Fib()
print(f[0])
print(f[5])
print(f[100])
print(f[0:5])
print(f[:10])
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Student(object):
def __init__(self):
self.name = "Michael"
def __getattr__(self, attr):
if attr == "score":
return 99
if attr == "age":
return lambda: 25
raise AttributeError("'Student' object has no attribute '%s'" % attr)
s = Student()
print(s.name)
print(s.score)
print(s.age())
# AttributeError: 'Student' object has no attribute 'grade'
print(s.grade)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
class Student(object):
def __init__(self, name):
self.name = name
def __call__(self):
print("My name is %s." % self.name)
s = Student("Michael")
s()