03python面向对象编程5

5.1 继承机制及其使用

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Python 的继承是多继承机制,即一个子类可以同时有多个直接父类。

Python 子类继承父类的语法是在定义子类时,将多个父类放在子类之后的圆括号里。语法格式如下:

In [2]:
# class Subclass (SuperClass1, SuperClass2, ...):
#     #类定义部分
 

从上面的语法格式来看,定义子类的语法非常简单,只需在原来的类定义后增加圆括号,井在圆括号中添加多个父类,即可表明该子类继承了这些父类。

如果在定义一个 Python 类时并未显式指定这个类的直接父类,则这个类默认继承 object 类。因此,object 类是所有类的父类,要么是其直接父类,要么是其间接父类。

实现继承的类被称为子类,被继承的类被称为父类(也被称为基类、超类)。父类和子类的关系,是一般和特殊的关系。例如水果和苹果的关系,苹果继承了水果,苹果是水果的子类,则苹果是一种特殊的水果。

由于子类是一种特殊的父类,因此父类包含的范围总比子类包含的范围要大,所以可以认为父类是大类,而子类是小类。

从实际意义上看,子类是对父类的扩展,子类是一种特殊的父类。从这个意义上看,使用继承来描述子类和父类的关系是错误的,用扩展更恰当。因此,这样的说法更加准确:Apple 类扩展了 Fruit 类。

 

下面程序示范了子类继承父类的特点。下面是 Fruit 类的代码:

In [3]:
class Fruit:
    def info(self):
        print("我是一个水果!重%g克" % self.weight)
class Food:
    def taste(self):
        print("不同食物的口感不同")
# 定义Apple类,继承了Fruit和Food类
class Apple(Fruit, Food):
    pass
In [4]:
# 创建Apple对象
a = Apple()
a.weight = 5.6
In [5]:
# 调用Apple对象的info()方法
a.info()
 
我是一个水果!重5.6克
In [6]:
# 调用Apple对象的taste()方法
a.taste()
 
不同食物的口感不同
 

5.2 关于Python的多继承

大部分面向对象的编程语言(除了 C++)都只支持单继承,而不支持多继承,这是由于多继承不仅增加了编程的复杂度,而且很容易导致一些莫名的错误。

Python 虽然在语法上明确支持多继承,但通常推荐如果不是很有必要,则尽量不要使用多继承,而是使用单继承,这样可以保证编程思路更清晰,而且可以避免很多麻烦。

当一个子类有多个直接父类时,该子类会继承得到所有父类的方法,这一点在前面示例中己经做了示范。现在的问题是,如果多个父类中包含了同名的方法,此时会发生什么呢?此时排在前面的父类中的方法会“遮蔽”排在后面的父类中的同名方法。例如如下代码:

In [13]:
class Item:
    def info (self):
        print("Item中方法:", '这是一个商品')
class Product:
    def info (self):
        print("Product中方法:", '这是一个工业产品')
In [14]:
class Mouse(Item, Product): # ①
    pass
m = Mouse()
m.info()
 
Item中方法: 这是一个商品
 

让 Mouse 继承了 Item 类和 Product 类,由于 Item 排在前面,因此 Item 中定义的方法优先级更好,Python 会优先到 Item 父类中搜寻方法,一旦在 Item 父类中搜寻到目标方法,Python 就不会继续向下搜寻了。

上面程序中 Item 和 Product 两个父类中都包含了 info() 方法,当 Mouse 子类对象调用 info() 方法时(子类中没有定义 info() 方法,因此 Python 会从父类中寻找 info() 方法),此时优先使用第一个父类 item 中的 info() 方法。

In [16]:
class Mouse(Product, Item):  # ①
    pass


m = Mouse()
m.info()
 
Product中方法: 这是一个工业产品
 

5.3 父类方法重写

子类扩展了父类,子类是一种特殊的父类。大部分时候,子类总是以父类为基础,额外增加新的方法。但有一种情况例外,子类需要重写父类的方法。

