You are on page 1of 59

Lecture 2: Basic Python Programming Part 2

CIS-275
a collection of data (variables) and
Creating New Classes methods (functions) that act on
those data.
● When we define a class, we create a new data type.
○ A class can be thought of as a blueprint for a type of object.
○ We can use this blueprint to create multiple objects (or instances of the class).
● A class definition describes the data an instance of the class will hold as well as the
methods that can be performed on it.

Class is like a Build-A-Bear workshop. It gives you the blueprint to make unique Teddy Bears (that aren't
duplicates of each other but bear many similar attributes) or instances.

eg. Class is to Build-A-Bear as objects/instances are to stuffed animals.

Chapter 1 Basic Python Programming 2


Creating New Classes
A class definition is made up of the following parts:

class <class name>(<parent class name>):


<class variable assignments>
<instance method definitions>

Methods, in general are functions associated with a class.


● An instance method is a function that can be called on instances of the class.
Methods usually retrieve or modify instance variables in some way.
● A class variable is shared by all instances of its class (more on this soon)
○ Note: The parent class name is optional. For now, we will leave this out of our
class definitions, but later on we will start using it.

Chapter 1 Basic Python Programming 3


Creating New Classes
● A few key terms relating to classes:
○ Instance variable: Storage held by an individual object. Each object of the same
class might have different values in their instance variables.
○ Instance method: A method that is run on an object of a class. It can perform
operations on that object’s instance variables.
○ Class variable: Storage that is common to all instances of a class. i.e. if we
change a class variable for one object it is actually changed for all objects of the
same class.
Instance Variable: two stuffed animals (the instances) made at Build-A-Bear (the class) with clothes and hats,
but the stuffed animals are different (different instance variable).
Instance methond: the act of putting on clothes (instance methond) on a stuffed animal (instance variable) at
Build-A-Bear (class)
Class variable: All stuffed animals (instances) at Build-A-Bear (class) have stuffing in them (class variable)

Chapter 1 Basic Python Programming 4


Creating New Classes
● Today we will demonstrate the creation of a simple class named Counter.
● In this course, we will usually define each class in its own .py file.
● In PyCharm, you can create a new .py file by right-clicking the project name (Classes
in this example) and selecting new → Python File.
● Give the file the same name as the class you are defining.
○ In this example, we are creating a file named Counter.py.

Chapter 1 Basic Python Programming 5


Creating New Classes
● After creating a class on a different file, we need to import it into Main (or whatever
other file you want to use that class on).
● For now, we will give a file the same name as the class defined on it:

from Counter import Counter

● This statement tells Python that from the Counter module (i.e. Counter.py) we want
to import the Counter class.
● We can now create Counter objects on the file the class is imported to.

Chapter 1 Basic Python Programming 6


Creating New Classes
● A Counter object will store an integer value, initialized to 0.
● The Counter class will be used for counting how many times a particular event
occurred, so we will restrict the client to incrementing its value by 1 at a time.
● The class will:
○ Have methods which allow the client to increment by 1, decrement by 1,
retrieve, or reset the value to 0.
○ Override a some of the built-in, default methods and overload a few operators,
allowing us to compare counter objects and add them together, etc.
○ Have a class variable to track how many Counter objects have been created.

Chapter 1 Basic Python Programming 7


Creating a Counter Class
class Counter:
# Class variable instances variable outside of constructor, therefore class variable
instances = 0

# Constructor
def __init__ (self):
""" Creates a new Counter object and initializes its data """
Counter.instances += 1 because instances is a class variable, we have to call the class (in this
self._value = 0 case Counter) it's attached to in order to use it, even if the code is inside
the class it's referring to .
● The __init__ method is a class’s constructor.
● When we define this method, we override the default constructor provided by Python.
● When a Counter object is created, its __init__ method is immediately called.
● To create an object, the client writes the class name followed by parentheses:
Counter() # Calls the __init__ method

Chapter 1 Basic Python Programming 8


Creating a Counter Class
class Counter:
# Class variable
instances = 0

# Constructor
def __init__ (self):
""" Creates a new Counter object and initializes its data """
Counter.instances += 1
self._value = 0

● Python implicitly passes self to every class method. This parameter is a reference to
the calling object (the object the method was called on)
○ Note: in __init__, self references the new object Python created.
● In this method, we attach the attribute _value to the new object and initialize it to 0.
○ By convention, a leading underscore signifies that an attribute is private: code
outside of the class should not access it directly.

Chapter 1 Basic Python Programming 9


Creating a Counter Class
class Counter:
# Class variable
instances = 0

