在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承
,新的class
称为子类(Subclass)
,而被继承的class称为基类、父类或超类(Base class、Super class)。
如:编写的一个名为Animal
的class,有一个run
的方法可以直接打印
class Animal(object):
def run(self):
print('Animal is running...')
当我们需要编写Dog
和Cat
类时,就可以直接从Animal
类继承:
class Dog(Animal):
pass
class Cat(Animal):
pass
Dog和Cat就是子类,Animal就是父类
继承最大的好处是子类获得了父类的全部功能。由于Animial
实现了run()
方法,因此,Dog
和Cat
作为它的子类,什么事也没干,就自动拥有了run()
方法:
dog = Dog()
dog.run()--》Animal is running...
cat = Cat()
cat.run()---》Animal is running...
也可以对子类增加一些方法,比如Dog类:
class Dog(Animal):
def run(self):
print('Dog is running...')
def eat(self):
print('Eating meat...')
继承的第二个好处需要我们对代码做一点改进
无论是Dog还是Cat,它们run()
的时候,显示的都是Animal is running...
,符合逻辑的做法是分别显示Dog is running...
和Cat is running...
,因此,对Dog
和Cat
类改进,以Dog
为例:
class Dog(Animal):
def run(self):
print('Dog is running...')--->Dog is running...
当子类和父类都存在相同的run()
方法时,我们说,子类的run()
覆盖了父类的run()
,在代码运行的时候,总是会调用子类的run()
。这样,我们就获得了继承的另一个好处:多态。
要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个class的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样
a = list() # a是list类型
b = Animal() # b是Animal类型
c = Dog() # c是Dog类型
判断一个变量是否是某个类型可以用isinstance()判断:
>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True
'看来a、b、c确实对应着list、Animal、Dog这3种类型'
'但是,还可以发现:'
>>> isinstance(c, Animal)
True
c不仅仅是Dog,c还是Animal!
因此如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类
,但是反之不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
再看一个例子:
'一个函数接受了一个`Animal类型`的变量'
def run_twice(animal):
animal.run()
animal.run()
当我们传入Animal的实例
时,run_twice()
就打印出:
'传入实例,相当于:s = Animal() run_twice(s)'
run_twice(Animal())
Animal is running...
Animal is running..
当我们传入Dog的实例
时,run_twice()就打印出:
>>> run_twice(Dog())
Dog is running...
Dog is running...
仔细想想,现在,如果我们再定义一个Tortoise类型,也从Animal派生:
class Tortoise(Animal):
def run(self):
print('Tortoise is running slowly...')
当我们调用run_twice()
时,传入Tortoise
的实例:
>>> run_twice(Tortoise())
Tortoise is running slowly...
Tortoise is running slowly...
你会发现,新增一个
Animal
的子类,不必对run_twice()
做任何修改,实际上,任何依赖Animal
作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。多态的好处就是,当我们需要传入
Dog、Cat、Tortoise……
时,我们只需要接收Animal
类型就可以了,因为Dog、Cat、Tortoise……
都是Animal
类型,然后,按照Animal
类型进行操作即可。由于Animal
类型有run()
方法,因此,传入的任意类型,只要是Animal
类或者子类,就会自动调用实际类型的run()
方法,这就是多态的意思:
对于一个变量,我们只需要知道它是Animal类型
,无需确切地知道它的子类型,就可以放心地调用run()
方法,而具体调用的run()
方法是作用在Animal、Dog、Cat
还是Tortoise
对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
特性
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
继承还可以一级一级地继承下来,
对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法
对于Python这样的动态语言来说,则不一定需要传入Animal
类型。我们只需要保证传入的对象有一个run()
方法就可以了:
class Timer(object):
def run(self):
print('Start...')
这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python的“file-like object“
就是一种鸭子类型。对真正的文件对象,它有一个read()
方法,返回其内容。但是,许多对象,只要有read()
方法,都被视为“file-like object“
。许多函数接收的参数就是“file-like object“
,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。
当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?
判断对象类型,使用type()
函数:
基本类型都可以用type()判断:
>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(None)
<type(None) 'NoneType'>
如果一个变量指向函数或者类,也可以用type()判断:
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>
但是type()函数返回对应的Class类型。
如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:
>>> type(123)==type(456
>'判断基本的数据类型可以直接写int,str等'
>>> type(123)==int--->True
>>> type('abc')==type('123')--->True
>>> type('abc')==str--->True
>>> type('abc')==type(123)--->False
要判断一个对象是否是函数,可以使用types模块中定义的常量:
import types
def fn():
pass
print(type(f) == types.FunctionType)--->Ture
print(type(abs) == types.BuiltinFunctionType)--->Ture
print(type(lambda x:x*x) == types.LambdaType)--->Ture
print(type((x for x in range(10)))==types.GeneratorType)--->Ture
对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数
。
回顾上面的例子,继承关系为:
object -> Animal -> Dog -> Husky
isinstance()
就可以告诉我们,一个对象是否是某种类型。先创建3种类型的对象:
>>> a = Animal()
>>> d = Dog()
>>> h = Husky()
>>> isinstance(h, Husky)
True --->'因为h变量指向的就是Husky对象。'
>>> isinstance(h, Dog)
True--->'Husky是从Dog继承下来的,所以,h也还是Dog类型'
也就是说:
isinstance()
判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上。
因此:
>>> isinstance(h, Animal)
True-->'h还是Animal类型:'
>>> isinstance(d, Dog) and isinstance(d, Animal)
True
但是:属于Dog类型的d不是Husky类型:
>>> isinstance(d, Husky)
False
能用type()
判断的基本类型也可以用isinstance()
判断:
>>> isinstance('a', str)--->True
>>> isinstance(123, int)--->True
>>> isinstance(b'a', bytes)--->True
并且还可以判断一个变量是否是某些类型中的一种,比如下面的代码就可以判断是否是list或者tuple:
>>> isinstance([1, 2, 3], (list, tuple))--->True
>>> isinstance((1, 2, 3), (list, tuple))--->True
总是优先使用isinstance()
判断类型,可以将指定类型及其子类“一网打尽”。
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list
比如,获得一个str对象的所有属性和方法:
>>> dir('ABC')
['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
类似__xxx__
的属性和方法在Python中都是有特殊用途的,比如__len__
方法返回长度。在Python
中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()
函数内部,它自动去调用该对象的__len__()
方法,所以,下面的代码是等价的:
>>> len('ABC')--->3
>>> 'ABC'.__len__()--->3
自己写的类,如果也想用len(myObj)
的话,就自己写一个__len__()
方法:
>>> class MyDog(object):
... def __len__(self):
... return 100
...
>>> dog = MyDog()
>>> len(dog)--->100
'剩下的都是普通属性或方法,比如lower()返回小写的字符串:'
>>> 'ABC'.lower()
'abc'
仅仅把属性和方法列出来是不够的,配合getattr()
、setattr()
以及hasattr()
,我们可以直接操作一个对象的状态:
>>> class MyObject(object):
... def __init__(self):
... self.x = 9
... def power(self):
... return self.x * self.x
...
>>> obj = MyObject()
紧接着,可以测试该对象的属性(注意属性都要加''):
>>> hasattr(obj, 'x')---> # 有属性'x'吗?
>>> obj.x--->9
>>> hasattr(obj, 'y')--->False # 有属性'y'吗?
>>> setattr(obj, 'y', 19) # 设置一个属性'y'
>>> hasattr(obj, 'y') --->True# 有属性'y'吗?
>>> getattr(obj, 'y')--->19 # 获取属性'y'
>>> obj.y --->19# 获取属性'y'
如果试图获取不存在的属性,会抛出AttributeError的错误:
>>> getattr(obj, 'z') # 获取属性'z'
Traceback (most recent call last):
File "" , line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
'可以传入一个default参数,如果属性不存在,就返回默认值:'
>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404
404
也可以获得对象的方法:
>>> hasattr(obj, 'power') # 有属性'power'吗?
True
>>> getattr(obj, 'power') # 获取属性'power'
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn
>>> fn # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x10077a6a0>>
>>> fn() # 调用fn()与调用obj.power()是一样的
81
由于Python是动态语言,根据类创建的实例可以任意绑定属性。
给实例绑定属性的方法是通过实例变量,或者通过self变量:
class Student(object):
def __init__(self, name):
self.name = name
s = Student('Bob')
s.score = 90
但是,如果Student类本身需要绑定一个属性呢?可以直接在class
中定义属性,这种属性是类属性,归Student
类所有:
class Student(object):
name = 'Student'
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
练习
为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:
class Student(object):
count = 0#公共类属性
def __init__(self, name):
self.name = name
Student.count += 1
self.count += 1#会对上一步的count值产生影响