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."""
Point
We 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).