Skip to content

Advanced Class

  • Class and Static Method
  • Class Attribute
  • Data Class
  • Dynamic Attribute
  • Typing

Class and Static Method

By convention, the first parameter of a method is self. It points to the newly created instance. In runtime, Python interpreter provides the argument. These methods are called instance methods.

Python has two other types of methods:

  • Class method: the first parameter is cls that points to the current class type. In runtime, Python interpreter provides the cls argument. Class methods are used to modify class attributes that will be introduced later.
  • Static method: Python interpreter doesn't provide any argument. These methods work like functions because they cannot change any class or instance attributes. They are defined in the class namespace.

Class Method

Python uses the @classmethod decorator to specify a class method. A decorator is a Python programming constructor that provide addition information or functions for a class, a function, a class attribute or a method.

You can call a class method without an instance using the ClassName.method() syntax. You can also call it with a class instance. Python interpreter provides the cls argument for both types of call.

class MyClass:

    @classmethod
    def call_method(cls):
        print(f"class method of {cls}")

MyClass.call_method() # class method of <class '__main__.MyClass'>

my = MyClass() 
my.call_method() # class method of <class '__main__.MyClass'>

Static Method

Python uses the @staticmethod decorator to specify a static method.

You can call a class method without an instance using the ClassName.method() syntax. You can also call it with a class instance. Python interpreter provides no argument for both types of call.

class MyClass:

    @staticmethod
    def call_method(number):
        print(f"call static method with argument {number}")

MyClass.call_method(42) # call static method with argument 42

my = MyClass()
my.call_method(42) # call static method with argument 42

Class Attribute

A class is a blueprint for its instances.

A class is an object in Python that can have its own attributes.

A class attribute is defined directly in a class body.

A class attribute can be access using both class name and the instances of the class.

If you write an attribute using an instance, it creates an instance attribute.

class MyClass:
    class_count = 1

    def __init__(self, count):
        self.instance_count = count

print(MyClass.class_count) # 1

MyClass.class_count += 1
print(MyClass.class_count) # 2

my = MyClass(42)
print(my.class_count) # 2
print(my.instance_count) # 42

# this creates a new instance attributed named class_count
my.class_count = 7 
print(my.class_count) # 7

# class attribute is still there
print(MyClass.class_count) # 2

Why Use Class Attribute

Python developers use class attributes in two common cases:

  • with descriptor (to be introduced later), to define instance attributes with some processing logic.
  • To store class level data that is not associated with an instance.

For example, keep a count of number of instances created. To avoid repeating the class name, you can use the special attribute self.__class__ to access its class object. In Python, identifiers start and end with double underscore are special identifiers.

class MyClass:
    count = 0

    def __init__(self, number):
        self.__class__.count += 1
        self.number = number

first = MyClass(5)
second = MyClass(7)
print(MyClass.count) # 2

Data Class

A data class consists of a collection of attributes. The purpose is to make it easy to use the composite data and its parts.

Python provides three help classes for building data classes.

  • collections.namedtuple: a function to build composite data with named attributes.
  • typing.NamedTuple: a class used to build composite data with typed and named attributes.
  • dataclasses.dataclass: a decorator used to build data classes.

Why Data Class

There are a lot of work to do to define a composite data class because a simple definition is not good enough.

  • The representation string is useless
  • The == uses the default identity equality that is incorrect here.
class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

point1 = Point(3, 5)
point2 = Point(3, 5)
print(point1) # <__main__.Point object at 0x1168c8390>
print(point1 == point2) # False

collections.namedtuple

The namedtuple is a simple solution to fix the simple class issues. You can use from module import name syntax to import an identifier from the module.

from collections import namedtuple

Point = namedtuple("Point", "x y")

point1 = Point(3, 5)
point2 = Point(3, 5)
print(point1) # Point(x=3, y=5)
print(point1 == point2) # True

typing.NamedTuple

The NamedTuple provides type hint to a data class. It works similar as namedtuple. In an IDE, it provides the type hints of data components. However, it doesn't enforce the data type in the runtime.

from typing import NamedTuple

class Point(NamedTuple):
    x: float = 0
    y: float = 0

