Designing with Classes

The previous chapter focuses on using Python’s OOP, the class. But OOP is also about design issues, i.e., how to use classes to model useful objects.

Python and OOP

Python’s implementation of OOP can be summarized by three ideas:

  • Inheritance

    Inheritance is based on attribute lookup in Python (in X.name expressions)

  • Polymorphism

    In X.method, the meaning of method depends on the type(class) of X.

  • Encapsulation

    Methods and operators implement behavior; data hiding is a convention by default

    Some OOP languages also define polymorhism to mean overloading functions based on the type signatures of their arguments. But because there are no type declarations in Python, this concept doesn’t really apply; Polymorhism in Python is based on object interfaces, not types

>>> class C:
...     def meth(self,x):
...       return x + x
...     def meth(self, x, y, z):
...       return x + y + z
>>> kda = C(); kad = C()
>>> print(kda.meth(2), kad.meth(2,3,4))
TypeError: meth() missing 2 required positional arguments: 'y' and 'z'

OOP and Inheritance: “Is a” Relationships

To illustrate, let’s put that pizza-making robot we talked about at the start of this part of the book to work. Suppose we’ve decided to explore alternative career paths and open a pizza restaurant. One of the first things we’ll need to do is hire employees to serve customers, prepare the food, and so on. Being engineers at heart, we’ve bdecided to build a robot to make the pizzas; but being politically and cybernetically correct, we’ve also decided to make our robot a full-fledged employee with a salary. Our pizza shop team can be defined by the four classes in the example file, employees.py. The most general class, Employee, provides common behavior such as bumping up salaries (giveRaise) and printing (__repr__). There are two kinds of employees, and so two subclasses of Employee: Chef and Server. Both override the inherited work method to print more specific messages. Finally, our pizza robot is modeled by an even more specific class: PizzaRobot is a kind of Chef,which is a kind of Employee. In OOP terms, we call these relationships “is-a” links:a robot is a chef, which is a(n) employee. Here’s theemployees.py file:

>>> class Employee:
...     def __init__(self, name, salary =0):
...         self.name = name
...         self.salary = salary
...     def giveRaise(self, percent):
...         self.salary = self.salary+(self.salary*percent)
...     def work(self):
...         print(self.name, "does stuff")
...     def __repr__(self):
...         return "<Employee: name=%s, salary = s%>" %(self.name, self.salary)
>>> class Chef(Employee):
...     def __init__(self,name):
...         Employee.__init__(self, name, 50000)
...     def work(self):
...         print(self.name, "makes food")
>>> class Server(Employee):
...     def __init__(self,name):
...         Employee.__init__(self, name, 40000)
...     def work(self):
...         print(self.name, "interfaces with customer")
>>> class PizzaRobot(Chef):
...     def __init__(self, name):
...         Chef.__init__(self, name)
...     def work(self):
...         print(self.name, "makes pizza")
>>> if __name__ == "__main__":
...     bob = PizzaRobot("bob")
...     print(bob)
...     bob.work()
...     bob.giveRaise(0.20)
...     print(bob); print()
...     for klass in Employee, Chef, Sever, PizzaRobot:
...         obj = klass(klass.__name__)
...         obj.work()

<Employee: name=bob, salary = 50000>
bob makes pizza
<Employee: name=bob, salary = 60000.0>

Employee does stuff
Chef makes food
Server interfaces with customer
PizzaRobot makes pizza

Stream Processors Revisited

For a more realistic composition example, recall the generic data stream processor function we partially coded in the introduction to OOP in Chapter 25:

>>> def processor(reader, converter, writer):
        while 1:
            data = reader.read()
            if not data: break
            data = converter(data)
            writer.write(data)

Rather than using a simple function here, we might code this as a class that uses composition to do its work to provide more structure and support inheritance. The following file, `streams.py`, demonstrates one way to code the class:

>>> class Processor:
    def __init__(self, reader, writer):
        self.reader = reader
        self.writer = writer
    def processor(self):
        while 1:
            data = self.reader.readline()
            if not data: break
            data = self.converter(data)
            self.writer.write(data)
    def converter(self,data):
        assert False, "converter must be defined" # Or raise exception

Why You Will Care: Classes and Persistence

