程式扎記: [Quick Python] 15. Classes and object-oriented programming - Part 1

標籤

2012年2月22日 星期三

[Quick Python] 15. Classes and object-oriented programming - Part 1


Preface : 
In this chapter, we discuss Python classes, which can be used in a manner analogous to C structures but which can also be used in a full object-oriented manner. For the benefit of readers who aren’t object-oriented programmers, we’ll discuss the use of classes as structures in the first two subsections. The remainder of the chapter discusses OOP in Python. This is only a description of the constructs available in Python; it’s not an exposition on object-oriented programming itself. 

This chapter covers : 
* Defining classes
* Using instance variables and @property
* Defining methods
* Defining class variables and methods
* Inheriting from other classes
* Making variables and methods private
* Inheriting from multiple classes

Defining classes : 
class in Python is effectively a data type. All the data types built into Python are classes, and Python gives you powerful tools to manipulate every aspect of a class’s behavior. You define a class with the class statement : 
  1. class MyClass:  
  2.     body  
body is a list of Python statements, typically variable assignments and function definitions. No assignments or function definitions are required. The body can be just a single pass statement. By convention, class identifiers are in CapCase—that is, the first letter of each component word is capitalized, to make them stand out. After you define the class, a new object of the class type (an instance of the class) can be created by calling the class name as a function : 
instance = MyClass()

- Using a class instance as a structure or record 
Class instances can be used as structures or records. Unlike C structures, the fields of an instance don’t need to be declared ahead of time but can be created on the fly. The following short example defines a class called Circle, creates a Circle instance, assigns to the radius field of the circle, and then uses that field to calculate the circumference of the circle : 
 

Like Java and many other languages, the fields of an instance/structure are accessed and assigned to by using dot notation. You can initialize fields of an instance automatically by including an __init__ initialization method in the class body. This function is run every time an instance of the class is created, with that new instance as its first argument. The __init__ method is similar to a constructor in Java, but it doesn’t really construct anything—it initializes fields of the class. This example creates circles with a radius of 1 by default : 
  1. class Circle:  
  2.     def __init__(self):  # (1)  
  3.         self.radius = 1  
  4. my_circle = Circle()  # (2)  
  5. print(2 * 3.14 * my_circle.radius)  # (3)  
  6. my_circle.radius = 5  # (4)  
  7. print(2 * 3.14 * my_circle.radius)  # (5)  
By convention, self is always the name of the first argument of __init__self is set to the newly created circle instance when __init__ is run (1). Next, the code uses the class definition. We first create a Circle instance object (2). The next line makes use of the fact that the radius field is already initialized (3). We can also overwrite the radius field (4); as a result, the last line prints a different result than the previous print() statement (5). 

Instance variables : 
Instance variables are the most basic feature of OOP. Take a look at the Circle class again : 
  1. class Circle:  
  2.     def __init__(self):  
  3.         self.radius = 1  
radius is an instance variable of Circle instances. That is, each instance of the Circle class has its own copy of radius, and the value stored in that copy may be different from the values stored in the radius variable in other instances. In Python, you can create instance variables as necessary by assigning to a field of a class instance : 
instance.variable = value

If the variable doesn’t already exist, it’s created automatically. This is how __init__ creates the radius variable. 

All uses of instance variables—both assignment and access—require explicit mention of the containing instance—that is, instance.variableA reference to variable by itself is a reference not to an instance variable but rather to a local variable in the executing method. This is different from C++ or Java, where instance variables are referred to in the same manner as local method function variables. I rather like Python’s requirement for explicit mention of the containing instance, because it clearly distinguishes instance variables from local function variables : 
 

Methods : 
method is a function associated with a particular class. You’ve already seen the special __init__ method, which is called on a new instance when that instance is first created. In the following example, we define another method, area, for the Circle class, which can be used to calculate and return the area for any Circle instance. Like most user-defined methods, area is called with a method invocation syntax that resembles instance variable access : 
 

