In my previous post I looked at some of the basics of Python. In this post I want to dig further into the syntax etc. for classes within Python.
Class naming conventions
We prefix a class name with the class keyword. Python naming convention suggests the class name should be Pascal case, i.e. ClassName.
The method naming convention, by default, should be all lowercase with underscores (snake case) to separate words, i.e. method_name.
Private/protected methods and instance variables should have an underscore prefix, i.e. _private_name.
Defining a class
The special method name __init__ can be thought of a little like a constructor, however we cannot overload it, but we can pass arguments into it
class Animal: def __init__(self, name): self.name = name
Note: Whilst it might appear possible to have multiple __init__ methods, it’s the last one declared which seems to be the one available to calling code.
We terminate the line that the class keyword is on with the colon and then methods or variables start on a new line and indented with a tab.
The self parameter passed to __init__ or any other class method is passed implicitly, i.e. we do not supply the self, Python does that for us.
Inheritance
Python classes support inheritance (even multiple inheritance) by declaring the base classes in a comma separated list within parenthesis, i.e.
class Animal: def __init__(self, name): self.name = name class Mammal: pass class Dog(Animal, Mammal): def __init__(self): super().__init__("Dog")
In this example you can see how we can call the base class’ __init__ method. The Dog class derives from both Animal and Mammal classes (in this instance the Mammal class has no methods etc. hence uses pass statement to create the empty class).
Calling base methods in Multiple Inheritance
Multiple inheritance always has the issue around what base method to call in situations where there’s multiple methods of the same name, for example if we have the following
class A: def name(self): print("A") class B: def name(self): print("B") class C(A, B): pass c = C() c.name()
then what base class method is actually called via c.name().
A quick note: In the above as we’re missing the method name() on the class C, this can be seen as the following
class C(A, B): def name(self): super(C, self).name()
The order of resolving the methods to be called is the Method Resolution Order (MRO).
For our simple example, the first name() method located depends upon the order of inheritance, i.e. in the example above A’s name() is called, switching the code for C to
class C(B, A): pass
results in B’s name method being called.
To ensure we are explicit about which code is called, it’s best to override the name method in the subclass like this
class C(A, B): def name(self): B.name(self)
and obviously this ensures B’s name is called.
Methods
We declare methods within a class using the keyword def and all methods are virtual, i.e. can be overridden in a subclass. Here’s a basic example over method overriding
class Animal: def name(self): pass class Dog(Animal): def name(self): return "Dog"
In this example we’re using the Animal a little like an abstract class and then implementing the name method in derived classes.
Member variables/fields
Let’s rewrite the previous code to now use a member variable in the base class to store the name of the Animal.
class Animal: _name = "" def name(self): return self._name class Dog(Animal): def __init__(self): self._name = "Dog"
In this case the prefixed _ denotes a protected/private variable. In most OO languages this encapsulates the member variable in such a way as to ensure it’s no accessible outside of the base and derived classes, however in Python it’s still available, and thus this will return the value stored within the _name variable. In PyCharm we get a hint that we’re accessing a protected member, but ultimately it’s still accessible, as per the example below
a = Dog() print(a._name)
Empty classes
In some cases we might want to create an empty class type, such as
class Duck: pass
If we need to then create the equivalent of a struct at runtime, then we can simply dynamically “add” variables like this
d = Duck() d.name = "Duck"
this leads us onto duck typing…
Duck typing
Duck typing allows us to declare Python type for use in other types that simple expect certain methods/variables to exist, for example
class Car: def __init__(self, engine): self.engine = engine def engine_size(self): return self.engine.size class EconomicalEngine: size = 1.0 class SportEngine: size = 3.5 car = Car(SportEngine()) print(car.engine_size())
In this example our Car has an engine but we do not define an Engine type, instead we simply state that the Car expects a type with a size variable. This does ofcourse means we could pass in anything with a size variable (which ofcourse might not always make much sense).
Static/class methods
Up until now we’ve seen instance methods on classes. Python also includes two decorators that can be used in conjunction with our methods to make them acts in a similar way to static methods.
The first of these is the decorator @staticmethod, for example
class Dog: @staticmethod def animal_type(): return "Dog"; # and we call the method like this print(Dog.animal_type()) # or d = Dog() print(d.animal_type())
Python also includes another static-like method type which is decorated with @classmethod. A class method differs from a static method in that it includes an implicit cls variable (by convention cls is used for the first argument). For example
class Dog: @classmethod def name(cls): return "English Springer Spaniel" # and we call the method like this print(Dog.name()) # or d = Dog() print(d.name())
So what’s the difference? Both appear to allow us to work in a “static” method way but the @classmethod allows us to still get an instance of the class, but it appears that this is almost like passing an instance of a new class into the method, i.e. these appear to be equivalent. Let’s assume we have this
class Dog: _name = "Dog" @classmethod def name(cls): print(cls) return cls._name d = Dog() d._name = "English Springer Spaniel" print(d.name) # same as print(Dog().name)
In the above we change the instance variable _name but both print statements will output “Dog” so appear functionally equivalent.