I’ve mentioned pickling a few times in this part of the book because it works especially well with class instances. For example, besides allowing us to simulate real-world interactions,the pizza shop classes developed here could also be used as the basis of a persistent restaurant database.Instances of classes can be stored away on disk in a single step using Python’s pickle or shelve modules. The object pickling interface is remarkably easy to use:

>>> import pickle
>>> class Employee:
...  def __init__(self, name, salary =0):
...      self.name = name
...      self.salary = salary
...  def giveRaise(self, percent):
...      self.salary = self.salary+(self.salary*percent)
...  def work(self):
...      print(self.name, "does stuff")
...  def __repr__(self):
...      return "<Employee: name=%s, salary = %s>" %(self.name, self.salary)
>>> class Chef(Employee):
...  def __init__(self,name):
...      Employee.__init__(self, name, 50000)
...  def work(self):
...      print(self.name, "makes food")
>>> class Server(Employee):
...  def __init__(self,name):
...      Employee.__init__(self, name, 40000)
...  def work(self):
...      print(self.name, "interfaces with customer")
>>> class PizzaRobot(Chef):
...  def __init__(self, name):
...      Chef.__init__(self, name)
...  def work(self):
...      print(self.name, "makes pizza")
>>> if __name__ == "__main__":
...  bob = PizzaRobot("bob")
...  sara = Server("sara")
...  nick = Chef("nick")
...  tony = Employee("tony")
...  print(bob)
...  bob.work()
...  bob.giveRaise(0.20)
...  print(bob); print()
...  for klass in Employee, Chef, Server, PizzaRobot:
...      obj = klass(klass.__name__)
...      obj.work()
...  print("---------------------------------")
...  with open("1.pickle", "wb") as file:
...      pickle.dump((sara,nick,tony,bob),file)

...  with open("1.pickle", "rb") as file:
...      obj = pickle.load(file)
...  print(obj)

params are parameter names and “python” is value and if there are many parameters, just use “&” between two parameters.

>>> import requests
>>> r = requests.get("https://www.baidu.com/s?wd=python")
>>> url = "https://www.baidu.com/s"
>>> params = {"wd": "python"}
>>> r = requests.get(url, params=params)
>>> print(r.url)

Chapter 25. OOP:The Big Picture

So far in this book, we’ve been using the term “object” generically. Really, the code written up to this point has been object-based—we’ve passed objects around our scripts, used them in expressions, called their methods, and so on. For our code to qualify as being truly object-oriented (OO), though, our objects will generally need to also participate in something called an inheritance hierarchy.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
class C2:
	pass
class C3:
	pass
class C1(C2, C3):
	print("passing")

class C1(C2, C3):
	"""
	If a class wants to guarantee that an attribute like name is always set in its instances,
	it more typically will fill out the attribute at construction time, like this:
	"""
	def __init__(self, who): # Set name when constructed
		self.name = who 
I1 = C1("bob")
I2 = C1("mel")
print(I1.name, I2.name)

"""
The __init__ method is known as the constructor because of when it is run. It's the most commonly
used representative of a larger class of methods called operator overloading methods, 
"""

""" The definition of operator overloading methods:
Such methods are inherited in class trees as usual and have double underscores at the start and end
of their names to make them distinct. Python runs them automatically when instances that support them
appear in the corresponding operations, and they are mostly an alternative to using simple method calls.
They're also optional: if omitted, the operations are not supported.
pattern: __xxxx__

"""

class test:
	def __init__(self, x, y):
		self.x = x
		self.y = y
	def __setattr__(self, name, value):
		#self.name1 = name1
		#self.name2 = name2
		self.__dict__[name] = value
		object.__setattr__(self, name, value)
		object.__init__
		return self.__dict__[name]#, object.name
t1 = test(3,5)
t1.z = 23
print(t1.x, t1.y, t1.z)
print(dir(object.__setattr__))

"""
As an example, suppose you're assigned the task of implementing an employee database application.
As a Python OOP programmer, you might begin by coding a general superclass that defines default
behavior common to all the kinds of employees in your organization:
"""
class Employee:   # General superclass
	def computeSalary(self): # Common or default behavior
		pass
	def giveRaise(self):
		pass
	def promote(self):
		pass
	def retire(self):
		pass
