Classes

A class is a programmer-defined data type. Python and its external packages contain many useful data types, but classes allow a programmer to create custom data types that are tailored to specific use-cases. Classes can contain functions, called methods as well as variables, called attributes. Classes are defined using the class keyword.

In this lesson, we will create a simple class called circle. Objects of this data type are intended to represent geometric circles. We will define our circle class in steps. At each step, our class will become slightly more complex.

Circle Class: Version 1

We will start will a very basic version that contains three attributes, r, x, and y. The attribute r is intended to store the radius of the circle, while x and y store the coordinates of the circle’s center point. Our class will also contain a single method, called area. This will return the area of the circle, rounded to two decimal places.

# Version 1

class Circle:
    
    r = 5
    x = 0
    y = 0
        
    def area(self):
        pi = 3.141592653589793
        return round(pi * self.r**2, 2)

Notice the references to the parameter self in the area method. This will refer to the instance of circle from which the area method is called. We will say more about this in a moment.

Creating Instances of a Class

A class is simply a template. Defining a class does not create any instances of that type. We will now create an instance of our circle class, storing it in a variable named c1.

c1 = Circle()

print(type(c1))
<class '__main__.Circle'>

Notice that we can directly access the attributes of the class by following the class name with a period, and then the name of the attribute.

print('c1 radius:', c1.r)
print('c1 center: (', c1.x, ',', c1.y, ')', sep='')
c1 radius: 5
c1 center: (0,0)

We can use a similar format to call a method belonging to a class instance.

print('Area of c1:', c1.area())
Area of c1: 78.54

Notice that when we defined the area() method, we specified that it was to accept one parameter, named self. But when we called area, we apparently did not provide it with any arguments. In fact, we did. The class instance that the method is called from is always passed in as the first argument of the method. So, when we type c1.area(), the circle object c1 is plugged in for the self parameter of area, and then when we encounter the expression self.r within this method, it is interpreted as c1.r.

Every method within a class should have a self parameter that appears first within the parameter list. And self will always refer to the class instance from which the method was called.

Changing Attribute Values

Our circle c1 has the default radius of 5. We can change this by directly changing the value of c1.r.

c1.r = 7
print('Area of c1:', c1.area())
Area of c1: 153.94

Creating Multiple Instances

We can create as many instances of a class as we would like. Each such instance will be a separate object, with its own attributes and methods.

c2 = Circle()
c2.r = 3

print('c1 radius:', c1.r)
print('c2 radius:', c2.r)
c1 radius: 7
c2 radius: 3

Circle Class: Version 2

Notice that each of our circles starts out with a default radius of 5, and a center at (0,0). We can easily change these, but it would be helpful if we could specify the desired values when creating an instance of circle. We will now update our circle class to allow us to do just that.

Constructors

A constructor is a method that is called when an instance of a class is created. It is intended to perform the initialization of the class object. In Python, constructors should always have the special name __init__(). The constructor must accept self as a parameter, but can also accept additional parameters to be used during initialization.

class Circle:
    
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r
        
    def area(self):
        pi = 3.141592653589793
        return pi * self.r**2

When a class object is created, the constructor is immediately called the object is passed to the constructor, along with any other arguments that were listed between the parentheses when the object was created. Below, we will create a circle with a center at (5,2), and a radius of 6.

c3 = Circle(5, 2, 6)

print('c3 radius:', c3.r)
print('c3 center: (', c3.x, ',', c3.y, ')', sep='')
c3 radius: 6
c3 center: (5,2)

This provides us with much more flexibility when creating instances of the circle class.

Circle Class: Version 3

Let’s complete our circle class by adding four new methods: contains(), intersect(), copy(), and __str__().

  • contains() will check to see whether or not a provided point is inside the circle.

  • intersect() will check to see whether or not two circles intersect.

  • copy() will create and return a copy of the instance from which it was called.

  • __str__() is used to display information about the center and radius of the circle.

We will discuss each of these methods in more detail after providing the definition of the class.

class Circle:
    
    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r
        
    def area(self):
        pi = 3.141592653589793
        return pi * self.r**2
    
    def contains(self, x, y):
        # Find distance between center and new point
        dist = ( (self.x - x)**2 + (self.y - y)**2 )**0.5
        
        # If that distance is less than r, return True. Otherwise, return False.
        if dist < self.r:
            return True
        return False
    
    def intersect(self, other):
        # Find distance between two centers
        dist = ( (self.x - other.x)**2 + (self.y - other.y)**2 )**0.5
        
        # The circles intersect if the distance is between
        # the sum and difference of the two radii.
        if (dist >= abs(self.r - other.r)) and (dist <= self.r + other.r): 
            return True
        return False
        
    def copy(self):
        new_circle = Circle(self.r, self.x, self.y)
        return new_circle
    
    def __str__(self):
        out =  f'Center: ({self.x}, {self.y})\n'
        out += f'Radius: {self.r}'
        return out

