Classes and Objects

In Python, objects are everywhere, in fact, primitive types like integers are also objects. Let's now learn the anatomy of these objects and how to make your own.

This record is a part of the materials for Krita Scripting Series. Before reading further, make sure to study materials published before.

In Python, objects are everywhere, in fact, primitive types like integers are also objects. Let’s now learn the anatomy of these objects and how to make your own.

Try to run this code in your scripter

from pprint import pprint
a = 10

pprint(dir(a))

You should get input like this

"""
['__abs__',
 '__add__',
 '__and__',
 '__bool__',
 '__ceil__',
...
 'from_bytes',
 'imag',
 'numerator',
 'real',
 'to_bytes']
"""

These are the components that represent the integer type in Python.

Btw, the dir function is a helper function to inspect the object to learn what it consists of.

The anatomy of class

There are 4 major aspect of class that are important to know:

  • Constructor
  • Destructor
  • Member/Field
  • Method

Before we go into details of each of each of these aspects, lets bring up the example of Color class we mentioned in previous records.

class Color:
    def __init__(self, r, g, b):
        self.r = r
        self.g = g
        self.b = b

    def normalized(self):
        rn = self.r / 255
        rg = self.g / 255
        rb = self.b / 255

        return rn, rg, rb

    def __repr__(self):
        return f"({self.r}, {self.g}, {self.b})"

Reference to self

The self is a special kind of variable that specifies the reference to current object. This is how the class level scope is achieved. When working with classes, the self is all over the place.

Constructor

Constructor is a special kind of method that is responsible for object creation. It is called when we run the code

color = Color()

We define the constructor by adding __init__ method to the class (line 2).

You should notice that some methods have double underscore around them. These are reserved method by the Python. You are not supposed to call these method yourself, the Python will refer to these methods whenever it is needed. Like in case of __repr__, when we print the instance of Color class the python will try to convert it into a string, and the method it will use is __repr__.

So by using __init__, we are telling the Python, please, when you make an instance of Color class, use this method to create it.

Destructor

The destructor is the special kind of method that is called when the object is removed from the memory. In our case we are not likely come to use it since the object removal is done by the Python behind the curtains using Garbage Collector, fancy algorithm to optimize memory usage. You do not need to concern yourself with it. However, if you curious, you may learn more about it.

Member/Field

Every class may or may not have members. Essentially, the class members are variables that defined for the class. The members of the class define the object state. By changing the values of its members you change the object state. The members can be changed directly or by calling object’s methods. It is recommended to define all the members during the object creation (i.e. in the __init__ method), even though you are able to create members at any point.

In the code snippet above, the member of the class Color are defined in the line 3, 4, and 5.

self.r = r
self.g = g
self.b = b

To define the member of the class we use the reference self with the dot and then the name of the member.

Method

The method is the function that is tied to the class. Whenever we call a method it means we send a signal to an object and want the object either to change state or to give us a response from the object. Both things altogether are also the case.

def normalized(self):
    rn = self.r / 255
    rg = self.g / 255
    rb = self.b / 255

When defining methods, the first argument has to always be self. This way we tell Python that the this function is part of an object and it should have access to its members and other methods directly.

Notice that in the normalized() method we defined three variables which we return right away. We do this intentionally to highlight the notion of class scope. Remember as we talked about global and local scope. The variables rn, rg, and rb are define in the local scope of the normalized() method and will be available only inside this method. The variables self.r, self.g and self.b are defined in the class scope and will be available in all methods that belong to this class. They also will be available outside of the class, however only in the scope that they have been defined. So being able to access the member of the class outside of the class not necessarily makes it global scope.

Extended example of the Color class

Let’s consider more complicated example. What if set the value of one of the channels to the say 7000. It is still valid operation from the programming stand point. However, it is not valid case for the color.

To make sure that our value as in valid range we can do two things. First, make the fields of the class private. Second, define the setters and getters methods to access the color.

class Color:
    def __init__(self, r, g, b):
        self.set_rgb(r, g, b)

    def normalized(self):
        rn = self._r / 255
        rg = self._g / 255
        rb = self._b / 255

        return rn, rg, rb

    def set_r(self, value):
        if value < 0:
            v = 0
        elif value > 255:
            v = 255
        else:
            v = value

        self._r = v
    
    def set_g(self, value):
        if value < 0:
            v = 0
        elif value > 255:
            v = 255
        else:
            v = value

        self._g = v
    
    def set_b(self, value):
        if value < 0:
            v = 0
        elif value > 255:
            v = 255
        else:
            v = value

        self._b = v

    def set_rgb(self, r, g, b):
        self.set_r(r)
        self.set_g(g)
        self.set_b(b)

    def __repr__(self):
        return f"({self._r}, {self._g}, {self._b})"


red = Color(1000, 0, -1100)
print(red)

Notice how we define the setter function for each color component separately. Each of the setters has a conditional structure to limit the value of the color between 0 and 255.

Once we went through the trouble of making these methods, we can reuse them to make a composite method to set the values all at once (line 42), or even use in constructor while creating the object (line 3).

The members self.r, self.g and self.b are now have underscore before them. This is the way of “agreeing” between the developers that these members are not supposed to be called outside of the class definition. But they can be used inside the class definition.

You still can access the members by calling self._r, however by changing the value directly you my break the integrity of the object. Like in our case we should control the range of each color component.

The setters from the code snippet above can be significantly reduced if we use built-in min and max functions. We can also return normalized color in one line.

class Color:
    def __init__(self, r: int, g: int, b: int):
        """Represent a color class

        Args:
            r (int): The value for the red component in the range [0, 255]
            g (int): The value for the green component in the range [0, 255]
            b (int): The value for the blue component in the range [0, 255]
        """
        self.set_rgb(r, g, b)

    def normalized(self):
        return self._r / 255, self._g / 255, self._b / 255

    def set_r(self, value):
        self._r = max(0, min(255, value))

    def set_g(self, value):
        self._g = max(0, min(255, value))

    def set_b(self, value):
        self._b = max(0, min(255, value))

    def set_rgb(self, r, g, b):
        self.set_r(r)
        self.set_g(g)
        self.set_b(b)

    def __repr__(self):
        return f"({self._r}, {self._g}, {self._b})"


red = Color(1000, 0, -1100)
print(red)

Here we add a little touch. We can specify type hint for the input arguments by using column symbol and add a docstring. The docstring is a good way to add description to the method to help your future self (or someone who will read the code) to learn what the method is supposed to do. Same docstring can be applied to a function.

We will learn how to generate the template for the docstring once we learn how to use IDE.

Inheritance

We are now equipped with the knowledge to approach one of the key concepts of Object Oriented Programming – inheritance.

Inheritance is usually explained by hierarchies. In a biological world the children usually inherit some of the feature of their parents like eye color or hair color.

In a programming world we can define a class with particular methods and members, then we can use inheritance to expand on this class and create our own variation.

class Pet:
    def __init__(self, name: str) -> None:
        self._name = name

    def name(self):
        return self._name

    def play(self):
        print(f"The pet {self._name} engaged in the play.")


class Cat(Pet):
    def __init__(self, name: str) -> None:
        super().__init__(name)

    def meow(self):
        print("The cat says Meow!")


class Dog(Pet):
    def __init__(self, name: str) -> None:
        super().__init__(name)

    def woof(self):
        print("The dog says Woof!")


bolt = Dog("Bolt")
garfield = Cat("Garfield")

print("Dog name:", bolt.name())
print("Cat name:", garfield.name())
bolt.woof()
garfield.meow()

We define the Pet class as a starting point. It will serve as as a base class. Next, we define the classes for Dog and Cat. To define the children class we need to specify the parent class in parenthesis after the class name (lines 12 and 20)

Notice that the constructor for the Pet class contains only a member name. However, the constructors for Cat and Dog do not. Also the Cat and Dog constructors have and interesting call super(). By calling this function, we communicate the Python that we want to access the the parent class. Then we call the __init__ method and pass the name. We mentioned that you are not expected to call the method that start with double underscores, however, in this case it is a legitimate operation.

The children classes have almost nothing added except meow and woof methods. All other members and methods are shared due to inheritance from Pet class.

In the lines 31 and 32 we print the names of the pets. In both cases we use the method name(). In the class Cat and Dog this method is not defined. This method is defined in the parent class. Notice how we were able to get different names. It is because we pass different names to the constructor.

Try to call meow on bolt variable and see the difference.

By using inheritance we can simplify the implementation of set of classes and reduce the amount of duplicated code. The art of creating classes and relationships between them is called Object Oriented Design. And the set of existing blueprints is called Design Patterns. For simple script you will probably not need these fancy words, though, you might need them once you start making your own plugins that will take up more than couple of thousand line of code.

Fake Action Class

It can be an overhead going through all of this trouble to describe how the classes work. The goal is to give you a general idea how the objects operate so then you will be more comfortable with exploring the ins and outs of API.

Krita Python API is not that advance and the documentation is scarce. Understanding these concept will help you find how to use something that is not mentioned in the documentation.

Remember the QAction class from before? And how we found out the name of the action we need to use with app.action()?

The class for action could look like this

class Action:
    def __init__(self, text: str) -> None:
        self._text = text

    def text(self):
        return self._text
    
act = Action("Fill Foreground Color")
print(act.text())

Here we omit all other members and methods for clarity.

We can get the text of the action, but not the name. Remember how we were able to find the method objectName() by referencing to the parent class?

Study the page for QAction class. We are interested in the table on the top of the page. Notice that the class QAction inherits QObject. Follow the link of the QObject class and look for objectName() method. This is how we knew that we can call this method on the action instance.

If we were to simulate the hierarchy of classes it would look something like this

class Object:
    def __init__(self, name: str) -> None:
        self._name = name

    def objectName(self):
        return self._name


class Action(Object):
    def __init__(self, text: str, name: str) -> None:
        super().__init__(name)
        self._text = text

    def text(self):
        return self._text


act = Action("Fill Foreground Color", "fill_foreground_color")
print(act.text())
print(act.objectName())

Next

We are now ready to learn about Krita components to actually do scripting.

Feedback and Suggestions

Should you have any questions or maybe you want to suggest next topic to cover. Please join our community and let us know.