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, """ |