Low-Level of Class Creation

Beginner

This tutorial is from open-source community. Access the source code

Introduction

In this lab, you will learn about the low-level steps involved in creating a class in Python. Understanding how classes are constructed using the type() function provides deeper insight into Python's object-oriented features.

You will also implement custom class creation techniques. The files validate.py and structure.py will be modified during this lab, allowing you to apply your new knowledge in a practical setting.

Manual Class Creation

In Python programming, classes are a fundamental concept that allows you to group data and functions together. Usually, we define classes using the standard Python syntax. For example, here's a simple Stock class. This class represents a stock with attributes like name, shares, and price, and it has methods to calculate the cost and sell shares.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

But have you ever wondered how Python actually creates a class behind the scenes? What if we wanted to create this class without using the standard class syntax? In this section, we'll explore how Python classes are constructed at a lower level.

Launch Python Interactive Shell

To start experimenting with manual class creation, we need to open a Python interactive shell. This shell allows us to execute Python code line by line, which is great for learning and testing.

Open a terminal in WebIDE and start the Python interactive shell by typing the following commands. The first command cd ~/project changes the current directory to the project directory, and the second command python3 starts the Python 3 interactive shell.

cd ~/project
python3

Defining Methods as Regular Functions

Before we create a class manually, we need to define the methods that will be part of the class. In Python, methods are just functions that are associated with a class. So, let's define the methods we want in our class as regular Python functions.

def __init__(self, name, shares, price):
    self.name = name
    self.shares = shares
    self.price = price

def cost(self):
    return self.shares * self.price

def sell(self, nshares):
    self.shares -= nshares

Here, the __init__ function is a special method in Python classes. It's called a constructor, and it's used to initialize the object's attributes when an instance of the class is created. The cost method calculates the total cost of the shares, and the sell method reduces the number of shares.

Creating a Methods Dictionary

Now that we have defined our methods as regular functions, we need to organize them in a way that Python can understand when creating the class. We do this by creating a dictionary that will contain all the methods for our class.

methods = {
    '__init__': __init__,
    'cost': cost,
    'sell': sell
}

In this dictionary, the keys are the names of the methods as they will be used in the class, and the values are the actual function objects we defined earlier.

Using type() Constructor to Create a Class

In Python, the type() function is a built - in function that can be used to create classes at a lower level. The type() function takes three arguments:

  1. The name of the class: This is a string that represents the name of the class we want to create.
  2. A tuple of base classes: In Python, classes can inherit from other classes. Here, we use (object,) which means our class inherits from the base object class, which is the base class for all classes in Python.
  3. A dictionary containing methods and attributes: This is the dictionary we created earlier that holds all the methods of our class.
Stock = type('Stock', (object,), methods)

This line of code creates a new class named Stock using the type() function. The class inherits from the object class and has the methods defined in the methods dictionary.

Testing Our Manually Created Class

Now that we have created our class manually, let's test it to make sure it works as expected. We'll create an instance of our new class and call its methods.

s = Stock('GOOG', 100, 490.10)
print(s.name)
print(s.cost())
s.sell(25)
print(s.shares)

In the first line, we create an instance of the Stock class with the name GOOG, 100 shares, and a price of 490.10. Then we print the name of the stock, calculate and print the cost, sell 25 shares, and finally print the remaining number of shares.

You should see the following output:

GOOG
49010.0
75

This output shows that our manually created class works just like a class created using the standard Python syntax. It demonstrates that a class is fundamentally just a name, a tuple of base classes, and a dictionary of methods and attributes. The type() function simply constructs a class object from these components.

Exit the Python shell when you're done:

exit()

Creating a Typed Structure Helper

In this step, we're going to build a more practical example. We'll implement a function that creates classes with type validation. Type validation is crucial as it ensures that the data assigned to class attributes meets specific criteria, like being a certain data type or within a particular range. This helps catch errors early and makes our code more robust.

Understanding the Structure Class

First, we need to open the structure.py file in the WebIDE editor. This file contains a basic Structure class. This class provides the fundamental functionality for initializing and representing structured objects. Initialization means setting up the object with the provided data, and representation is about how the object is displayed when we print it.