point1 = Point(3, 5)
point2 = Point(3, 5)
print(point1) # Point(x=3, y=5)
print(point1 == point2) # True

dataclasses.dataclass

The dataclass is a decorator for building mutable or immutable data class.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

point1 = Point(3, 5)
point2 = Point(3, 5)
print(point1) # Point(x=3, y=5)
print(point1 == point2) # True

Which One to Use

  • Simple immutable composite data: namedtuple
  • Simple with type hints: NamedTuple
  • Rich function or mutable composite data: dataclass

Dynamic Attribute

In Python, both data attributes and methods of a class are collectively called as attributes.

Methods are different from data attributes because they are callable using a so-call function call syntax obj.method().

Dynamic attributes are calculated but are accessed as regular data attributes.

Python provides @property decorator to define the read and write operations for dynamic attributes.

class Person:

    def __init__(self, firstname, lastname):
        self.firstname = firstname
        self.lastname = lastname

    @property
    def fullname(self):
        return self.firstname + " " + self.lastname

    @fullname.setter
    def fullname(self, full):
        self.firstname, self.lastname = full.split()

tom = Person("Tom", "Hank")
print(tom.fullname) # Tom Hank

tom.fullname = "Tommy Hanks"
print(tom.firstname, tom.lastname) # Tommy Hanks   

Other Dynamic Attributes

When you want to define some logics behind an attribute that can be used in multiple places, you can define the so-called descriptor classes that control the read, write, and delete of class attributes.

Additionally, special class methods such as __getattr__ and __setattr__ can also customize the attribute access behaviors.

These advanced features are introduced in the Python data model section.

Typing

The type of a value defines the set of valid operations for the value.

Typing is the mechanism of a programming language for type definition and type checking.

A class defines the type of its instances. Its methods define its data operations.

Therefore, class and class inheritance define the valid operations of an instance.

Dynamic Typing vs Static Typing

Because there is no separate compiling stage for a Python program, Python is categorized as a dynamic typing program language. A dynamic typing language checks data type and operations at runtime.

Programming languages such as C, C++, Java and C# can check types and operations when they compile source code, they are called static typing program language. These language can find many type errors during the compiling phase.

Since Python 3.5, Python introduces type hints that allow external type checker tools to check type and operations before the code execution. It helps to improve the code quality by finding out invalid daa operations such as dividing a string by a number.

Duck Typing and Goose Typing

As a dynamic typing program language, Python supports two run-time typing mechanism:

  • duck typing: just run the operation, throw an exception if the value doesn't support it.
  • goose typing: use isinstance to check the instance type, then run the corresponding operations.

Duck Typing Example

Python supports duck typing from the beginning. It got the name from the so-called duck test: " if it quacks like a duck, then it is a duck".

For example, all Python objects have a special method __str__, either inherit from object or defined in their classes. The method creates a string representation of the object, Therefore, you can call print(obj) for any object. print just call the obj.__str__() and write the string.

The duck typing allows you to apply an operation on different types as long as they define a common method.

Following is an example:

class Duck:
    def quack(self):
        print("quack, quack")

class Player:
    def quack(self):
        print("quack like a duck")


actors = [Duck(), Player()]
for actor in actors:
    actor.quack()

Goose Typing Example

In goose typing, you check the type of an instance and call the corresponding operations.

class Duck:
    def quack(self):
        print("quack, quack")

class Dog:
    def bark(self):
        print("bark, bark")

actors = [Duck(), Dog()]

for actor in actors:
    if isinstance(actor, Duck):
        actor.quack()
    elif isinstance(actor, Dog):
        actor.bark()
    else:
        print("oops")

Abstract Base Classes

To support goose typing, Python defines many abstract base classes in its collection.abc module. Here the word abstract means that you don't create an instance for the classes. They are used for goose typing purpose.

The supported operations are in collection.abc document.

For example, if a class defines the special __len__ method, then you can call len function on all its instances.

import collections.abc

# return None if the object doesn't support len() operation
def get_len_safe(my_obj):
    size = None
    if isinstance(my_obj, collections.abc.Sized):
        size = len(my_obj)

    return size