The contains() Method

The contains() method accepts three parameters: self, x, and y. The parameter self is expected to be an instance of the Circle class. The parameters x and y are expected to be the coordinates of a point. This method returns True if the point provided lies inside the circle, and return False otherwise. This is accomplished by calculating the distance between the center of the circle and the new point. The point lies inside the circle if and only if that distance is less than the radius.

Notice that the contains() method accepts two parameters named x and y, and recall that each circle object has attributes named x and y. Within the method, the attributes will be stored in self.x and self.y. These will be distinct from the values of the parameters x and y. These parameter values will be created in a local scope when the method is called, and will disappear when the function finishes executing. The attributes, on the other hand, will persist as part of the object itself.

Let’s now test this method.

c4 = Circle(16, 20, 4)

print(c4.contains(18, 21))
print(c4.contains(19, 24))
True
False

The intersect() Method

The intersect() checks to see if two circles intersect. To accomplish this, the method first calculates the distance between the centers of the circles. If that distance is less than the sum of the circles’ radii and greater than the absolute value of the difference between the radii, then the circles intersect.

Notice that this method is required to accept two circle objects as arguments. These are referred to as self and other within the function definition. The parameter self will, as always, reference to the circle instance from which the method was called. The object other will be provided to the method by listing it between the parentheses.

c5 = Circle(x=0, y=0, r=4)
c6 = Circle(x=4, y=2, r=6)
c7 = Circle(x=5, y=4, r=1)

print('c5 and c6 intersect:', c5.intersect(c6))
print('c5 and c7 intersect:', c5.intersect(c7))
print('c6 and c7 intersect:', c6.intersect(c7))
c5 and c6 intersect: True
c5 and c7 intersect: False
c6 and c7 intersect: False

The copy() Method

As is the case with lists, when we set a variable equal to another variable containing a class instance, the new variable will refer to the currently existing instance rather than a copy of that instance. We will demonstrate this below.

c8 = c7
c8.r = 3

print('c7 radius:', c7.r)
print('c8 radius:', c8.r)
c7 radius: 3
c8 radius: 3

We can copy instances of a class by providing them with a copy() method, as we have done with the Circle class. This method creates and returns a new instance of the class with the same attribute values as the instance from which it was called. We will now demonstrate that this does, in fact, create a new instance of the class.

c9 = c8.copy()
c9.r = 9

print('c8 radius:', c8.r)
print('c9 radius:', c9.r)
c8 radius: 3
c9 radius: 9

The __str__() Method

Let __init__(), the __str__() method is a special method that can belong to any class, and that performs a specific function. Generally speaking, the __str__() method should return a string that contains some information about a particular instance of the class. This method is called whenever an instance of the class is passed to the print() function. In that case, the string returned by the __str__() method is what is actually displayed.

Notice that the __str__() method for the Circle is a string that states the center and radius of a Circle instance. We will now call print on a Circle object to confirm that this information is what is actually displayed.

print(c9)
Center: (3, 5)
Radius: 9

Application: Sensor Network

import numpy as np
import matplotlib.pyplot as plt
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
<ipython-input-15-e0e1492b7973> in <module>
      1 import numpy as np
----> 2 import matplotlib.pyplot as plt

ModuleNotFoundError: No module named 'matplotlib'
np.random.seed(1)
n = 40
x_array = np.random.uniform(0, 100, n)
y_array = np.random.uniform(0, 100, n)
r_array = np.random.uniform(10, 16, n)

plt.figure(figsize=[10,10])
ax = plt.gca()
plt.scatter(x_array, y_array, edgecolor='k', s=100)
for i in range(n):
    ax.add_artist(plt.Circle((x_array[i],y_array[i]), r_array[i], color='cornflowerblue', alpha=0.2))
plt.show()
site_list = []
for i in range(n):
    temp = Circle(x_array[i], y_array[i], r_array[i])
    site_list.append(temp)   
def count_sites(site_list, x, y):
    site_count = 0
    for i in range(len(site_list)):
        if(site_list[i].contains(x, y)):
            site_count += 1

    return site_count
print(count_sites(site_list, 20, 82))
print(count_sites(site_list, 60, 50))
print(count_sites(site_list, 80, 90))