# Constructor
def __init__ (self):
""" Creates a new Counter object and initializes its data """
Counter.instances += 1
self._value = 0

● instances is a class variable. It is not attached to an individual Counter object but


rather associated with the Counter class as a whole.
● _value is an instance variable. Each Counter object will have its own _value
attribute and each may contain different numbers.

Chapter 1 Basic Python Programming 10


Creating a Counter Class
from Counter import Counter

def main():

c1 = Counter()
c2 = Counter()
print(c1.instances) # Prints 2

main()

● This code demonstrates how a class variable works.


● Each time the Counter constructor is called, Counter.instances is updated.
● We can access class variables from an object of that class or from the class itself.
● All three of the following lines work and all print the same value:
print(c1.instances) # Prints 2
print(c2.instances) # Prints 2
print(Counter.instances) # Prints 2

Chapter 1 Basic Python Programming 11


Property Methods
class Counter:

# Constructor and class variables go here


@property
def value(self):
return self._value method name should match attribute you're trying to retrieve

● self._value is private, so client code should not access it directly. also called a getter
● We can create a property method to allow the client to retrieve a private attribute.
● When the client calls a property method, they can leave off the parentheses. It appears
as if they are directly accessing an attribute:

my_counter.value # calls the property method 'value'. self is passed implicitly

Chapter 1 Basic Python Programming 12


Property Methods
class Counter:

# Constructor and class variables go here


@property method name should match attribute you're trying to retrieve
def value(self):
return self._value to call it would be <variablename>.value

@value.setter val gets updated


def value(self, val): to call it would be <variablename>.value = <number> to the new <number>
self._value = val

● A property setter allows the client to set the value of a private attribute.
● The decorator before the method header must include the name of the related property
and the method name must be the same.

my_counter = Counter()
my_counter.value = 5 # Calls the 'value' property setter, passes 5 for 'val'.
print(my_counter.value) # prints 5

Chapter 1 Basic Python Programming 13


Creating a Counter Class
● Suppose we have this code on main.py:

from Counter import Counter

def main():

Counter()
# More code goes here!

main()

● Is there anything wrong here?

Chapter 1 Basic Python Programming 14


Creating a Counter Class
● Suppose we have this code on main.py:

from Counter import Counter

def main():

Counter()
# More code goes here!

main()

● Is there anything wrong here? Yes!


● We create a Counter object but do not assign it to a variable.
○ This object is immediately garbage collected.

Chapter 1 Basic Python Programming 15


Creating a Counter Class
from Counter import Counter

def main():

my_counter = Counter()
print(my_counter.value)
# More code goes here!

main()

● This updated code assigns the newly created object to the variable my_counter.
● It will now remain in memory as long as at least one variable points to it.

Chapter 1 Basic Python Programming 16


Creating a Counter Class
class Counter:
# Constructor goes here

def increment( self):


""" adds 1 to the counter """
self._value += 1

● The increment method increases _value by one.


my_counter.increment() # Increment my_counter._value by 1

● Recall that we want to restrict the client from updating by a value other than one.
● For this reason, we want the client to only use this method to update their Counter.

Chapter 1 Basic Python Programming 17


Protecting Private Data
● One of the nice things about providing the client with methods to update internal data
is we can prevent them from accidentally using incorrect values.
● For example, what might the client do wrong when they create a Counter?
c = Counter()

Chapter 1 Basic Python Programming 18


Protecting Private Data
● One of the nice things about providing the client with methods to update internal data
is we can prevent them from accidentally using incorrect values.
● For example, what might the client do wrong when they create a Counter?
c = Counter()

● Suppose they set the value to a negative number:

c._value = -5

● Or they may try to increment the value by more than 1:


c._value += 10

Chapter 1 Basic Python Programming 19


Instance Variables
● Recall that _value is an instance variable.
● Each instance has its own unique instance variables:

my_counter1 = Counter()
my_counter2 = Counter()

my_counter2.increment() # only update my_counter2._value

print(my_counter1.value)
print(my_counter2.value)

_value: 0
● After this code executes, we have two reference my_counter1
variables pointing to two different Counter objects:
● Since we only called increment on my_counter2, _value: 1
its _value is the only one that is higher.
my_counter2
Chapter 1 Basic Python Programming 20
Class Variables
● What will the following code print?
my_counter1 = Counter()
my_counter2 = Counter()
print("They are equal!" if my_counter1 == my_counter2 else "They are not equal")

● Note: The third statement is a syntactic shortcut and is equivalent to the following:

if my_counter1 == my_counter2:
print("They are equal!")
else:
print("They are not equal!")

Chapter 1 Basic Python Programming 21


Class Variables
● What will the following code print? “They are not equal”
my_counter1 = Counter()
my_counter2 = Counter()
print("They are equal!" if my_counter1 == my_counter2 else "They are not equal")

● Although both Counter objects have the same _value of 0, Python currently does
consider them to be equal.
● By default, the == operator only evaluates to true if both its operands reference the
same object in memory.
● However, there are two Counter objects, each in different memory locations:
therefore they're not
my_counter1 _value: 0 equal

my_counter2 _value: 0

Chapter 1 Basic Python Programming 22


used to find if two instances are
The __eq__ method equal to each other

my_counter1 = Counter()
my_counter2 = Counter()
print("They are equal!" if my_counter1 == my_counter2 else "They are not equal")

● Every Python object comes with several built-in methods that are called when they
are operands of certain operators.
○ For example, the __eq__ method is called on the left operand of ==.
○ If a class does not define a body for __eq__, its default version is called.
■ This version returns True if both operands reference the same object.
○ However, we can override this method!

my_counter1 _value: 0

my_counter2 _value: 0

Chapter 1 Basic Python Programming 23


The __eq__ method
● Let’s override the default __eq__ method in Counter:
class Counter:

# Other methods here


def __eq__(self, other):
???

● Note: When the following statement executes, __eq__ is called on my_counter1:


my_counter1 == my_counter2

● Python passes my_counter1 to self and my_counter2 to other


● i.e. the left operand of the == operator is considered the calling object and the right
operand is the argument.

Chapter 1 Basic Python Programming 24


The __eq__ method
● Let’s override the default __eq__ method in Counter:
def __eq__(self, other):
if self is other: # Evaluates to True if self and other reference same object
return True
elif self._value == other.value:
return True
return False

● Is there anything missing from this method?


○ Hint: What will the following code do?
my_counter1 = Counter()
print(my_counter1 == "Hello")

Chapter 1 Basic Python Programming 25


The __eq__ method
● Let’s override the default __eq__ method in Counter:
def __eq__(self, other):
if self is other: # Evaluates to True if self and other reference same object
return True
elif self._value == other.value:
return True
return False

● Is there anything missing from this method?


○ Yes!
my_counter1 = Counter()
print(my_counter1 == "Hello") AttributeError: 'str' object has no attribute 'value'

Chapter 1 Basic Python Programming 26


The __eq__ method
● Let’s override the default __eq__ method in Counter:
class Counter:

# Other methods here


def __eq__(self, other):
if self is other: # Evaluates to True if self and other reference same object
return True
elif self._value == other.value:
return True
return False

● If the right operand of an == expression does not reference a Counter object, it won’t
have a value property method.
● When __eq__ attempts to call it on a str object (for example), an Error is raised!
● We would usually rather simply return False in this scenario.

Chapter 1 Basic Python Programming 27


The __eq__ method
class Counter:
def __eq__(self, other):
1 if self is other: # Evaluates to True if self and other reference same object
return True
2 if type(self) != type(other):
return False
3 elif self._value == other.value:
return True
4 return False

● Note: The type built-in function returns a variable’s type.


● This method now returns:
○ True if both operands point to the exact same object. 1
○ False if other does not reference a Counter object. 2
○ True if both objects contain the same _value. 3
○ False otherwise. 4

Chapter 1 Basic Python Programming 28


The __str__ Method
● The __str__ method should return a string representation of an object’s internal data.
○ By default, it prints out the object’s type and memory location:
● __str__ is called implicitly on an object when it is passed to the print function:

my_counter = Counter()
print(my_counter) # Calls __str__ on my_counter

● If Counter does not override __str__, this code will print something like this:

<Counter.Counter object at 0x0275D1C0>

Chapter 1 Basic Python Programming 29


The __str__ Method
● It is up to us to decide what string best represents an object’s data.
● The following is an example of doing so. Is there anything wrong with it?
def __str__(self):
print("Counter Object with a Value of ", self._value)

Chapter 1 Basic Python Programming 30


The __str__ Method
● It is up to us to decide what string best represents an object’s data.
● The following is an example of doing so. Is there anything wrong with it? Yes!
def __str__(self):
print("Counter Object with a Value of ", self._value)

● Recall that __str__ should return a string, not simply print it out.
○ A TypeError will be raised when the above method executes.
● Instead, we should do something like this:

def __str__(self):
return "Counter Object with a Value of " + str(self._value)

Chapter 1 Basic Python Programming 31


Comparing Counter Objects
● Consider the following code:
my_counter is 0
my_counter = Counter()
my_counter.increment()
my_counter is 1
my_counter2 = Counter() my_counter is 0
print("my_counter is larger" if my_counter > my_counter2 else "my_counter2 is larger")

● What prints?

● What should we want to print?

Chapter 1 Basic Python Programming 32


Comparing Counter Objects
● Consider the following code:

my_counter = Counter()
my_counter.increment()
my_counter2 = Counter()
print("my_counter is larger" if my_counter > my_counter2 else "my_counter2 is larger")

● What prints? Nothing! An Error is raised.


○ Unlike with ==, objects in Python do not have default behavior for the >
operator. This program will crash.

● What should we want to print? “my_counter is larger”

Chapter 1 Basic Python Programming 33


__lt__ is for less than, __gte__ is for

The __gt__ Method greater than or equal to, __lte__ is


for less than or equal to

● When Python encounters the > operator, it calls the __gt__ method on its left
operand, passing the right operand as an argument.
○ If the __gt__ method is not supported by the left operand’s class, an Error
occurs.
● This function can be defined fairly easily:

def __gt__(self, other):


return self._value > other.value

● Note: I didn’t check to see if self and other are the same type.
○ In this case, we probably still want an Error to be raised if the client tries to see if
a Counter object is greater than another type of object.
○ This would likely imply a mistake in the client’s code.

Chapter 1 Basic Python Programming 34


Adding Two Counter Objects
● Consider the following code:
my_counter = Counter()
my_counter.increment( 3)
my_counter2 = Counter()
my_counter2.increment()
my_counter3 = my_counter + my_counter2
print(my_counter3)

● What will occur?

● What should we want to occur?

Chapter 1 Basic Python Programming 35


Adding Two Counter Objects
● Consider the following code:
my_counter = Counter()
my_counter.increment( 3)
my_counter2 = Counter()
my_counter2.increment()
my_counter3 = my_counter + my_counter2
print(my_counter3)

● What will occur? The program will crash: The + operator is not defined for Counter
objects. As with >, no default method exists.

● What should we want to occur?


○ my_counter + my_counter2 should create a new Counter object
○ Its _value should be the addition of the _values of the other two Counters.

Chapter 1 Basic Python Programming 36


The __add__ Method
● When Python encounters the + operator, it calls the __add__ method on its left
operand, passing the right operand as an argument.
○ If the __add__ method is not supported by the left operand’s class, an Error
occurs.

def __add__(self, other):


???

● Note: The __add__ method should return a new object, not mutate either of its
arguments (self or other).

Chapter 1 Basic Python Programming 37


The __add__ Method
● When Python encounters the + operator, it calls the __add__ method on its left
operand, passing the right operand as an argument.
○ If the __add__ method is not supported by the left operand’s class, an Error
occurs.

def __add__(self, other):

# Create a new Counter object to return


return_counter = Counter()

# Set the new object's value to the sum of the other two's values
return_counter.value = self.value + other.value
return return_counter

Chapter 1 Basic Python Programming 38


The __add__ Method
● With the + operator now overridden, the following code prints “Counter object with a
value of 4”:

my_counter = Counter()
my_counter.increment( 3)
my_counter2 = Counter()
my_counter2.increment()
my_counter3 = my_counter + my_counter2
print(my_counter3)

Chapter 1 Basic Python Programming 39


Other Methods to Override
● When we build more complicated classes and data structures in this course, we will
override other operators!

Chapter 1 Basic Python Programming 40


The Date Class
● For the remaining portion of the lecture, let’s create a class for use in scheduling
applications.
● Each Date object will represent a particular calendar date and store a day, a month,
and a year value.

Chapter 1 Basic Python Programming 41


The Date Class
● In this simple constructor, the client is able to set the month, day, and year of a Date
when they create one.

class Date:
def __init__(self, day, month, year):
self._day = day
self._month = month
self._year = year

● In the client code, a Date object can be created in the following way:
new_years = Date(1, 1, 2022)

Chapter 1 Basic Python Programming 42


The Date Class
● The days_in_month method returns how many days are in the Date object’s month
(ignoring leap years for now):

Chapter 1 Basic Python Programming 43


The Date Class
● The days_in_month method returns how many days are in the Date object’s month
(ignoring leap years for now):
def days_in_month( self):
if self.month in (4, 6, 9, 11):
return 30
elif self.month == 2:
return 28
else:
return 31

Chapter 1 Basic Python Programming 44


The Date Class
● Why might we not want to allow the client to do something like this?

d1 = Date(5, 12, 22)


d1._day += 1

Chapter 1 Basic Python Programming 45


The Date Class
● Why might we not want to allow the client to do something like this?

d1 = Date(5, 12, 22)


d1._day += 1