To open the file, we'll use the following command in the terminal:

cd ~/project

After running this command, you'll be in the correct directory where the structure.py file is located. When you open the file, you'll notice the basic Structure class. Our goal is to extend this class to support type validation.

Implementing the typed_structure Function

Now, let's add the typed_structure function to the structure.py file. This function will create a new class that inherits from the Structure class and includes the specified validators. Inheritance means that the new class will have all the functionality of the Structure class and can also add its own features. Validators are used to check if the values assigned to the class attributes are valid.

Here's the code for the typed_structure function:

def typed_structure(clsname, **validators):
    """
    Create a Structure class with type validation.

    Parameters:
    - clsname: Name of the class to create
    - validators: Keyword arguments mapping attribute names to validator objects

    Returns:
    - A new class with the specified name and validators
    """
    cls = type(clsname, (Structure,), validators)
    return cls

The clsname parameter is the name we want to give to the new class. The validators parameter is a dictionary where the keys are the attribute names and the values are the validator objects. The type() function is used to create a new class dynamically. It takes three arguments: the class name, a tuple of base classes (in this case, just the Structure class), and a dictionary of class attributes (the validators).

After adding this function, your structure.py file should look like this:

## Structure class definition

class Structure:
    _fields = ()

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError(f'Expected {len(self._fields)} arguments')

        ## Set all of the positional arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        ## Set the remaining keyword arguments
        for name, value in kwargs.items():
            setattr(self, name, value)

    def __repr__(self):
        attrs = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({attrs})'

def typed_structure(clsname, **validators):
    """
    Create a Structure class with type validation.

    Parameters:
    - clsname: Name of the class to create
    - validators: Keyword arguments mapping attribute names to validator objects

    Returns:
    - A new class with the specified name and validators
    """
    cls = type(clsname, (Structure,), validators)
    return cls

Testing the typed_structure Function

Let's test our typed_structure function using the validators from the validate.py file. These validators are used to check if the values assigned to the class attributes are of the correct type and meet other criteria.

First, open a Python interactive shell. We'll use the following commands in the terminal:

cd ~/project
python3

The first command takes us to the correct directory, and the second command starts the Python interactive shell.

Now, import the necessary components and create a typed structure:

from validate import String, PositiveInteger, PositiveFloat
from structure import typed_structure

## Create a Stock class with type validation
Stock = typed_structure('Stock', name=String(), shares=PositiveInteger(), price=PositiveFloat())

## Create a stock instance
s = Stock('GOOG', 100, 490.1)

## Test the instance
print(s.name)
print(s)

## Test validation
try:
    invalid_stock = Stock('AAPL', -10, 150.25)  ## Should raise an error
except ValueError as e:
    print(f"Validation error: {e}")

We import the String, PositiveInteger, and PositiveFloat validators from the validate.py file. Then we use the typed_structure function to create a Stock class with type validation. We create an instance of the Stock class and test it by printing its attributes. Finally, we try to create an invalid stock instance to test the validation.

You should see output similar to:

GOOG
Stock('GOOG', 100, 490.1)
Validation error: Expected a positive value

When you're done testing, exit the Python shell:

exit()

This example demonstrates how we can use the type() function to create custom classes with specific validation rules. This approach is very powerful as it allows us to generate classes programmatically, which can save a lot of time and make our code more flexible.

Efficient Class Generation

Now that you understand how to create classes using the type() function, we're going to explore a more efficient way to generate multiple similar classes. This method will save you time and reduce code duplication, making your programming process smoother.

Understanding the Current Validator Classes

First, we need to open the validate.py file in the WebIDE. This file already contains several validator classes, which are used to check if values meet certain conditions. These classes include Validator, Positive, PositiveInteger, and PositiveFloat. We'll be adding a Typed base class and several type - specific validators to this file.

To open the file, run the following command in the terminal:

cd ~/project

Adding the Typed Validator Class

Let's start by adding the Typed validator class. This class will be used to check if a value is of the expected type.

