CIS 1051 - Temple Rome Spring 2023¶

Intro to Problem solving and¶

Programming in Python¶

LOGO

LOGO

Classes and Objects¶

Prof. Andrea Gallegati

( tuj81353@temple.edu )

So far, we have seen how to use:

  • functions to organize code
  • (some) built-in types to organize data

object-oriented programming uses programmer-defined types to organize both code and data.

Programmer-Defined Types¶

let's create a Point type, representing a point in 2D space.

In mathematical notation, points are often written:

  • in parentheses
  • with a comma separating the coordinates

(0,0) represents the origin

(x,y) represents the point x units to the right and y units up from the origin.

We might represent points in Python:

  • storing the coordinates separately in two variables, x and y.
  • storing the coordinates as elements in a list or tuple.
  • creating a new type to represent points as objects.

Creating a new type is more complicated, but its advantages will be apparent soon.

A programmer-defined type is also called a class.

A class definition looks like this:

In [1]:
class Point:
    """Represents a point in 2-D space."""
  • the header indicates the new class is called Point
  • the body is a docstring to explains what it's for.

We will define variables and methods inside a class definition (body).

This creates a class object, that is like a factory to construct objects:

In [2]:
Point
Out[2]:
__main__.Point

whose “full name” is __main__.Point being the class definition at the top level.

To create a Point, you call Point as if it were a function (aka constructor):

In [3]:
blank = Point()
blank
Out[3]:
<__main__.Point at 0x7fa4f85e72b0>

The return value is a reference to a Point object, we assigned to blank.

This is called instantiation since the object is an instance of the class.

When we print an instance, Python tells:

  • what class it belongs to
  • where it is stored in memory

( prefix `0x` is for **hexadecimal** numbers - memory addresses )

Every object is an instance of some class, so “object” and “instance” are interchangeable.

In mathematical notation, points are often written:

  • in parentheses
  • with a comma separating the coordinates

Attributes¶

to assign values to an instance, use dot notation:

In [6]:
blank.x = 3.0
blank.y = 4.0

similar to what we did for selecting a variable from a module (e.g. math.pi or string.whitespace).

In this case we are assigning values to named elements of an object: attributes.

State diagrams showing objects and their attributes are called: objects diagrams

Each attribute refers to a floating-point number.

Read attributes values always using the dot notation:

In [8]:
blank.y
Out[8]:
4.0
In [9]:
x = blank.x
x
Out[9]:
3.0

There is no conflict between the variable x and the attribute x.

We can use dot notation in any expression:

In [10]:
'(%g, %g)' % (blank.x, blank.y)
Out[10]:
'(3, 4)'
In [13]:
import math
distance = math.sqrt(blank.x**2 + blank.y**2)
distance
Out[13]:
5.0

We can pass an instance as an argument, to functions, as well

In [15]:
def print_point(p):
    print('(%g, %g)' % (p.x, p.y))

... where we encapsulated the string formatting (% operator) above

In [16]:
print_point(blank)
(3, 4)

p is an alias for blank: if the function modifies p, blank changes accordingly.

Rectangles¶

Sometimes we have to make decisions:

it might not be so obvious which attributes an object should be made of.

Designing a class to represent rectangles and assuming the rectangle to be either vertical or horizontal:

  • we could specify one corner, the width, and the height.
  • we could specify the center, the width, and the height.
  • we could specify two opposing corners.

At first it could be hard to say whether either is better than the other.

Let's implement the first one.

In [22]:
class Rectangle:
    """Represents a rectangle. 

    attributes: width, height, corner.
    """

The docstring lists the attributes:

  • width and height are numbers
  • corner is a Point object that specifies the lower-left corner

Let's instantiate a Rectangle object and assign values to its attributes:

In [35]:
box = Rectangle()
box.width = 100.0
box.height = 200.0
box.corner = Point()
box.corner.x = 0.0
box.corner.y = 0.0

... expressions like box.corner.x mean:

“Go to the object box refers to and select the attribute named corner; then go to that object and select the attribute named x.”

Objects attributes of other objects are embedded.

Instances as Return Values¶

Functions can return instances as well.

In [23]:
def find_center(rect):
    p = Point()
    p.x = rect.corner.x + rect.width/2
    p.y = rect.corner.y + rect.height/2
    return p

this takes a Rectangle as an argument and returns a Point (with the coordinates of its center).

In [24]:
center = find_center(box)
print_point(center)
(50, 100)

Objects Are Mutable¶

We can change the state of an object by assignming one of its attributes.

In [36]:
box.width = box.width + 50
box.height = box.height + 100

this changes the size of a rectangle without changing its position!

We can also write functions to modify objects.

In [26]:
def grow_rectangle(rect, dwidth, dheight):
    rect.width += dwidth
    rect.height += dheight

this takes a Rectangle object and two numbers, dwidth and dheight, to add them to the rectangle's width and height.

In [37]:
box.width, box.height
Out[37]:
(150.0, 300.0)
In [38]:
grow_rectangle(box, 50, 100)
box.width, box.height
Out[38]:
(200.0, 400.0)

rect is an alias for box: when the function modifies rect, box changes accordingly.

In [54]:
def move_rectangle(rect, dx, dy):
    rect.corner.x += dx
    rect.corner.y += dy

this takes a Rectangle object and two numbers, dx and dy, to add them to the rectangle's corner coordinates.

Copying¶

Aliasing can make a program difficult to read: changes in one place might have unexpected effects in another place.

( hard to keep track of all the variables referring to a given object )

Copying an object is often an alternative to aliasing.

The copy module contains a copy function to duplicate any object:

In [39]:
p1 = Point()
p1.x = 3.0
p1.y = 4.0

import copy
p2 = copy.copy(p1)
In [40]:
print_point(p1)
(3, 4)
In [41]:
print_point(p2)
(3, 4)

p1 and p2 contain the same data, but they are not the same Point!

In [42]:
 p1 is p2
Out[42]:
False
In [43]:
p1 == p2
Out[43]:
False

As expected the is operator tells p1 and p2 are not the same object.

Contrary, we expected the == operator to yield True ( same data ).

... but for instances, the == operator default behavior is the same as the is operator: it checks for the object identity, not object equivalence.

For programmer-defined types, Python doesn’t know what to consider equivalent. At least, not yet.

Moreover, using copy.copy copies the Rectangle object but not the embedded Point object attribute.

In [44]:
box2 = copy.copy(box)
box2 is box
Out[44]:
False
In [45]:
box2.corner is box.corner
Out[45]:
True

This is a shallow copy: it copies the object and any references it contains, but not the embedded objects.

For most applications, this is not what you want.

  • invoking grow_rectangle on one Rectangle would not affect the other (same data; different references)
  • invoking move_rectangle on either would affect both! (same data; same references)

This behavior is confusing and error-prone.

... copy module provides a deepcopy method that copies the object as well as the objects it refers to, and the objects they refer to, and so on!

In [50]:
box3 = copy.deepcopy(box)
box3 is box
Out[50]:
False
In [47]:
box3.corner is box.corner
Out[47]:
False

box3 and box now are two completely separate objects (same data; different references).

In [52]:
box3 == box
Out[52]:
False
In [53]:
box3.corner == box.corner
Out[53]:
False

Debugging¶

Working with objects it is likely to encounter new exceptions.

Trying to access an attribute that doesn’t exist, we get an AttributeError:

In [57]:
p = Point()
p.x = 3
p.y = 4
p.z
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-57-9b05942bb1b6> in <module>()
      2 p.x = 3
      3 p.y = 4
----> 4 p.z

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

... not sure what type an object is? We can ask:

In [59]:
type(p)
Out[59]:
__main__.Point

we can even check whether an object is an instance of a class:

In [60]:
isinstance(p, Point)
Out[60]:
True

... not sure whether an object has a particular attribute?

In [61]:
hasattr(p, 'x')
Out[61]:
True
In [62]:
hasattr(p, 'z')
Out[62]:
False

First argument can be any object; the second is a string with the attribute's name.

... or even use a try statement to see if the object has the attributes you need:

In [63]:
try:
    x = p.x
except AttributeError:
    x = 0

useful inside functions working with different types (aka Polymorphism).