Class and Instance Attributes
Introduction
- Regular (instance) methods need a class instance and can access the instance through
self. They can read and modify an objects state freely. - Class methods, marked with the
@classmethoddecorator, don’t need a class instance. They can’t access the instance (self) but they have access to the class itself via cls. - Static methods, marked with the
@staticmethoddecorator, don’t have access to cls or self. They work like regular functions but belong to the class’s namespace.
Tip
Static and class methods communicate and (to a certain degree) enforce developer intent about class design. This can have maintenance benefits.
Static Methods
We used class attributes as public attributes in the previous section. Of course, we can make public attributes private as well. We can do this by adding the double underscore again. If we do so, we need a possibility to access and change these private class attributes. We could use instance methods for this purpose:
class Robot:
__counter = 0
def __init__(self):
type(self).__counter += 1
def RobotInstances(self):
return Robot.__counter
if __name__ == "__main__":
x = Robot()
print(x.RobotInstances())
y = Robot()
print(x.RobotInstances())
Output:
1
2
This is not a good idea for two reasons: First of all, because the number of robots has nothing to do with a single robot instance and secondly because we can’t inquire the number of robots before we create an instance. If we try to invoke the method with the class name Robot.RobotInstances(), we get an error message, because it needs an instance as an argument:
Robot.RobotInstances()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-35-f53600e3296e> in <module>
----> 1 Robot.RobotInstances()
TypeError: RobotInstances() missing 1 required positional argument: 'self'
The next idea, which still doesn’t solve our problem, is omitting the parameter “self”:
class Robot:
__counter = 0
def __init__(self):
type(self).__counter += 1
def RobotInstances():
return Robot.__counter
Now it’s possible to access the method via the class name, but we can’t call it via an instance:
from static_methods2 import Robot
Robot.RobotInstances()
Output::
0
x = Robot()
x.RobotInstances()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-40-4d5e11c3474a> in <module>
1 x = Robot()
----> 2 x.RobotInstances()
TypeError: RobotInstances() takes 0 positional arguments but 1 was given
The call "x.RobotInstances()" is treated as an instance method call and an instance method needs a reference to the instance as the first parameter.
So, what do we want? We want a method, which we can call via the class name or via the instance name without the necessity of passing a reference to an instance to it.
The solution lies in static methods, which don’t need a reference to an instance. It’s easy to turn a method into a static method. All we have to do is to add a line with staticmethod() directly in front of the method header. It’s the decorator syntax.
You can see in the following example that we can now use our method RobotInstances the way we want:
class Robot:
__counter = 0
def __init__(self):
type(self).__counter += 1
@staticmethod
def RobotInstances():
return Robot.__counter
if __name__ == "__main__":
print(Robot.RobotInstances())
x = Robot()
print(x.RobotInstances())
y = Robot()
print(x.RobotInstances())
print(Robot.RobotInstances())
Output:
0
1
2
2
Class Methods
Static methods shouldn’t be confused with class methods. Like static methods class methods are not bound to instances, but unlike static methods class methods are bound to a class. The first parameter of a class method is a reference to a class, i.e. a class object. They can be called via an instance or the class name.
class Robot:
__counter = 0
def __init__(self):
type(self).__counter += 1
@classmethod
def RobotInstances(cls):
return cls, Robot.__counter
if __name__ == "__main__":
print(Robot.RobotInstances())
x = Robot()
print(x.RobotInstances())
y = Robot()
print(x.RobotInstances())
print(Robot.RobotInstances())
Class Methods vs. Static Methods and Instance Methods
This example will demonstrate the usefulness of class methods in inheritance. We define a class Pet with a method about(). This method should give some general class information. The class Pet will be inherited in a subclass Dog and Cat. The method about() will be inherited as well. We will demonstrate that we will encounter problems, if we define the method about() as a normal instance method or as a static method. We will start by defining about() as an instance method:
class Pet:
_class_info = "pet animals"
def about(self):
print("This class is about " + self._class_info + "!")
class Dog(Pet):
_class_info = "man's best friends"
class Cat(Pet):
_class_info = "all kinds of cats"
p = Pet()
p.about()
d = Dog()
d.about()
c = Cat()
c.about()
This may look alright at first at first glance. On second thought we recognize the awful design. We had to create instances of the Pet, Dog and Cat classes to be able to ask what the class is about. It would be a lot better, if we could just write Pet.about(), Dog.about() and Cat.about() to get the previous result. We cannot do this. We will have to write Pet.about(p), Dog.about(d) and Cat.about(c) instead.
Now, we will define the method “about” as a “staticmethod” to show the disadvantage of this approach. As we have learned previously in our tutorial, a staticmethod does not have a first parameter with a reference to an object. So about will have no parameters at all. Due to this, we are now capable of calling about() without the necessity of passing an instance as a parameter, i.e. “Pet.about()”, “Dog.about()” and “Cat.about()”. Yet, a problem lurks in the definition of about(). The only way to access the _class_info is putting a class name in front. We arbitrarily put in “Pet”. We could have put there “Cat” or “Dog” as well. No matter what we do, the solution will not be what we want:
class Pet:
_class_info = "pet animals"
@staticmethod
def about():
print("This class is about " + Pet._class_info + "!")
class Dog(Pet):
_class_info = "man's best friends"
class Cat(Pet):
_class_info = "all kinds of cats"
Pet.about()
Dog.about()
Cat.about()
In other words, we have no way of differenciating between the class Pet and its subclasses Dog and Cat.The problem is that the method “about” does not know that it has been called via the Pet, the Dog or the Cat class .
A classmethod is the solution to all our problems. We will decorate about() with a classmethod decorator instead of a staticmethod decorator:
class Pet:
_class_info = "pet animals"
@classmethod
def about(cls):
print("This class is about " + cls._class_info + "!")
class Dog(Pet):
_class_info = "man's best friends"
class Cat(Pet):
_class_info = "all kinds of cats"
Pet.about()
Dog.about()
Cat.about()