Published on

Python Classes

Authors

Python supports the object-oriented programming (OOP) paradigm through classes. They provide an elegant way to define reusable pieces of code that encapsulate data and behavior in a single entity. With classes, you can quickly and intuitively model real-world objects and solve complex problems.

Table of Contents

  1. Defining a Class in Python
  2. Class Inheritance
  3. @staticmethod and @classmethod decorators
  4. How to built your decorator
  5. How to built decorators with params for classes

Defining a Class in Python

In python everything is an object, including classes. Data Classes are the building blocks of a Data Model. Within each Data Class lies several Data Elements and these are the descriptions of an individual field, variable, column or property. They also can have functions called methods which are used to manipulate the data within the class.

You can also have a Data Class within a Data Class, known as a Nested Data Class, which can be a useful way of managing complex sets of data. There is no limit on the number of Nested Data Classes you can include.

class Device:
    def __init__(self, name, connected_by):
        self.name = name
        self.connected_by = connected_by
        self.connected = True

    def __str__(self):
        return f"Device {self.name!r} ({self.connected_by})"

    def __repr__(self):
        return f"Device: ({self.name})"

    def disconnect(self):
        self.connected = False
        print(f"{self.name} disconnected.")


def main():
    printer = Device("Printer", "USB")

    print(printer) # Device 'Printer' (USB)
    print(repr(printer)) # Device: (Printer)

if __name__ == "__main__":
    main()

__init__(): This is the constructor method. It is called when an object is created from the class and it allows the class to initialize the attributes of the class.

__str__(): This is the string representation method. It is called when the object is passed to the print() function or when str() is called on the object. It allows the class to define a string representation of the object.

__repr__(): This is the object representation method. It is called when the object is passed to the repr() function. It allows the class to define an object representation of the object.

disconnect(): This is a method that allows the device to disconnect from the network.

Class Inheritance

Inheritance allows us to define a class that inherits all the methods and properties from another class.

  • Parent class is the class being inherited from, also called base class.
  • Child class is the class that inherits from another class, also called derived class.
...

class Printer(Device):
    def __init__(self, name, connected_by, capacity):
        super().__init__(name, connected_by)
        self.capacity = capacity
        self.remaining_pages = capacity

    def __str__(self):
        return f"{super().__str__()} ({self.remaining_pages} pages remaining)"

    def print(self, pages):
        if not self.connected:
            raise TypeError("Device is disconnected at this time, cannot print.")
        print(f"Printing {pages} pages.")
        self.remaining_pages -= pages

def main():
    printer = Printer("Printer", "USB", 500) # output: Device 'Printer' (USB) (500 pages remaining)

    printer.print(20)                        # output: Printing 20 pages.
    print(printer)                           # output: Device 'Printer' (USB) (480 pages remaining)

    printer.print(50)                        # output: Printing 50 pages.
    print(printer)                           # output: Device 'Printer' (USB) (430 pages remaining)

    printer.disconnect()                     # output: Printer disconnected.
    printer.print(30)                        # output: Error: Device is disconnected at this time, cannot print.

Device is a Parent class. A device has a name and it can be connected by either USB or Ethernet. It can also be connected or disconnected. It is a base class for other devices.

Printer is a device or a Child class of the Device parent class. Also has a name, can be connected by either USB or Ethernet, and also can be connected or disconnected. However, has an additional parameter, an original capacity of printing a specific amount of pages. When we call the method print, the device prints the number of pages and reduces from the total capacity of printing. It is a derived class from the Device parent class.

@staticmethod and @classmethod Decorators

A decorator is a function that takes another function as input, extends its behavior, and returns a new function as output. This is possible because, in Python, functions are first-class objects, which means they can be passed as arguments to functions and also be returned from functions, just as other types of objects such as string, int, or float. A decorator can be used to decorate a function or a class.

