6.2. User-defined Types#

Most modern programming languages provide a way to define new data types through the use of classes.

A particular class essentially defines a new user-defined data type detailing how a particular kind of data is going to be structured and how it can be manipulated.


https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/CPT-OOP-objects_and_classes.svg/2560px-CPT-OOP-objects_and_classes.svg.png

Fig. 6.6 A class defines a new data type and an object is an instance of a class.#


A class therefore, at its essence, is a blueprint for creating objects (of that class). An object is an instance of a class, similar to how 7 is an instance of the int class and “Hello World!” is an instance of the str class.

6.2.1. class keyword#

Consider the concept of a Point in euclidean space.

In two dimensions, a point is two numbers (coordinates) that are treated collectively as a single object. In mathematical notation, points are often written in parentheses with a comma separating the coordinates. For example, (0, 0) represents the origin, and (x, y) represents the point \(x\) units to the right and \(y\) units up from the origin.

A natural way to represent a point in Python is with two numeric values. The question, then, is how to group these two values into a compound object. The quick and dirty solution is to use a list or tuple, and for some applications that might be the best choice.

An alternative is to define a new user-defined compound type, also called a class. This approach involves a bit more effort, but it has advantages that will be apparent soon.

A class definition looks like this:

class Point:
    """Represents a point in 2-D space."""
    pass

Class definitions can appear anywhere in a program, but they are usually near the beginning (after the import statements). The syntax rules for a class definition are the same as for other compound statements. There is a header which begins with the keyword, class, followed by the name of the class, and ending with a colon.

This definition creates a new class called Point. The pass statement has no effect; it is only necessary because a compound statement must have something in its body. A docstring could serve the same purpose:

class Point:
    "Point class for storing mathematical points."
    pass

By creating the Point class, we created a new type, also called Point. The members of this type are called instances of the type or objects. Creating a new instance is called instantiation, and is accomplished by calling the class. Classes, like functions, are callable, and we instantiate a Point object by calling the Point class:

type(Point)
type
p = Point()
type(p)
__main__.Point

The variable p is assigned a reference to a new Point object.

It may be helpful to think of a class as a factory for making objects, so our Point class is a factory for making points. The class itself isn’t an instance of a point, but it contains the machinary to make point instances.

6.2.2. Dot . operator#

The . operator is used to access the attributes and methods of a class.

The dot operator is the primary means of interaction with the objects of a class. It is used to:

  • access the values of attributes

  • assign values to attributes

  • modify attributes values

  • call the methods of the class

Examples of the use of the dot operator are given in following sections.



https://i.ibb.co/YbpDxmC/oop-hierarchy.png

Fig. 6.7 Class definitions in Python are comprised primarily of attributes and methods. There are two types of attributes: instance attributes and class attributes. There are three types of methods: instance methods, class methods, and static methods.#

6.2.3. Attributes#

Like real world objects, object instances have both form and function. The form consists of data elements contained within the instance. These data elements are called attributes (also known as variables or properties).

There are two types of data attributes in Python:

  1. Instance attributes / variables are attributes that are unique to each instance. They are defined inside the constructor method __init__ and are prefixed with self keyword.

  2. Class attributes / variables are attributes that have the same value for all class instances. We define class attributes outside all the methods, usually they are placed at the top, right below the class header.



6.2.3.1. Instance attributes#

The attributes of an object are also called instance variables because they are associated with a particular instance of the class. These variables are not shared by all instances of the class, and they are not defined until the instance is created.

We can add new data elements to an instance using dot operator:

p.x = 3
p.y = 4

This syntax is similar to the syntax for selecting a variable from a module, such as list.append or string.lower. In this case the attribute we are selecting is a data item from an instance.

The following state diagram shows the result of these assignments:



The variable p refers to a Point object, which contains two attributes. Each attribute refers to a number.

We can read the value of an attribute using the same syntax:

print(p.y)
x = p.x
print(x)
4
3

The expression p.x means, “Go to the object p refers to and get the value of x. In this case, we assign that value to a variable named x.

Note that there is no conflict between the variable x and the attribute x. The purpose of dot operator is to identify which variable you are referring to unambiguously.

You can use dot operator as part of any expression, so the following statements are legal:

print('(%d, %d)' % (p.x, p.y))
distance_squared = p.x * p.x + p.y * p.y
(3, 4)

The first line outputs (3, 4); the second line calculates the value 25.

Now that we have defined a new custom type, we can create instances of that type and manipulate them as we would an object of any native type. For example, you can pass objects of custom type as an argument to a function in the usual way:

def print_point(p):
    print('(%s, %s)' % (str(p.x), str(p.y)))

print_point takes a point as an argument and displays it in the standard format. If you call print_point(p) with point p as defined previously, the output is (3, 4).




6.2.3.2. Class Attributes#

Not all attributes are instance attributes. Some attributes are shared by all instances of a class. These attributes are called class attributes.

Class attributes are defined within the class definition but outside any of the class’s methods. They are not tied to any particular instance of the class, but rather are shared by all instances of the class.

For example, in the Point class, we could add a class attribute to keep track of how many points have been created. We’ll call this class attribute n:

class Point:
    n = 0
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        Point.n += 1

The n attribute is created when the Point class is defined, and it is accessed using dot operator. The following code demonstrates how the n attribute is used:

p1 = Point()
print(p1.n) # 1
p2 = Point()
print(p2.n) # 2

The n attribute is shared by all instances of the Point class. Any changes made to the n attribute are reflected in all instances of the class.




6.2.4. Methods#

The second part of the class definition is a suite of methods. A method is a function that is associated with a particular class . As examples below show, the syntax for defining a method is the same as for defining a function.


6.2.4.1. Instance Methods#

Instance methods are defined inside the body of the class definition. They are used to perform operations and manipulate the instance attributes of our objects. The vast majority of the methods in a class are instance methods.

The first input to all instance methods is self, which is a reference to the specific instance (object) of the class on which the method is called.

6.2.4.1.1. self#

The self parameter is a reference to the instance of the class. It is used to access variables that belong to the class. It does not have to be named self, you can call it whatever you like, but it has to be the first parameter of any function in the class.

When you create a new instance of the class, Python automatically determines what self is (the instance of the class) and passes it to the __init__ method.

6.2.4.1.2. __init__#

Since our Point class is intended to represent two dimensional mathematical points, ALL point instances ought to have x and y attributes, but that is not yet so with our Point objects.

p2 = Point()
p2.x
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Input In [8], in <cell line: 2>()
      1 p2 = Point()
----> 2 p2.x

AttributeError: 'Point' object has no attribute 'x'

To solve this problem we add an initialization method to our class.

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

A method behaves like a function but it is part of an object. Like a data attribute it is accessed using dot notation. The initialization method is called automatically when the class is called.

Let’s add another method, distance_from_origin, to see better how methods work:

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def distance_from_origin(self):
        return ((self.x ** 2) + (self.y ** 2)) ** 0.5

Let’s create a few point instances, look at their attributes, and call our new method on them:

>>> p = Point(3, 4)
>>> p.x
3
>>> p.y
4
>>> p.distance_from_origin()
5.0
>>> q = Point(5, 12)
>>> q.x
5
>>> q.y
12
>>> q.distance_from_origin()
13.0
>>> r = Point()
>>> r.x
0
>>> r.y
0
>>> r.distance_from_origin()
0.0

When defining a method, the first parameter refers to the instance being created. It is customary to name this parameter self. In the example session above, the self parameter refers to the instances p, q, and r respectively.

6.2.4.1.3. Getters and Setters#

In general it is considered bad practice to access and modify the attributes of an object directly.

Instead, it is better to use getter and setter methods to access and modify the attributes of an object.

A getter method is a method that returns the value of an attribute of an object. A setter method is a method that sets the value of an attribute of an object.

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def get_x(self):
        return self.x

    def set_x(self, x):
        self.x = x

    def get_y(self):
        return self.y

    def set_y(self, y):
        self.y = y

The get_x and get_y methods are getter methods for the x and y attributes respectively. The set_x and set_y methods are setter methods for the x and y attributes respectively.

The following code demonstrates how to use the getter and setter methods:

p = Point(3, 4)
print("p.x: ", p.get_x())
print("p.y: ", p.get_y())

p.set_x(100)
p.set_y(200)
print("p.x: ", p.get_x())
print("p.y: ", p.get_y())

6.2.4.2. Class Methods#

A class method is a method that is bound to the class and not the object of the class. They have the access to the state of the class as it takes a class parameter that points to the class and not the object instance.

They can modify a class state that would apply across all the instances of the class. For example it can modify a class variable that will be applicable to all the instances.

Class methods are defined using the @classmethod decorator. They take a cls parameter that points to the class and not the object instance.

class Point:
    n = 0
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        Point.n += 1

    @classmethod
    def get_n(cls):
        return cls.n

6.2.4.3. Static Methods#

Static methods are methods that are not bound to an instance of a class.

Static methods are defined using the @staticmethod decorator. They do NOT take the self parameter and can be called on the class itself.

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    @staticmethod
    def distance_from_origin(x, y):
        return ((x ** 2) + (y ** 2)) ** 0.5


6.2.5. Public, Protected and Private Access Levels#

In Python, there are 3 types of access levels:

  • Public attributes can be accessed by anyone.

  • Protected attributes are those attributes that follow the single underscore _ convention. These attributes should not be accessed directly. However, they can be accessed and modified in a derived class. Accessing protected attributes directly is not considered a good practice.

  • Private attributes are those attributes that follow the double underscore __ convention. These attributes should not be accessed directly. However, they can be accessed and modified in a derived class. Accessing private attributes directly is not allowed and will result in an AttributeError.

class Car:
    def __init__(self):
        self.__maxspeed = 200
        self._name = "Supercar"

    def drive(self):
        print('driving. maxspeed ' + str(self.__maxspeed))

car = Car()
car.drive()
print(car._name)
print(car.__maxspeed)
driving. maxspeed 200
Supercar
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Cell In[2], line 12

     10 car.drive()

     11 print(car._name)

---> 12 print(car.__maxspeed)



AttributeError: 'Car' object has no attribute '__maxspeed'