● Two reasons:
○ If the client accesses the _day attribute directly, they could set an invalid value:
d1._day = 55

○ When the client wants to update the _day, suppose the Date is currently at the
end of the month.
○ The client must calculate if it’s the end of the month (or even year) and then
adjust the month as well.
■ This calculation can be a little complex.
Chapter 1 Basic Python Programming 46
The Date Class
● Let’s write a method named advance which updates a Date object to the next day.
● This method should take into account the possibility that we’re at the end of the
month and the end of the year.

Chapter 1 Basic Python Programming 47


The Date Class
● Let’s write a method named advance which updates a Date object to the next day.
● This method should take into account the possibility that we’re at the end of the
month and the end of the year.
def advance(self):
self.day += 1
if self.day > self.days_in_month():
# wrap to next month
self.day = 1
self.month +=1
if self.month > 12:
# wrap to next year
self.month = 1

● We have abstracted this complexity away from the client.


● They no longer need to worry about advancing to an invalid date!

Chapter 1 Basic Python Programming 48


The Date Class
● A simple override of the __str__ method will allow us to test our Date class as well
as the advance method.

def __str__(self):
return f'{self._month}/{self._day}/{self._year}'

Chapter 1 Basic Python Programming 49


The Date Class
● Consider the following property and setter:
@property
def day(self):
return self._day

@day.setter
def day(self, new_day):
self._day = new_day

● Do they allow the client to make mistakes?

Chapter 1 Basic Python Programming 50


The Date Class
● Consider the following property and setter:
@property
def day(self):
return self._day

@day.setter
def day(self, new_day):
self._day = new_day

● Do they allow the client to make mistakes? Yes!

d1 = Date(5, 12, 22)


d1.day = 500 # ERROR: Invalid date!

Chapter 1 Basic Python Programming 51


The Date Class
● We need to validate the day the client tries to assign to a Date object as well as the
month.
● Furthermore, we need to validate these values in both the setters and the constructor
(so the client doesn’t initialize a Date with invalid data).
● To help with this, let’s write two helper functions:
○ validate_day: Make sure the day the client is trying to assign is within the
correct range based on the object’s _month.
○ validate_month: Make sure the month the client is trying to assign is within
the range of 1-12.

Chapter 1 Basic Python Programming 52


The Date Class
● Note that both methods raise Errors.
● During class initialization, it’s almost always better to raise an Error rather than
simply printing a message.

def validate_month(self, month):


if month < 1 or month > 12:
raise ValueError(f'Invalid month: {month}')

def validate_day(self, day):


if day < 1 or day > self.days_in_month():
raise ValueError(f'Invalid day: {day}')

● Why?

Chapter 1 Basic Python Programming 53


The Date Class
● Note that both methods raise Errors.
● During class initialization, it’s almost always better to raise an Error rather than
simply printing a message.

def validate_month(self, month):


if month < 1 or month > 12:
raise ValueError(f'Invalid month: {month}')

def validate_day(self, day):


if day < 1 or day > self.days_in_month():
raise ValueError(f'Invalid day: {day}')

● Why? When the client initializes a Date object and they provide invalid data, we
don’t know what the default _day or _month should be.
● It’s better to crash the program and let the client know they made a mistake.
Chapter 1 Basic Python Programming 54
The Date Class
● In the constructor, we now call both.
● The month must be validated and set first, since validate_day checks its value.

def __init__(self, day, month, year):


self.validate_month(month)
self._month = month
self.validate_day(day)
self._day = day
self._year = year

● And we call the relevant method in the corresponding setter:

@day.setter
def day(self, new_month):
self.validate_month(new_month)
self._month = new_month

Chapter 1 Basic Python Programming 55


The Date Class
● Next, we can override the __eq__ method to allow two Date objects to be compared.
● Two dates are equal if:

Chapter 1 Basic Python Programming 56


The Date Class
● Next, we can override the __eq__ method to allow two Date objects to be compared.
● Two dates are equal if:
○ The day, month, and year are all the same!

def __eq__(self, other):


return self._day == other._day and self._month == other._month and self._year == other._year

Chapter 1 Basic Python Programming 57


The Date Class
● Finally, we might want to sort a list containing Date objects.
● To do this, we need to override the __gt__ method.

Chapter 1 Basic Python Programming 58


The Date Class
● Finally, we might want to sort a list containing Date objects.
● To do this, we need to override the __gt__ method.

def __gt__(self, other):


if self._year > other.year:
return True
elif self._year == other.year and self._month > other.month:
return True
elif self._year == other.year and self._month == other.month and self._day > other.day:
return True
else:
return False

Chapter 1 Basic Python Programming 59

You might also like