Γλώσσα Προγραμματισμού ΙΙ

Διάλεξη 21ης/23ης Απριλίου 2015 - Κλάσεις και αντικείμενα

Η Python παρέχει τη δυνατότητα στους χρήστες της γλώσσας να οργανώνουν τα προγράμματά τους γύρω από τις λεγόμενες κλάσεις (classes), δηλαδή δομές που ενσωματώνουν τόσο δεδομένα όσο και μεθόδους (ή συναρτήσεις) που δρούν πάνω σε αυτά τα δεδομένα. Εδώ θα χρησιμοποιήσουμε τις κλάσεις ως αφηρημένους τύπους δεδομένων για τον ορισμό αντικειμένων (objects) που, όπως είπαμε, περιέχουν δεδομένα και μεθόδους που δρουν πάνω σε αυτά τα δεδομένα. Η ιδέα αυτή είναι αρκετά παλιά (μέσα της δεκαετίας του 1970) αλλά μόνο σχετικά πρόσφατα με την γλώσσα προγραμματισμού C++ και ιδίως την Python άρχισε αυτή η τεχνική προγραμματισμού (αντικειμενοστραφής προγραμματισμός) να γίνεται δημοφιλής.

Η αλήθεια είναι πάντως ότι έχουμε χρησιμοποιήσει την ιδέα αυτή και πριν: για παράδειγμα για να βρούμε πόσες φορές περιέχεται ένα στοιχείο σε ένα αντικείμενο τύπου list γράψαμε π.χ. ['a', 'b', 'c', 'd', 'c'].count('c').

Για να ορίσουμε μια κλάση γράφουμε μετά τη λέξη-κλειδί class τό όνομά της και ορίζουμε τόσο τα δεδομένα της κλάσης όοο και τυχόν μεθόδους που ορίζονται για αντικείμενα της συγκεκριμένης κλάσης. Για παράδειγμα, σε κάποιο πρόγραμμα ίσως ήταν χρήσιμο να αναφερθούμε σε δομές που αντιπροσωπεύουν ένα υπάλληλο μιας εταιρείας. Θα μπορούσαμε λοιπόν να γράψουμε


class Person:
	def setName(self, name):
		self.name = name
	def getName(self):
		return self.name
	def greet(self):
		print 'Hello, world!. I am %s.' % self.name

Ας προσπαθήσουμε να καταλάβουμε τι είναι ο παραπάνω ορισμός. Στο παραπάνω παράδειγμα ορίζουμε τρεις μεθόδους, οι οποίες μοιάζουν με οριμούς συναρτήσεων με τη μόνη διαφορά ότι περιέχονται στο σώμα της εντολής class. Η λέξη Person είναι φυσικά το όνομα της συγκεκριμένης κλάσης. Η παράμετρος self αναφέρεται στο αντικείμενο (με τύπο Person) το οποίο ορίζουμε:


>>> a = Person()
>>> b = Person()
>>> a.setName('Luke Skywalker')
>>> b.setName('Darth Vader')
>>> a.greet()
Hello, world! I'm Luke Skywalker.
>>> b.greet()
Hello, world! I'm Darth Vader.
>>> a.getName()
Luke Skywalker

Με τις παραπάνω εντολές ορίσαμε δύο αντικείμενα τύπου Person. Το μοναδικό δεδομένο ή χαρακτηριστικό (attribute) για τα δύο αυτά αντικείμενα είναι ένα string η τιμή του οποίου αποτελεί το όνομα του αντικειμένου. Παρατηρήστε ότι στη μέθοδο setName ανατίθεται στο δεδομένο self.name η τιμή της παραμέτρου name, του δευτέρου δηλαδή ορίσματος της μεθόδου. Γίνεται αντιληπτό λοιπόν ότι χωρίς την παράμετρο self δεν θα ήταν δυνατή η πρόσβαση στα χαρακτηριστικά ενός αντικειμένου, στο παράδειγμά μας το string name.

Η Python μας δίνει όμως τη δυνατότητα της αρχικοποίησης των χαρακτηριστικών ενός αντικειμένου τη στιγμή της δημιουργίας του με την ειδική μέθοδο __init__. Η μέθοδος αυτή ονομάζεται κατασκευαστής (constructor). Στο παράδειγμα που ακολουθεί η κλάσει Rectangle (παραλληλόγραμμο) αρχικοποιεί τα χαρακτηριστικά της width (πλάτος) και height (μήκος) σε μηδέν με χρήση του κατασκευαστή:


class Rectangle:
	def __init__(self):
		self.width = 0
		self.height = 0
	def getWidth(self):
		return self.width
	def setWidth(self, size):
		self.width = size

Ένα παράδειγμα χρήσης αυτής της κλάσης είναι το ακόλουθο:


>>> r = Rectangle()
>>> r.width
0
>>> r.setWidth(2)
>>> r.getWidth()
2

Είναι φυσικά δυνατόν η μέθοδος __init__ να δεχθεί ορίσματα επιπλέον του self, όπως στην κλάση Rational που ορίζεται ως


class Rational:
	def __init__(self, n, d=1):
		self.num = n
		self.den = d

και μας δίνει τη δυνατότητα να γράψουμε, για παράδειγμα,


>>> p = Rational(2,5)
>>> q = Rational(3)
>>> p.den
5
>>> q.den
1

Στο παράδειγμα που ακολουθεί χρησιμοποιούμε μια ακόμα από τις μεθόδους που ορίζει η Python, την __str__, η οποία ορίζει τον τρόπο με τον οποίο τυπώνεται ένα αντικείμενο του δεδομένου τύπου:


class intSet:
	def __init__(self):
		self.vals = []
	def insert(self, e):
		self.vals.append(e)
	def member(self, e):
		return e in self.vals
	def remove(self, e):
		self.vals.remove(e)
	def __str__(self):
		self.vals.sort()
		result = ''
		for e in self.vals:
			result = result + str(e) + ','
		return '{' + result[:-1] + '}'

Όταν στην συνάρτηση print δωθεί ένα όρισμα τύπου intSet, τότε το αντικείμενο τυπώνεται σύμφωνα με τον ορισμό της __str__:


>>> s = intSet()
>>> s.insert(3)
>>> s.insert(4)
>>> print s
{3,4}

Στο παράδειγμα που ακολουθεί βλέπουμε πως μπορούμε να χρησιμοποιήσουμε μια κλάση για να αναπαραστήσουμε μια συνάρτηση, συγκεκριμένα τη συνάρτηση $y(t; v_0) = v_0 t - \frac{1}{2} g t^2$ η οποία δίνει το ύψος ενός αντικειμένου όταν εκτοξευτεί προς τα πάνω με αρχική ταχύτητα $v_0$. Παρατηρήστε ότι τις παραμέτρους $v_0$ και $g$ τις χειριζόμαστε ως δεδομένα:


class Y:
	def __init__(self, v0):
		self.v0 = v0
		self.g = 9.81
	def value(self, t):
		return self.v0 * t - 0.5 * self.g * t**2

Μπορούμε λοιπόν να γράψουμε y = Y(3) για να κατασκευάσουμε ένα αντικείμενο το οποίο αντιπροσωπεύει τη συνάρτηση $y(t; 3)$ και v = y.value(0.1) για να υπολογίσουμε την τιμή $y(0.1; 3)$. Αξίζει να σημειώσουμε εδώ ότι η Python μεταφράζει την εντολή y = Y(3) σε Y.__init__(y,3) και την εντολή v = y.value(0.1) σε v = Y.value(y, 0.1).

Σημειώνουμε τέλος, ότι κάθε κλάση περιέχει το χαρακτηριστικό __dict__ το οποίο είναι ένα λεξικό το οποίο περιέχει το όνομα και την τιμή κάθε χαρακτηριστικού της κλάσης. Για παράδειγμα, για την κλάση Y που ορίσαμε παραπάνω έχουμε


>>> y = Y(2)
>>> print y.__dict__
{'v0': 2, 'g': 9.81}

Οι κανόνες που διέπουν τη χρήση της παραμέτρου self έχουν ως εξής:

Η χρήση των κλάσεων για τη λύση προβλημάτων στα μαθηματικά και στις φυσικές επιστήμες, είναι μάλλον λιγότερο από ιδανική, όπως μαρτυρεί και το προηγούμε παράδειγμα, αλλά υπάρχουν πολλά άλλα προβλήματα όπου η χρήση κλάσεων για την προσομοίωση αντικειμένων είναι εξαιρετικά χρήσιμ ιδέα. Ας πάρουμε για παράδειγμα, το πρόβλημα της προσομοίωσης ενός τραπεζικού λογαριασμού. Θα θέλαμε, κατ' ελάχιστο, ως χαρακτηριστικά της κλάσης, το όνομα του ιδιοκτήτη του λογαριασμού, τον αριθμό του λογαριαμού και το υπόλοιπό του, όπως και τις λειτουργίες της ανάληψης ή κατάθεσης χρημάτων στο λογαριασμό. Μια πρώτη προσέγγιση ακολουθεί παρακάτω:


class BankAccount:
	def __init__(self, name, number, iniiial_amount):
		self.name = name
		self.num = number
		self.balance = initial_amount

	def deposit(self, amount):
		self.balance += amount

	def withdraw(self, amount):
		self.balance -= amount

	def __str__(self):
		s = '%s, %s, balance: %s' % (self.name, self.num, self.balance)
		return s
 

Ένα τυπικό παράδειγμα χρήσης της κλάσης BankAccount ακολουθεί:


>>> a1 = BankAccount('John Plex', '19371554951', 20000)
>>> a2 = BankAccount('Mike Plex', '19371563268', 40000)
>>> a1.deposit(1200)
>>> a2.withdraw(3000)
>>> print a1
John Plex, 19371554951, balance: 21200
>>> print a2
Mike Plex, 19371563268, balance: 37000
 

Φυσικά, ο συγγραφέας της κλάσης BankAccount θα ήθελε οι χρήστες της να χρησιμοποιούν μόνο τον κατασκευαστή και τις μεθόδους deposit και withdraw, αλλά τίποτα δεν εμποδίζει ένα κακόβουλο χρήση να αλλάξει την τιμή του χαρακτηριστικού balance αυθαίρετα. Μια μερική λύση στο συγκεκριμένο πρόβλημα παρέχεται από την Python με τα λεγόμενα προστατευμένα χαρακτηριστικά (protected attributes) στα οποία ο πρώτος χαρακτήρας του ονόματος ενός χαρακτηριστικού είναι ο χαρακτήρας '_' (κάτω παύλα) και προειδοποιεί τον χρήστη για την αδυναμία αλλαγής του συγκεκριμένου χαρακτηριστικού. Έτσι, θα ήταν προτιμότερο να γράψουμε:


class ProtectedBankAccount:
	def __init__(self, name, number, iniiial_amount):
		self._name = name
		self._num = number
		self._balance = initial_amount

	def deposit(self, amount):
		self._balance += amount

	def withdraw(self, amount):
		self._balance -= amount

	def get_balance(self):
		return self._balance

	def __str__(self):
		s = '%s, %s, balance: %s' % (self._name, self._num, self._balance)
		return s
 

Ένας τηλεφωνικός κατάλογος


class Person:
	def __init__(self, name, mobile_phone=None, office_phone=None, private_phone=None, email=None):
		self.name = name
		self.mobile = mobile_phone
		self.office = office_phone
		self.private = private_phone
		self.email = email

	def add_mobile_phone(self, number):
		self.mobile = number

	def add_office_phone(self, number):
		self.office = number

	def add_private_phone(self, number):
		self.private = number

	def add_email(self, address):
		self.email = address

>>> p1 = Person('Hans Hanson', office_phone='767828283', email=’h@hanshanson.com’)
>>> p2 = Person('Ole Olsen', office_phone='767828292')
>>> p2.add_email('olsen@somemail.net')

Δεδομένης της παραπάνω κλάσης, ένας τηλεφωνιικός κατάλογος μπορεί να φτιαχτεί ως λίστα ή λεξικό, για παράδειγμα, phone_book = [p1, p2]. Αν προσθέσουμε και μια μέθοδο για την εκτύπωση των χαρακτηριστικών ενός αντικειμένου τύπου Person, όπως για παράδειγμα