例如,鸟类都包含了飞翔方法,其中驼鸟是一种特殊的鸟类,因此驼鸟应该是鸟的子类,它也将从鸟类获得飞翔方法,但这个飞翔方法明显不适合驼鸟,为此,驼鸟需要重写鸟类的方法。

如下程序示范了子类重写父类的方法:

In [17]:
class Bird:
    # Bird类的fly()方法
    def fly(self):
        print("我在天空里自由自在地飞翔...")
        
class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("我只能在地上奔跑...")
In [18]:
# 创建Ostrich对象
os = Ostrich()
# 执行Ostrich对象的fly()方法,将输出"我只能在地上奔跑..."
os.fly()
 
我只能在地上奔跑...
 

这种子类包含与父类同名的方法的现象被称为方法重写(Override),也被称为方法覆盖。可以说子类重写了父类的方法,也可以说子类覆盖了父类的方法。

 

5.4 使用未绑定方法调用被重写的方法

如果在子类中调用重写之后的方法,Python 总是会执行子类重写的方法,不会执行父类中被重写的方法。如果需要在子类中调用父类中被重写的实例方法,那该怎么办呢?

别忘了,Python 类相当于类空间,因此 Python 类中的方法本质上相当于类空间内的函数。所以,即使是实例方法,Python 也允许通过类名调用。区别在于:在通过类名调用实例方法时,Python 不会为实例方法的第一个参数 self 自动绑定参数值,而是需要程序显式绑定第一个参数 self。这种机制被称为未绑定方法。

通过使用未绑定方法即可在子类中再次调用父类中被重写的方法。例如如下程序:

In [19]:
class BaseClass:
    def foo (self):
        print('父类中定义的foo方法')
        
class SubClass(BaseClass):
    # 重写父类的foo方法
    def foo (self):
        print('子类重写父类中的foo方法')
    def bar (self):
        print('执行bar方法')
        # 直接执行foo方法,将会调用子类重写之后的foo()方法
        self.foo()
        # 使用类名调用实例方法(未绑定方法)调用父类被重写的方法
        BaseClass.foo(self)
In [20]:
sc = SubClass()
sc.bar()
 
执行bar方法
子类重写父类中的foo方法
父类中定义的foo方法
 

上面程序中 SubClass 继承了 BaseClass 类,并重写了父类的 foo() 方法。接下来程序在 SubClass 类中定义了 bar() 方法,该方法的第 11 行代码直接通过 self 调用 foo() 方法,Python 将会执行子类重写之后的 foo() 方法;第 13 行代码通过未绑定方法显式调用 BaseClass 中的 foo 实例方法,井显式为第一个参数 self 绑定参数值,这就实现了调用父类中被重写的方法。

 

5.5 super函数:调用父类的构造方法

Python 的子类也会继承得到父类的构造方法,如果子类有多个直接父类,那么排在前面的父类的构造方法会被优先使用。例如如下代码:

In [48]:
class Employee :
    def __init__ (self, salary):
        self.salary = salary
    def work (self):
        print('普通员工正在写代码,工资是:', self.salary)
        
class Customer:
    def __init__ (self, favorite, address):
        self.favorite = favorite
        self.address = address
    def info (self):
        print('我是一个顾客,我的爱好是: %s,地址是%s' % (self.favorite, self.address))
In [49]:
# Manager继承了Employee、Customer
class Manager (Employee, Customer):
    pass

m = Manager(25000)
m.work()  #①
 
普通员工正在写代码,工资是: 25000
In [50]:
m.info()
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-50-00c123459052> in <module>
----> 1m.info()

<ipython-input-48-4616f2c22e21> in info(self)
     10         self.address = address
     11     def info (self):
---> 12print('我是一个顾客,我的爱好是: %s,地址是%s' % (self.favorite, self.address))

AttributeError: 'Manager' object has no attribute 'favorite'
In [51]:
class Manager (Customer, Employee):
    pass
In [52]:
m = Manager('IT产品', '广州')
m.info()  #②
 
我是一个顾客,我的爱好是: IT产品,地址是广州
In [53]:
class Manager (Employee, Customer):
    pass

m = Manager('IT产品', '广州')
m.info() 
 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-53-8db72262183d> in <module>
      2     pass
      3 
----> 4m = Manager('IT产品', '广州')
      5 m.info()

TypeError: __init__() takes 2 positional arguments but 3 were given
 

从上面的实验可以看出多继承的子类在构造方法方面的问题,为了让 Manager 能同时初始化两个父类中的实例变量,Manager 应该定义自己的构造方法,即重写父类的构造方法。Python 要求:如果子类重写了父类的构造方法,那么子类的构造方法必须调用父类的构造方法。

子类的构造方法调用父类的构造方法有两种方式:
1)使用未绑定方法,这种方式很容易理解。因为构造方法也是实例方法,当然可以通过这种方式来调用。
2)使用 super() 函数调用父类的构造方法。

super 其实是一个类,因此调用 super() 的本质就是调用 super 类的构造方法来创建 super 对象。

使用 super() 构造方法最常用的做法就是不传入任何参数(这种做法与 super(type, obj) 的效果相同),然后通过 super 对象的方法既可调用父类的实例方法,也可调用父类的类方法。在调用父类的实例方法时,程序会完成第一个参数 self 的自动绑定,如上帮助信息中 ① 号信息所示。在调用类方法时,程序会完成第一个参数 cls 的自动绑定,如上面帮助信息中 ② 号信息所示。

掌握了 super() 函数的用法之后,接下来可以将上面程序改为如下形式:

In [84]:
# Manager继承了Employee、Customer
class Manager(Employee, Customer):
    # 重写父类的构造方法
    def __init__(self, salary, favorite, address):
        print('--Manager的构造方法--')
        # # 通过super()函数调用父类的构造方法,好像会报错
        # super.__init__(salary)
        # 与上一行代码的效果相同
        Employee.__init__(self,salary)
        # 使用未绑定方法调用父类的构造方法
        Customer.__init__(self,favorite, address)
In [85]:
# 创建Manager对象
m = Manager(25000,'IT产品', '广州')
 
--Manager的构造方法--
In [86]:
m.work()  #①
 
普通员工正在写代码,工资是: 25000
In [87]:
m.info()  #②
 
我是一个顾客,我的爱好是: IT产品,地址是广州
 

6.1 slots:限制类实例动态添加属性和方法

前面介绍了为对象动态添加方法,但是所添加的方法只是对当前对象有效,如果希望为所有实例都添加方法,则可通过为类添加方法来实现。例如如下代码:

In [89]:
class Cat:
    def __init__(self, name):
        self.name = name
        
def walk_func(self):
    print('%s慢慢地走过一片草地' % self.name)
In [90]:
d1 = Cat('Garfield')
d2 = Cat('Kitty')
In [91]:
# 为Cat动态添加walk()方法,该方法的第一个参数会自动绑定
Cat.walk = walk_func  # ①
In [92]:
# d1、d2调用walk()方法
d1.walk()
 
Garfield慢慢地走过一片草地
In [93]:
d2.walk()
 
Kitty慢慢地走过一片草地
 

程序中 ① 号代码为 Cat 动态添加了 walk() 方法,为类动态添加方法时不需要使用 MethodType 进行包装,该函数的第一个参数会自动绑定。为 Cat 动态添加 walk() 方法之后,Cat 类的两个实例 d1、d2 都具有了 walk() 方法,因此上面程序中最后两行 d1、d2 都可调用 walk() 方法。

Python 的这种动态性固然有其优势,但也给程序带来了一定的隐患,即程序定义好的类,完全有可能在后面被其他程序修改,这就带来了一些不确定性。如果程序要限制为某个类动态添加属性和方法,则可通过 slots 属性来指定。

slots 属性的值是一个元组,该元组的所有元素列出了该类的实例允许动态添加的所有属性名和方法名(对于 Python 而言,方法相当于属性值为函数的属性)。例如如下程序:

In [95]:
class Dog:
    __slots__ = ('walk', 'age', 'name')
    def __init__(self, name):
        self.name = name
    def test(self):
        print('预先定义的test方法')
In [96]:
from types import MethodType

d = Dog('Snoopy')
# 只允许动态为实例添加walk、age、name这3个属性或方法
d.walk = MethodType(lambda self: print('%s正在慢慢地走' % self.name), d)
In [98]:
d.age = 5
d.walk()
 
Snoopy正在慢慢地走
In [99]:
d.foo = 30 # AttributeError
 
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-99-b08ea83fbb60> in <module>
----> 1d.foo = 30 # AttributeError

AttributeError: 'Dog' object has no attribute 'foo'
 

上面程序中第 2 行代码定义了 slots=('walk','age', 'name'),这意味着程序只允许为 Dog 实例动态添加 walk、age、name 这三个属性或方法。因此上面程序中第 10 行、第 11 行代码为 Dog 对象动态添加 walk() 方法和 age 属性都是允许的。

但如果程序尝试为 Dog 对象添加其他额外属性,程序就会引发 AttributeError 错误,如上面最后一行代码所示。

需要说明的是,slots 属性并不限制通过类来动态添加属性或方法,因此下面代码是合法的:

In [100]:
# __slots__属性并不限制通过类来动态添加方法
Dog.bar = lambda self: print('abc') # AttributeError
d.bar()
 
abc
In [101]:
Dog.foo = 30
d.foo
Out[101]:
30
 

下面代码通过 Dog 类动态添加了 bar() 方法,这样 Dog 对象就可以调用该 bar() 方法了。

此外,slots 属性指定的限制只对当前类的实例起作用,对该类派生出来的子类是不起作用的。例如如下代码:

In [105]:
class GunDog(Dog):
    def __init__(self, name):
        Dog.__init__(self,name)
    pass
In [106]:
gd = GunDog('Puppy')
In [107]:
# 完全可以为Gundog实例动态添加属性
gd.speed = 99
print(gd.speed)
 
99
 

正如从上面代码所看到的,Dog 的子类 GunDog 的实例完全可以动态添加 speed 属性,这说明 slots 属性指定的限制只对当前类起作用。

如果要限制子类的实例动态添加属性和方法,则需要在子类中也定义 slots 属性,这样,子类的实例允许动态添加属性和方法就是子类的 slots 元组加上父类的 slots 元组的和。

 

6.2 type函数:动态创建类

前面己经提到使用 type() 函数可以查看变量的类型,但如果想使用 type() 直接查看某个类的类型呢?看如下程序:

In [108]:
class Role:
    pass
r = Role()
# 查看变量r的类型
print(type(r)) # <class '__main__.Role'>
# 查看Role类本身的类型
print(type(Role)) # <class 'type'>
 
<class '__main__.Role'>
<class 'type'>
 

从上面的输出结果可以看到,Role 类本身的类型是 type。这句话有点拗口,怎样理解 Role 类的类型是 type?

从 Python 解释器的角度来看,当程序使用 class 定义 Role 类时,也可理解为定义了一个特殊的对象(type 类的对象),并将该对象赋值给 Role 变量。因此,程序使用 class 定义的所有类都是 type 类的实例。

实际上 Python 完全允许使用 type() 函数(相当于 type 类的构造器函数)来创建 type 对象,又由于 type 类的实例就是类,因此 Python 可以使用 type() 函数来动态创建类。例如如下程序:

In [109]:
def fn(self):
    print('fn函数')
# 使用type()定义Dog类
Dog = type('Dog', (object,), dict(walk=fn, age=6))
In [110]:
# 创建Dog对象
d = Dog()
# 分别查看d、Dog的类型
print(type(d))
print(type(Dog))
 
<class '__main__.Dog'>
<class 'type'>
In [111]:
d.walk()
print(Dog.age)
 
fn函数
6
 

上面第 4 行代码使用 type() 定义了一个 Dog 类。在使用 type() 定义类时可指定三个参数:
参数一:创建的类名。
参数二:该类继承的父类集合。由于 Python 支持多继承,因此此处使用元组指定它的多个父类。即使实际只有一个父类,也需要使用元组语法(必须要多一个逗号)。
参数三:该字典对象为该类绑定的类变量和方法。其中字典的 key 就是类变量或方法名,如果字典的 value 是普通值,那就代表类变量;如果字典的 value 是函数,则代表方法。

由此可见,第 4 行代码定义了一个 Dog 类,该类继承了 object 类,还为该类定义了一个 walk() 方法和一个 age 类变量。

从上面的输出结果可以看出,使用 type() 函数定义的类与直接使用 class 定义的类井没有任何区别。事实上,Python 解释器在执行使用 class 定义的类时,其实依然是使用 type() 函数来创建类的。因此,无论通过哪种方式定义类,程序最终都是创建一个 type 的实例。

 

6.3 metaclass详解

如果希望创建某一批类全部具有某种特征,则可通过 metaclass 来实现。使用 metaclass 可以在创建类时动态修改类定义。

为了使用 metaclass 动态修改类定义,程序需要先定义 metaclass, metaclass 应该继承 type 类,并重写 new() 方法。

下面程序定义了一个 metaclass 类:

In [112]:
# 定义ItemMetaClass,继承type
class ItemMetaClass(type):
    # cls代表动态修改的类
    # name代表动态修改的类名
    # bases代表被动态修改的类的所有父类
    # attr代表被动态修改的类的所有属性、方法组成的字典
    def __new__(cls, name, bases, attrs):
        # 动态为该类添加一个cal_price方法
        attrs['cal_price'] = lambda self: self.price * self.discount
        return type.__new__(cls, name, bases, attrs)
 

上面程序定义了一个 ItemMetaClass 类,该类继承了 type 类,并重写了 new 方法,在重写该方法时为目标类动态添加了一个 cal_price 方法。

metaclass 类的 new 方法的作用是:当程序使用 class 定义新类时,如果指定了 metaclass,那么 metaclass 的 new 方法就会被自动执行。

例如,如下程序使用 metaclass 定义了两个类:

In [113]:
# 定义Book类
class Book(metaclass=ItemMetaClass):
    __slots__ = ('name', 'price', '_discount')
    def __init__(self, name, price):
        self.name = name
        self.price = price
    @property
    def discount(self):
        return self._discount
    @discount.setter
    def discount(self, discount):
        self._discount = discount
# 定义cellPhone类
class CellPhone(metaclass=ItemMetaClass):
    __slots__ = ('price', '_discount' )
    def __init__(self, price):
        self.price = price
    @property
    def discount(self):
        return self._discount
    @discount.setter
    def discount(self, discount):
        self._discount = discount
 

上面程序定义了 Book 和 CellPhone 两个类,在定义这两个类时都指定了 metaclass 信息,因此当 Python 解释器在创建这两个类时,ItemMetaClass 的 new 方法就会被调用,用于修改这两个类。

ItemMetaClass 类的 new 方法会为目标类动态添加 cal_price 方法,因此,虽然在定义 Book、CellPhone 类时没有定义 cal_price() 方法,但这两个类依然有 cal_price() 方法。如下程序测试了 Book、CellPhone 两个类的 cal_price() 方法:

In [114]:
b = Book("Python基础教程", 89)
b.discount = 0.76
# 创建Book对象的cal_price()方法
print(b.cal_price())
 
67.64
In [115]:
cp = CellPhone(2399)
cp.discount = 0.85
# 创建CellPhone对象的cal_price()方法
print(cp.cal_price())
 
2039.1499999999999
 

从上面的运行结果来看,通过使用 metaclass 可以动态修改程序中的一批类,对它们集中进行某种修改。这个功能在开发一些基础性框架时非常有用,程序可以通过使用 metaclass 为某一批需要具有通用功能的类添加方法。

 
 

猜你喜欢

转载自www.cnblogs.com/xinmomoyan/p/10809032.html