@staticmethod, @classmethod and @property are “magical” decorators and can become very handy for our development work and make your code more clean.

  1. @staticmethod: a static method is a method that does not require the creation of an instance of a class. For Python, it means that the first argument of a static method is not self, but a regular positional or keyword argument. Also, a static method can have no arguments at all. Static methods are used to just place a method inside a class because you feel it belongs there (i.e. for code organisation, mostly!)
class Cellphone:
    def __init__(self, brand, number):
        self.brand = brand
        self.number = number

    @property
    def number(self):
        _number = "-".join([self.number[:3], self.number[3:6], self.number[6:]])
        return _number

    @staticmethod
    def get_emergency_number():
        return "911"

    def main() -> None:
        Cellphone.get_emergency_number()  # '911'
  1. @classmethod a class method is created with the @classmethod decorator and requires the class itself as the first argument, which is written as cls. A class method normally works as a factory method and returns an instance of the class with supplied arguments. However, it doesn't always have to work as a factory class and return an instance. You can create an instance in the class method, do whatever you need, and don’t have to return it.
class Cellphone:
    def __init__(self, brand, number):
        self.brand = brand
        self.number = number

    @property
    def number(self):
        _number = "-".join([self.number[:3], self.number[3:6], self.number[6:]])
        return _number

    @staticmethod
    def get_emergency_number():
        return "911"

    @classmethod
    def iphone(cls, number):
        _iphone = cls("Apple", number)
        print("An iPhone is created.")
        return _iphone

def main() -> None:
    iphone = Cellphone.iphone("1112223333") # An iPhone is created.

    iphone.number()                     # "111-222-3333"
    iphone.get_emergency_number()       # "911"
    iphone.name                         # "Apple"
class CrawlSpider(Spider):
    rules: Sequence[Rule] = ()

    ...
    @classmethod
    def from_crawler(cls, crawler, *args, **kwargs):
        spider = super().from_crawler(crawler, *args, **kwargs)
        spider._follow_links = crawler.settings.getbool(
            'CRAWLSPIDER_FOLLOW_LINKS', True)
        return spider
  1. @property is the decorator that turns a specific function to return a property of the class object. You cannot change a property unless you setter conditions to change a property.
class Cellphone:
    def __init__(self, brand, number):
        self.brand = brand
        self.number = number

    @property
    def number(self):
        _number = "-".join([self.number[:3], self.number[3:6], self.number[6:]])
        return _number

    @number.setter
    def number(self, number):
        if len(number) != 10:
            raise ValueError("Invalid phone number.")
        self._number = number

    ....

def main()-> None:
    cellphone = Cellphone("Samsung", "1112223333")
    print(cellphone.number)          # 111-222-3333

How to built your decorator

A decorator is a function that returns another function if pre-set of conditions applies. We can build a safety decorator such as @login_required decorator from the Flask-Login package.

from typing import Callable, Any

def get_admin_password():
    return "1234"

def make_secure(func: Callable[[Any], Any]) -> Callable[[Any], Any]:
    def secure_function():
        if user["access_level"] == "admin":
            return func()
        else:
            return f"No admin permissions for {user['username']}."

    return secure_function

# `get_admin_password` is now `secure_func` from below
get_admin_password = make_secure(get_admin_password)

user = {"username": "jose", "access_level": "guest"}
print(get_admin_password()) # No admin permissions for jose.

user = {"username": "bob", "access_level": "admin"}
print(get_admin_password()) # 1234

How to built decorators with params for classes

import functools

# create make_secure decorator for classes
def make_secure(func):
    @functools.wraps(func)
    def secure_function(*args, **kwargs):
        if user["access_level"] == "admin":
            return func(*args, **kwargs)
        else:
            return f"No admin permissions for {user['username']}."

    return secure_function


@make_secure
def get_password(role: str):
    if role == "admin":
        return "1234"
    elif role == "billing":
        return "super_secure_password"


user = {"username": "jose", "access_level": "guest"}
print(get_password("admin"))    # No admin permissions for jose.
print(get_password("billing"))  # No admin permissions for jose.

user = {"username": "bob", "access_level": "admin"}
print(get_password("admin"))   # 1234
print(get_password("billing")) # super_secure_password.

References: