

Prof. Andrea Gallegati
So far, we have seen how to use:
object-oriented programming uses programmer-defined types to organize both code and data.
let's create a Point type, representing a point in 2D space.
In mathematical notation, points are often written:
(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:
x and y.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:
class Point:
"""Represents a point in 2-D space."""
PointWe will define variables and methods inside a class definition (body).
This creates a class object, that is like a factory to construct objects:
Point
__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):
blank = Point()
blank
<__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:
( 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:
to assign values to an instance, use dot notation:
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:
blank.y
4.0
x = blank.x
x
3.0
There is no conflict between the variable x and the attribute x.
We can use dot notation in any expression:
'(%g, %g)' % (blank.x, blank.y)
'(3, 4)'
import math
distance = math.sqrt(blank.x**2 + blank.y**2)
distance
5.0
We can pass an instance as an argument, to functions, as well
def print_point(p):
print('(%g, %g)' % (p.x, p.y))
... where we encapsulated the string formatting (% operator) above
print_point(blank)
(3, 4)
p is an alias for blank: if the function modifies p, blank changes accordingly.
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:
At first it could be hard to say whether either is better than the other.
Let's implement the first one.
class Rectangle:
"""Represents a rectangle.
attributes: width, height, corner.
"""
The docstring lists the attributes:
width and height are numberscorner is a Point object that specifies the lower-left cornerLet's instantiate a Rectangle object and assign values to its attributes:
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.
Functions can return instances as well.
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).
center = find_center(box)
print_point(center)
(50, 100)
We can change the state of an object by assignming one of its attributes.
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.
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.
box.width, box.height
(150.0, 300.0)
grow_rectangle(box, 50, 100)
box.width, box.height
(200.0, 400.0)
rect is an alias for box: when the function modifies rect, box changes accordingly.
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.
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:
p1 = Point()
p1.x = 3.0
p1.y = 4.0
import copy
p2 = copy.copy(p1)
print_point(p1)
(3, 4)
print_point(p2)
(3, 4)
p1 and p2 contain the same data, but they are not the same Point!
p1 is p2
False
p1 == p2
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.
box2 = copy.copy(box)
box2 is box
False
box2.corner is box.corner
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.
grow_rectangle on one Rectangle would not affect the other (same data; different references)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!
box3 = copy.deepcopy(box)
box3 is box
False
box3.corner is box.corner
False
box3 and box now are two completely separate objects (same data; different references).
box3 == box
False
box3.corner == box.corner
False
Working with objects it is likely to encounter new exceptions.
Trying to access an attribute that doesn’t exist, we get an AttributeError:
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:
type(p)
__main__.Point
we can even check whether an object is an instance of a class:
isinstance(p, Point)
True
... not sure whether an object has a particular attribute?
hasattr(p, 'x')
True
hasattr(p, 'z')
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:
try:
x = p.x
except AttributeError:
x = 0
useful inside functions working with different types (aka Polymorphism).