class Person:
	...
	def printPerson(self):
		s = self.name + '\n'
		if self.mobile is not None:
			s += 'mobile phone:   %s\n' % self.mobile
		if self.office is not None:
			s += 'office phone:   %s\n' % self.office
		if self.private is not None:
			s += 'private phone:  %s\n' % self.private
		if self.email is not None:
			s += 'email address:  %s\n' % self.email
		print s

τότε μπορούμε να εκτυπώσουμε τον τηλεφωνικό κατάλογο με μια ανακύκλωση:


>>> for person on phone_book:
...	person.printPerson()
...
Hans Hanson
office phone:   767828283
email address:  h@hanshanson.com

Ole Olsen
office phone:   767828292
email address:  olsen@somemail.net

Μια κλάση για πολυώνυμα

Μπορούμε να αποθηκεύσουμε του συντελεστές του πολυωνύμου σε μια λίστα. Το στοιχείο στη θέση i θα είναι, φυσικά, ο συντελεστής του όρου $x^i$. 'Ετσι, θα θέλαμε η κλήση Polynomial([1, 0, -1, 2]) να ορίζει το πολυώνυμο $1 - x^2 + 2x^3$. Θέλουμε ακόμα να μπορούμε να υπολογίζουμε τιμές του πολυωνύμου όπως κάναμε νωρίτερα στην κλάση Y. Αυτή τη φορά όμως θα χρησιμοποιήσουμε την ειδική μέθοδο __call__.


class Polynomial:
	def __init__(self, coeff):
		self.coeff = coeff

	def __call__(self, x):
		v = 0
		for i in range(len(self.coeff)):
			v += self.coeff[i]*x**i
		return v

>>> p = Polynomial([-1, 0, 1, 2])
>>> p(2)
18

Δείτε ότι με τη χρήση της ειδικής συνάρτησης __call__ το αντικείμενο p συμπεριφέρεται ακριβώς ως συνάρτηση. Σημειώνουμε επίσης ότι η υλοποίηση της μεθόδου δεν είναι η καλύτερη δυνατή μια και ο πιο οικονομικός τρόπος υπολογισμού της τιμής ενός πολυωνύμου χρησιμοποιεί το σχήμα Horner. Αφήνουμε την υλοποίηση της συγκεκριμένης μεθόδου ως άσκηση για τον ενδιαφερόμενο αναγνώστη.

Με την ειδική μέθοδο __add__ μπορούμε να προσθέσουμε πολυώνυμα:


class Polynomial:
	...
	def __add__(self, other):
		if len(self.coeff) > len(other.coeff):
			result_coeff = self.coeff[:]
			for i in range(len(other.coeff)):
				result_coeff[i] += other.coeff[i]
		else:
			result_coeff = other.coeff[:]
			for i in range(len(self.coeff)):
				result_coeff[i] += self.coeff[i]
		return Polynomial(result_coeff)

>>> p1 = Polynomial([1, -1])
>>> p2 = Polynomial([0, 1, 0, 0, ー6, -1])
>>> p3 = p1 + p2
>>> print p3.coeff
[1, 0, 0, 0, ー6, -1]

Παραθέτουμε μια ακόμα μέθοδο η οποία εκτυπώνει ένα πολυώνυμο (προσπαθήστε να καταλάβετε τι ακριβώς κάνει):


class Polynomial:
	...
	def __str__(self):
		s = ''
		for i in range(len(self.coeff)):
			if self.coeff[i] != 0:
				s += ' + %g*x^%d' % (self.coeff[i], i)
		s = s.replace('+ -', '- ')
		s = s.replace('x^0', '1')
		s = s.replace(' 1*', ' ')
		s = s.replace('x^1 ', 'x ')

		if s[0:3] == ' + ':
			s = s[3:]
		if s[0:3] == ' - ':
			s = '-' + s[3:]
		return s

>>> p = Polynomial([1, -1, 2])
>>> print p
1 - x + 2*x^2