class Typed(Validator):
    expected_type = object  ## Default, will be overridden in subclasses

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        super().check(value)

In this code, expected_type is set to object by default. Subclasses will override this with the specific type they are checking for. The check method uses the isinstance function to check if the value is of the expected type. If not, it raises a TypeError.

Traditionally, we would create type - specific validators like this:

class Integer(Typed):
    expected_type = int

class Float(Typed):
    expected_type = float

class String(Typed):
    expected_type = str

However, this approach is repetitive. We can do better by using the type() constructor to generate these classes dynamically.

Generating Type Validators Dynamically

We'll replace the individual class definitions with a more efficient approach.

_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str)
]

globals().update((name, type(name, (Typed,), {'expected_type': ty}))
                 for name, ty in _typed_classes)

Here's what this code does:

  1. It defines a list of tuples. Each tuple contains a class name and the corresponding Python type.
  2. It uses a generator expression with the type() function to create each class. The type() function takes three arguments: the class name, a tuple of base classes, and a dictionary of class attributes.
  3. It uses globals().update() to add the newly created classes to the global namespace. This makes the classes accessible throughout the module.

Your completed validate.py file should look something like this:

## Basic validator classes

class Validator:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        self.check(value)
        instance.__dict__[self.name] = value

    @classmethod
    def check(cls, value):
        pass

class Positive(Validator):
    @classmethod
    def check(cls, value):
        if value <= 0:
            raise ValueError('Expected a positive value')
        super().check(value)

class PositiveInteger(Positive):
    @classmethod
    def check(cls, value):
        if not isinstance(value, int):
            raise TypeError('Expected an integer')
        super().check(value)

class PositiveFloat(Positive):
    @classmethod
    def check(cls, value):
        if not isinstance(value, float):
            raise TypeError('Expected a float')
        super().check(value)

class Typed(Validator):
    expected_type = object  ## Default, will be overridden in subclasses

    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        super().check(value)

## Generate type validators dynamically
_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str)
]

globals().update((name, type(name, (Typed,), {'expected_type': ty}))
                 for name, ty in _typed_classes)

Testing the Dynamically Generated Classes

Now, let's test our dynamically generated validator classes. First, open a Python interactive shell.

cd ~/project
python3

Once you're in the Python shell, import and test our validators.

from validate import Integer, Float, String

## Test the Integer validator
i = Integer()
i.__set_name__(None, 'test_int')
try:
    i.check("not an integer")
    print("Error: Check passed when it should have failed")
except TypeError as e:
    print(f"Integer validation: {e}")

## Test the String validator
s = String()
s.__set_name__(None, 'test_str')
try:
    s.check(123)
    print("Error: Check passed when it should have failed")
except TypeError as e:
    print(f"String validation: {e}")

## Add a new validator class to the list
import sys
print("Current validator classes:", [cls for cls in dir() if cls in ['Integer', 'Float', 'String']])

You should see output showing the type validation errors. This indicates that our dynamically generated classes are working correctly.

When you're done testing, exit the Python shell:

exit()

Expanding the Dynamic Class Generation

If you want to add more type validators, you can simply update the _typed_classes list in validate.py.

_typed_classes = [
    ('Integer', int),
    ('Float', float),
    ('String', str),
    ('List', list),
    ('Dict', dict),
    ('Bool', bool)
]

This approach provides a powerful and efficient way to generate multiple similar classes without writing repetitive code. It allows you to easily scale your application as your requirements grow.

Summary

In this lab, you have learned about the low - level mechanisms of class creation in Python. First, you mastered how to manually create a class using the type() constructor, which requires a class name, a tuple of base classes, and a dictionary of methods. Second, you implemented a typed_structure function to dynamically create classes with validation capabilities.

Moreover, you used the type() constructor along with globals().update() to efficiently generate multiple similar classes, thus avoiding repetitive code. These techniques offer powerful ways to programmatically create and customize classes, useful in frameworks, libraries, and metaprogramming. Understanding these underlying mechanisms deepens your insight into Python's object - oriented features and enables more advanced programming.