Method invocation syntax consists of an instance, followed by a period, followed by the method to be invoked on the instance. The previous syntax is sometimes calledbound method invocation. area() can also be invoked as an unbound method by accessing it through its containing class. This is less convenient and is almost never done. When a method is invoked in this manner, its first argument must be an instance of the class in which that method is defined : 
>>> print(Circle.area(c))
28.27431

Like __init__, the area() method is defined as a function within the body of the class definition. The first argument of any method is the instance it was invoked by or on, named self by convention. Methods can be invoked with arguments, if the method definitions accept those arguments. 
This version of Circle adds an argument to the __init__ method, so that we can create circles of a given radius without needing to set the radius after a circle is created :
  1. class Circle:  
  2.     def __init__(self, radius):  
  3.         self.radius = radius  
  4.     def area(self):  
  5.         return self.radius * self.radius * 3.14159  
Note the two uses of radius here. self.radius is the instance variable called radiusradius by itself is the local function variable called radius. The two aren’t the same! In practice, we’d probably call the local function variable something like r or rad, to avoid any possibility of confusion. Using this definition of Circle, we can create circles of any radius with one call on the circle class. The following creates a Circle of radius 5 : 
c = Circle(5)

All the standard Python function features—default argument values, extra arguments, keyword arguments, and so forth—can be used with methods. For example, we could have defined the first line of __init__ to be : 
  1. def __init__(self, radius=1):  
Then, calls to circle would work with or without an extra argument; Circle() would return a circle of radius 1, and Circle(3) would return a circle of radius 3. 

There’s nothing magical about method invocation in Python. It can be considered shorthand for normal function invocation. Given a method invocationinstance.method(arg1, arg2, . . .), Python transforms it into a normal function call by using the following rules : 
1. Look for the method name in the instance namespace. If a method has been changed or added for this instance, it’s invoked in preference over methods in the class or superclass. This is the same sort of lookup discussed later in section 15.4.1.
2. If the method isn’t found in the instance namespace, look up the class type class of instance, and look for the method there. In the previous examples,
class is Circle—the type of the instance c.
3. If the method still isn’t found, look for the method in the superclasses.
4. When the method has been found, make a direct call to it as a normal Python, using instance as the first argument of the function, and shifting all the other arguments in the method invocation one space over to the right. So, instance.method(arg1, arg2, . . .) becomes class.method(instance, arg1, arg2, . . .).

Class variables : 
class variable is a variable associated with a class, not an instance of a class, and is accessed by all instances of the class, in order to keep track of some class-level information, such as how many instances of the class have been created at any point in time. Python provides class variables, although using them requires slightly more effort than in most other languages. Also, you need to watch out for an interaction between class and instance variables. 

A class variable is created by an assignment in the class body, not in the __init__ function; after it has been created, it can be seen by all instances of the class. We can use a class variable to make a value for pi accessible to all instances of the Circle class : 
  1. class Circle:  
  2.     pi = 3.14159  
  3.     def __init__(self, radius):  
  4.         self.radius = radius  
  5.     def area(self):  
  6.         return self.radius * self.radius * Circle.pi  
With the above definition entered, we can type : 
>>> Circle.pi
3.14159
>>> Circle.pi = 4
>>> Circle.pi
4
>>> Circle.pi = 3.14159
>>> Circle.pi
3.14159

This is exactly how we would expect a class variable to act. It’s associated with and contained in the class that defines it. Notice in this example that we’re accessingCircle.pi before any circle instances have been created. Obviously, Circle.pi exists independently of any specific instances of the Circle class. 

You can also access a class variable from a method of a class, through the class name. We do so in the definition of Circle.area(), where the area function makes specific reference to Circle.pi. In operation, this has the desired effect; the correct value for pi is obtained from the class and used in the calculation : 
>>> c = Circle(3)
>>> c.area()
28.27431

You may object to hardcoding the name of a class inside that class’s methods. You can avoid doing so through use of the special __class__ attribute, available to all Python class instances. This attribute returns the class of which the instance is a member, for example : 
>>> Circle
 # 這邊因為我的 Circle 是從 Exam01 import 進來.
>>> c.__class__

The class named Circle is represented internally by an abstract data structure, and that data structure is exactly what is obtained from the __class__ attribute of c, an instance of the Circle class. This lets us obtain the value of Circle.pi from c without ever explicitly referring to the Circle class name : 
>>> c.__class__.pi
3.14159

Of course, we could use this internally in the area method to get rid of the explicit reference to the Circle class; replace Circle.pi with self.__class__.pi

- An oddity with class variables 
There’s a bit of an oddity with class variables that can trip you up if you aren’t aware of it. When Python is looking up an instance variable, if it can’t find an instance variable of that name, it will then try to find and return the value in a class variable of the same name. Only if it can’t find an appropriate class variable will it signal an error. This does make it efficient to implement default values for instance variables; just create a class variable with the same name and appropriate default value, and avoid the time and memory overhead of initializing that instance variable every time a class instance is created. But this also makes it easy to inadvertently refer to an instance variable rather than a class variable, without signaling an error. Let’s look at how this operates in conjunction with the previous example. 

First, we can refer to the variable c.pi, even though c doesn’t have an associated instance variable named pi. Python will first try to look for such an instance variable, but when it can’t find it, it will then look for a class variable pi in Circle and find it : 
>>> c = Circle(3)
>>> c.pi # It refer to Circle.pi
3.14159

(This may or may not be what you want; it’s convenient but can be prone to error, so be careful.

Now, what happens if we attempt to use c.pi as a true class variable, by changing it from one instance with the intent that all instances should see the change? Again, we’ll use the earlier definition for Circle : 
>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> c1.pi = 3.14 # 此時是對 instance c1 新增一個 field 為 pi, 而不是類別 Circle.pi!
>>> c1.pi
3.14
>>> c2.pi
3.14159 # 此時 instance c2 的 pi 還是參考到 Circle.pi.
>>> Circle.pi
3.14159

This doesn’t work as it would for a true class variable—c1 now has its own copy of pi, distinct from the Circle.pi accessed by c2. This is because the assignment to c1.picreates an instance variable in c1; it doesn’t affect the class variable Circle.pi in any way. Subsequent lookups of c1.pi return the value in that instance variable, whereas subsequent lookups of c2.pi look for an instance variable pi in c2, fail to find it, and resort to returning the value of the class variable Circle.pi. If you want to change the value of a class variable, access it through the class name, not through the instance variable self. 

Static methods and class methods : 
Python classes can also have methods that correspond explicitly to static methods in a language such as Java. In addition, Python has class methods, which are a bit more advanced. 

- Static methods 
Just as in Java, you can invoke static methods even though no instance of that class has been created, although you can call them using a class instance. To create a static method, use the @staticmethod decorator, as shown in listing below : 
- Exam02.py :
  1. """circle module: contains the Circle class."""  
  2. class Circle:  
  3.     """Circle class"""  
  4.     all_circles = []  
  5.     pi = 3.14159  
  6.     def __init__(self, r=1):  
  7.         """Create a Circle with the given radius"""  
  8.         self.radius = r  
  9.         self.__class__.all_circles.append(self)  
  10.     def area(self):  
  11.         """determine the area of the Circle"""  
  12.         return self.__class__.pi * self.radius * self.radius  
  13.     @staticmethod  
  14.     def total_area():  
  15.         total = 0  
  16.         for c in Circle.all_circles:  
  17.             total = total + c.area()  
  18.         return total  

Now, interactively type the following : 
>>> from Exam02 import Circle
>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> Circle.total_area()
15.70795
>>> c2.radius = 3
>>> Circle.total_area()
31.415899999999997

Also notice that documentation strings are used. In a real module, you’d probably put in more informative strings, indicating in the class docstring what methods are available and including usage information in the method docstrings : 
>>> import Exam02
>>> Exam02.__doc__
'circle module: contains the Circle class.'
>>> Exam02.Circle.__doc__
'Circle class'
>>> Exam02.Circle.area.__doc__
'determine the area of the Circle'

- Class methods 
Class methods are similar to static methods in that they can be invoked before an object of the class has been instantiated or by using an instance of the class. But class methods are implicitly passed the class they belong to as their first parameter, so you can code them more simply, as in listing below : 
- Exam03.py :
  1. """circle module: contains the Circle class."""  
  2. class Circle:  
  3.     """Circle class"""  
  4.     all_circles = []  
  5.     pi = 3.14159  
  6.     def __init__(self, r=1):  
  7.         """Create a Circle with the given radius"""  
  8.         self.radius = r  
  9.         self.__class__.all_circles.append(self)  
  10.     def area(self):  
  11.         """determine the area of the Circle"""  
  12.         return self.__class__.pi * self.radius * self.radius  
  13.     @classmethod  #(1)  
  14.     def total_area(cls):  #(2)  
  15.         total = 0  
  16.         for c in cls.all_circles:  #(3)  
  17.             total = total + c.area()  
  18.         return total  

The @classmethod decorator is used before the method def (1). The class parameter is traditionally cls (2). You can use cls instead of self.__class__ (3). 

By using a class method instead of a static method, we don’t have to hardcode the class name into total_area. That means any subclasses of Circle can still call total_areaand refer to their own members, not those in Circle : 
>>> from Exam03 import Circle
>>> c1 = Circle(1)
>>> c2 = Circle(2)
>>> Circle.total_area()
15.70795
>>> c2.radius = 3
>>> Circle.total_area()
31.415899999999997

Inheritance : 
Inheritance in Python is easier and more flexible than inheritance in compiled languages such as Java and C++ because the dynamic nature of Python doesn’t force as many restrictions on the language. To see how inheritance is used in Python, we start with the Circle class given previously and generalize. We might want to define an additional class for squares : 
  1. class Square:  
  2.     def __init__(self, side=1):  
  3.         self.side = side  
Now, if we want to use these classes in a drawing program, they must define some sense of where on the drawing surface each instance is. We can do so by defining an x coordinate and a y coordinate in each instance : 
  1. class Square:  
  2.     def __init__(self, side=1, x=0, y=0):  
  3.         self.side = side  
  4.         self.x = x  
  5.         self.y = y  
  6. class Circle:  
  7.     def __init__(self, radius=1, x=0, y=0):  
  8.         self.radius = radius  
  9.         self.x = x  
  10.         self.y = y  
This approach works but results in a good deal of repetitive code as we expand the number of shape classes, because each shape will presumably want to have this concept of position. No doubt you know where we’re going here. This is a standard situation for using inheritance in an object-oriented language. Instead of defining thex and y variables in each shape class, abstract them out into a general Shape class, and have each class defining an actual shape inherit from that general class. In Python, that looks like this : 
  1. class Shape:  
  2.     def __init__(self, x, y):  
  3.         self.x = x  
  4.         self.y = y  
  5. class Square(Shape): #Says Square inherits from Shape  
  6.     def __init__(self, side=1, x=0, y=0):  
  7.         super().__init__(x, y)  #Must call __init__ method of Shape  
  8.         self.side = side  
  9. class Circle(Shape): #Says Circle inherits from Shape  
  10.     def __init__(self, r=1, x=0, y=0):  
  11.         super().__init__(x, y) # Must call __init__ method of Shape  
  12.         self.radius = r  
There are (generally) two requirements in using an inherited class in Python, both of which you can see in the bolded code in the Circle and Square classes. The first requirement is defining the inheritance hierarchy, which you do by giving the classes inherited from, in parentheses, immediately after the name of the class being defined with the class keyword. In the previous code, Circle and Square both inherit from ShapeThe second and more subtle element is the necessity to explicitly call the __init__ method of inherited classes. Python doesn’t automatically do this for you, but you can use the super() function to have Python figure out which inherited class to use. This is accomplished in the example code by the super().__init__(x,y) lines. This calls the Shape initialization function with the instance being initialized and the appropriate arguments. If this weren’t done, then in the example, instances of Circle and Square wouldn’t have their x and y instance variables set. 

Instead of using super(), we could call Shape’s __init__ by explicitly naming the inherited class using Shape.__init__(self, x, y), which would also call the Shapeinitialization function with the instance being initialized. This wouldn’t be as flexible in the long run, because it hardcodes the inherited class’s name, which could be a problem later if the design and the inheritance hierarchy change. On the other hand, the use of super can be tricky in more complex cases. Because the two methods don’t exactly mix well, clearly document whichever approach you use in your code. 

Inheritance comes into effect when you attempt to use a method that isn’t defined in the base classes but is defined in the superclass. To see this, let’s define another method in the Shape class called move, which will move a shape by a given displacement. It will modify the x and y coordinates of the shape by an amount determined by arguments to the method. The definition for Shape now becomes : 
  1. class Shape:  
  2.     def __init__(self, x, y):  
  3.         self.x = x  
  4.         self.y = y  
  5.     def move(self, delta_x, delta_y):  
  6.         self.x = self.x + delta_x  
  7.         self.y = self.y + delta_y  
If we enter this definition for Shape and the previous definitions for Circle and Square, we can then engage in the following interactive session : 
>>> c = Circle(1)
>>> c.move(3, 4)
>>> c.x
3
>>> c.y
4

The Circle class didn’t define a move method immediately itself, but because it inherits from a class that implements move, all instances of Circle can make use of move

Inheritance with class and instance variables : 
Inheritance allows an instance to inherit attributes of the class. Instance variables are associated with object instances, and only one instance variable of a given name exists for a given instance. To see this, consider the following example. Using these class definitions : 
- Exam05.py :
  1. class P:  
  2.     z = "Hello"  
  3.     def set_p(self):  
  4.         self.x = "Class P"  
  5.     def print_p(self):  
  6.         print(self.x)  
  7.   
  8. class C(P):  
  9.     def set_c(self):  
  10.         self.x = "Class C"  
  11.     def print_c(self):  
  12.         print(self.x)  

execute the following code : 
 

The object c in this example is an instance of class CC inherits from P, but c doesn’t inherit from some invisible instance of class P. It inherits methods and class variables directly from P. Because there is only one instance (c), any reference to the instance variable x in a method invocation on c must refer to c.x. This is true regardless of which class defines the method being invoked on c. As you can see, when they’re invoked on c, both set_p and print_p, defined in class P, refer to the same variable referred to by set_c and print_c when they’re invoked on c

In general, this is what is desired for instance variables because it makes sense that references to instance variables of the same name should refer to the same variable. Occasionally, somewhat different behavior is desired, which you can achieve using private variables. These are explained in the next subsection. 

Class variables are inherited, but you should take care to avoid name clashes and be aware of a generalization of the same behavior you saw in the earlier subsection on class variables. In our example, a class variable z is defined for the superclass P. It can be accessed in three different ways: through the instance c, through the derived class C, or directly through the superclass P : 
>>> c.z; C.z; P.z
'Hello'
'Hello'
'Hello'

But if we try setting it through the class C, a new class variable will be created for the class C. This has no effect on P’s class variable itself (as accessed through P). But future accesses through the class C or its instance c will see this new variable rather than the original : 
>>> C.z = "John"
>>> c.z; C.z; P.z
'John'
'John'

'Hello'

Similarly, if we try setting z through the instance c, a new instance variable will be created, and we’ll end up with three different variables : 
>>> c.z = "Peter"
>>> c.z; C.z; P.z
'Peter'
'John'
'Hello'

Supplement : 
[Quick Python] 15. Classes and object-oriented programming - Part 2

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!