"""
That is, you can code subclasses that customize just the bits of behavior that differ per employee type
the rest of the employee types' behavior will be inherited from the more general class. For example,
if engineers have a unique salary computation rule(i.e., not hours times rate), you can replace just that
one method in a subclass.
"""
class Engineer(Employee):
	def computeSalary(self):
		pass

"""
Because the computeSalary version here appears lower in the class tree, it will replace (override) the general
verison in Employee. You then create instances of the kinds of employee classes that the real employees belong to,
to get the correct behavior.
"""
bob = Employee()
mel = Engineer()

"""
when you later ask for these employees' salaries, they will be computed according to the classes from which the
objects were made, due to the principles of the inheritance search
"""
company = [bob, mel]  # A composite object
for emp in company:
	print(emp.computeSalary()) # Run this object's version
	


"""
This is yet another instance of the idea of polymorphism introduced in Chapter 4 and revisited in Chapter 16.
Recall that polymorphism means that the meaning of an operation depends on the object being 
operated on. Here, the method computeSalary is located by inheritance search in each object before it is called.
In other applications, polymorphism might also be used to hide(i.e., encapsulate) interface differences. For example,
a program that processes data streams might be coded to expect objects with input and output methods, without caring
what those methods actually do:
"""
def processor(reader, converter, writer):
	while 1:
		data = reader.read()
		if not data: break
		data = converter(data)
		writer.write(data)

class Reader:
	def read(self): pass  # Default behavior and tools
	def other(self): pass
class FileReader(Reader):
	def read(self): pass  # Read from a local file
class SocketReader(Reader):
	def read(self): pass # Read from a network socket

#processor(FileReader(), Converter, FileWriter())
#processor(SocketReader(), Converter, TapeWriter())
#proceesor(FtpReader(), Converter, XmlWriter())

"""
Inheritance hierarchy

Note that company list in this example could be stored in a file with Python object pickling, introduced
in Chapter 9 when we met files, to yield a persistent employee database. Python also comes with a module 
named shelve, which would allow you to store the pickled representation of the class instances in an access-
by-key filesystem; the third-party open source ZODB system does the same but has better support for production-
quality object-oriented databases.
"""

"""
Programming in such an OOP world is just a matter of combining and specializing already debugged code by writing
subclasses of your own
"""
"""
Objects at the bottom of the tree inherit attributes from objects higher up in the tree-a feature that enables us to
program by customizing code, rather than changing it, or starting from scratch.
"""

"""
2. Where does an inheritance search look for an attribute?
An inheritance search looks for an attribute first in the instance object, then in the class the instance was created
from, then in all higher superclasses, progressing from the bottom to the top of the object
tree, and from left to right (by default). The search stops at the first place the attribute is found. Because the lowest
version of a name found along the way wins, class hiearchies naturally support customization by extension.

3. What is the difference between a class object and an instance object?
Both class and instance objects are namespace(package of variablee that appear as attributes). The main difference between
is that classes are a kind of factory for creating multiple instances. Classes also support operator overloading methods,
which instances inherit, and treat any functions nested within them as special methods for processing instances.

4. Why is the first argument in a class method function special?
The first argument in a class method function is special because it always receives the instance object that is the implied
subject of the method call. It's usually called self by convention. Because method functions always have this implied subject
object context by default, we say they are "object-oriented" --i.e., designed to process or change objects.

5. What is the __init__ method used for?
If the __init__ method is coded or inherited in a class, Python calls it automatically each time an instance of that class is
created. It's known as the constructor method; it is passed the new instance implicitly, as well as any arguments passed explicitly
to the class name. It's also the most commonly used operator overloading method. If no __init__ method is present, instances simply 
begin life as empty namespaces.

6. How do you create a class instance?
You create a class instance by calling the class name as though it were a function; any arguments passed into the class name show up
as arguments two and beyond in the __init-_ constructor method. The new instance remembers the class it was created from for inheritance
purposes.

8. How do you specify a class's superclasses?
You specify a class's superclasses by listing them in parentheses in the class statement, after the new class's name. The left-to-right
order in which the classes are listed in the parentheses gives the left-to-right inheritance search order in the class tree,

"""