These lecture notes are partly adapted from https://www.python-course.eu/.
When we create instance variables of a class, we must decide what level of access "users" of the class should have over the data and procedural attributes. Some OOP languages enforce these distinctions, Python uses a special naming convention to "signal the attribute type."
__
or _
): these attributes should only be used inside of the class definition. Among these, attributes with __
are strictly private and essentially invisible from outside. Attributes with _
can be used from outside but really should not be. class TestingAttributes():
__slots__ = ['__val', '_val', 'val']
def __init__(self):
self.__val = "I am strictly private."
self._val = "I am private but accessible from outside."
self.val = "I am public."
a = TestingAttributes()
a.__val
a._val
a.val
__str__
¶If we print an object of our class coordinate
, notice that the output is not very human readable. We can create user--defined way of a "pretty" object description using the special __str__
method.
Python calls the __str__
method when you print a class object. We can choose what it does! Suppose we want to it to print <3, 4>
when called on an object with x-coordinate 3
and y-coordinate 4
. Let us write a __str__
method to do this.
class A:
"""Test printing of objects."""
pass
a = A()
print(a)
class A:
"""Test printing of objects."""
def __str__(self):
return 'print representation'
a = A()
print(a)
class Name:
"""Class to represent a person's name."""
__slots__ = ['_f', '_m', '_l']
def __init__(self, first, last, middle=''):
self._f = first
self._m = middle
self._l = last
def __str__(self):
if len(self._m):
return '{}. {}. {}'.format(self._f[0], self._m[0], self._l)
return '{}. {}'.format(self._f[0], self._l)
n1 = Name('Shikha', 'Singh')
n2 = Name('Iris', 'Howley', 'K.')
print(n1)
print(n2)
Encapsulation is the bundling of data with the methods that operate on that data. It is often accomplished by providing two kinds of procedural attributes:
class Coordinate(object):
"""Represents the coordinates of a point."""
__slots__ = ['_x', '_y']
def __init__(self, x, y):
self._x = x
self._y = y
def getX(self):
return self._x
def getY(self):
return self._y
def __str__(self):
return '<{},{}>'.format(self._x, self._y)
p = Coordinate(4, 3)
q = Coordinate(0, 2)
print(p)
print(q)
p.getX()
p.getY()
p.getX() + q.getX()
p.getY() + q.getY()
Note. Explicilty calling getter methods as functions, especially if they are used a lot, can be cumbersome. It would be nice if we can write p.x + p.y
instead of p.getX() + q.getX()
.
@property
¶Annotations @
. Python provides a rich collection of syntactic notes that can change how code is interpreted, called annotations. These are typically prefixed with the at-sign (@).
@property
annotation (Treat a procedural attribute as a data attribute): If we’d like to treat an accessor method as-if it were a data attribute, we can use the @property annotationclass Coordinate(object):
"""Represents the coordinates of a point."""
slots = ['_x', '_y']
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
p = Coordinate(4, 3)
q = Coordinate(0, 2)
p.x # notice no parenthesis!
p.y
p.x + q.x, p.y + q.y
In the previous example, we used @property
to access the data attributes of the Coordinate objects. This is not the only use of the annotation, we can use it for any accessor method that we want to invoke as if it was a data attribute.
class Name:
"""Class to represent a person's name."""
__slots__ = ['_f', '_m', '_l']
def __init__(self, first, last, middle=''):
self._f = first
self._m = middle
self._l = last
def initials(self):
if len(self._m):
return '{}. {}. {}.'.format(self._f[0], self._m[0], self._l[0]).upper()
return '{}. {}.'.format(self._f[0], self._l[0]).upper()
me = Name('Shikha', 'Singh')
me.initials()
iris = Name('Iris', 'Howley', 'K')
iris.initials()
iris.initials
Using @property. Let us annotate the initials class with @property
to make treat initials as if it is a data attribute of Name
class.
class Name:
"""Class to represent a person's name."""
__slots__ = ['_f', '_m', '_l']
def __init__(self, first, last, middle=''):
self._f = first
self._m = middle
self._l = last
@property
def initials(self):
if len(self._m):
return '{}. {}. {}.'.format(self._f[0], self._m[0], self._l[0]).upper()
else:
return '{}. {}.'.format(self._f[0], self._l[0]).upper()
iris = Name('Iris', 'Howley', 'K')
iris.initials # notice no parenthesis!
Let us apply the concept we have learn to finish up our coordinate class.
We have already implemented:
x(self)
: returns the x coordinate of the calling instance (annotated with @)y(self)
: returns the y coordinate of the calling instance (annotated with @)__str__
: print representation of the classWe will now implement:
_subX(self, other)
: subtracts the x coordinates of self and other and returns the answer_subY(self, other)
: subtracts the y coordinate of self and other and returns the answerdist(self, others)
: computes the Euclidean distance between coordinates self and otherradius(self)
: computes and returns the radius of the calling coordindate (annotated with @)Let us define a method distance
for the Coordinate
class which when called on an object of the class computes the Euclidean distance between that object and another point. Recall that the Eucliden distance between two points $p_1 = (x_1, y_1)$ and $p_2 = (x_2, y_2)$ is
class Coordinate(object):
"""Represents the coordinates of a point."""
__slots__ = ['_x', '_y']
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
def _subX(self, other):
"""Subtracts the x coordinates of self
and other and returns the answer"""
return self._x - other._x
def _subY(self, other):
"""Subtracts the y coordinates of self
and other and returns the answer"""
return self._y - other._y
def dist(self, other):
sqX = self._subX(other)**2
sqY = self._subY(other)**2
return round((sqX + sqY)**0.5, 2)
@property
def radius(self):
"""Returns the distance of the point from (0,0)"""
origin = Coordinate(0,0)
return self.dist(origin)
def __str__(self):
return '<{}, {}>'.format(self._x, self._y)
p1 = Coordinate(0,2)
p2 = Coordinate(3, 4)
p1.x
p2.y
p1.dist(p2) # call distance on object pt, zero is argument
round(13**0.5,2)
p1.radius
p2.radius
Today we saw how Python supports data abstraction (separating the data and details of the implementation from the user) via :
@property
annotation