在这一节,结合之前学过的内容,我们来看如何在Python中表示类之间的继承关系。
首先,基于上一节的例子,我们创建了三个文件:
- Person.py:
- Employee.py:
- Main.py:
现在,我们打开Employee.py,在其中给Person
添加一个派生类Empolyee
:
from Person import Person
class Employee(Person):
在这里例子里,有三点值得我们注意:
第一、为了在Employee.py中使用Person
,我们在文件开始使用了from Person import Person
。由于Person
定义在Person.py中,因此from
后面的模块名是Person
,从中,我们引入了class Person
。虽然它们同名,大家要把它们分清楚。
第二、我们的类定义,要和import
语句保持两个空行,这是Python中推荐的编码习惯。
第三、Python中,我们用(Person)
这样的形式表示基类。在这个括号里,我们可以用逗号分隔多个基类。但经验告诉我们,多重继承在更多时候并不会像我们想象一样正常工作。因此,我们还是暂时只讨论单一继承的情况。
派生类的init方法
定义了派生类之后,第一件事,就是定义它的__init__
方法,就像你已有的OO经验一样,在派生类的__init__
方法里,我们要初始化派生类和基类两部分内容:
class Employee(Person):
def __init__(self, work_id, name, age):
Person.__init__(self, name, age)
self.work_id = work_id
这里唯一要注意的是派生类调用基类__init__
的方法,是通过类名调用的。然后,我们就可以这样来创建派生类对象了。在Main.py中,添加下面的代码:
from Employee import Employee
mars = Employee(11, 'mars', 30)
既然有了派生类,继续之前,我们介绍几个判断类关系的函数。要判断某个类是否是另外一个类的派生类可以使用issubclass
:
print(issubclass(Employee, Person)) # True
它的第一个参数是派生类类型,第二个参数是基类类型,最后,返回一个boolean值。要判断一个对象是否是某个类的对象,可以使用isinstance
:
print(isinstance(mars, Person)) # True
print(isinstance(mars, Employee)) # True
它的第一个参数是类对象,第二个参数是要判断所属的类型,同样返回一个boolean值。要注意的是,这两个函数都直接传给它们类型就好了,并不用给它们传递类型的字符串名称。
重写类方法
在Python里,重写基类方法和在派生类中定义方法是没有任何区别的,我们不用像Swift一样使用override
关键字,只要重定义了,就算是重写了。
我们通过重写几个Python class
中内置的方法,来理解这个过程。实际上,之前我们已经干过这个事了,就是在派生类中重写__init__
。除此之外,class
中还有以下可以重写的方法。
理解repr和str
每个Python class
都包含了两个和类型的字符串表达方式有关的方法,叫做__repr__
和__str__
,默认情况下,它们返回的结果是相同的。在Main.py中,添加下面的代码:
print(mars.__repr__())
print(mars.__str__())
# <Employee.Employee object at 0x109563550>
执行一下就会看到,它们返回的都是类似上面注释中的结果。那么,为什么要有两个方法呢?也许你可以找到很多关于这个问题的讨论。但在我看来,理解下面这两点就足以让你用对它们了:
- 首先,
__str__
是通过调用__repr__
实现的,因此,它的默认实现确没什么特别的用途; - 其次,
__repr__
是给一个类型的开发者使用的,它应该包含关于类型的更多信息;而__str__
的描述则是针对类型的使用者使用的,它应该尽可能易读、友好;
有了上面这两个原则之后,我们就可以试着改写__str__
的实现,让它只返回类型名称本身,例如这样:
class Employee(Person):
# ...
def __str__(self):
return "Employee"
稍后,我们还会看到更好的获取类型名称的方法,这里我们只是直接返回了Employee
。重新执行下,就会发现,此时__repr__
和__str__
的结果不同了。
'''
<Employee.Employee object at 0x10e388518>
Employee
'''
重定义对象的比较行为
除了重定义类型的表达方式之外,我们还可以明确指定两个类对象的比较方法。在Python 2的时候,这是通过重写__cmp__
方法实现的。但在Python 3中,已经不提倡这么做了。Python 3中提供了一组表意更明确的函数:
-
__eq__
: equal -
__ne__
: not equal -
__lt__
: less than -
__le__
: less equal -
__gt__
: greater than -
__ge__
: greater equal
首先来看,当我们不定义这些方法的时候,比较两个Employee
对象,执行的是比较对象引用的操作:
mars = Employee(11, 'mars', 30)
eleven = Employee(11, 'mars', 30)
print(mars == eleven) # false
显然,mars
和eleven
引用的是不同的对象,因此这个比较结果肯定是False
。这时,如果我们希望按照对象的值比较,认为只要姓名、年龄和工号相等,那么两个Employee
对象就相等,就可以重写__eq__
方法:
class Employee(Person):
# ...
def __eq__(self, other):
return self.name == other.name and \
self.age == other.age and \
self.work_id == other.work_id
可以看到,__eq__
有两个参数,self
可以理解为==
的左操作数,other
是==
的右操作数。而它的比较逻辑,就是逐个比较Employee
的每个属性,很简单。
定义好__eq__
之后,之前的比较结果,就变成True
了。
定义Class attributes
在这一节最后,我们来看如何给class
添加静态属性,在Python里,这叫做class attribute。
之前我们提到过,所有直接定义在__init__
方法里的属性,都是类对象属性,它们都是绑定在某个对象上的。如果我们把属性定义在__init__
外面,这个属性就被所有的类对象共享了。例如,我们添加一个统计所有员工对象数量的counter
:
class Employee(Person):
counter = 0
可以看到,counter
的初始化是在定义的时候完成的。这样,counter
就会被所有的类对象共享了,我们可以在每次__init__
方法被调用的时候,把它加1:
def __init__(self, work_id, name, age):
Person.__init__(self, name, age)
self.work_id = work_id
Employee.counter += 1
此时,由于我们有mars
和eleven
两个Employee
对象,访问counter
的时候,得到的值,就是2了:
mars = Employee(11, 'mars', 30)
eleven = Employee(11, 'mars', 30)
print(Employee.counter) # 2
在上面的例子里可以看到看到,访问class attribute的时候,我们要使用ClassName.Attribute
这样的形式。
看到这,你可能会想,这个counter
可以随便被人修改啊,用它来计数并不靠谱。没错,默认情况下,class
的所有的属性和方法都是可以被外部访问的。如果我们要隐藏这个属性,可以在它前面使用两个下划线:
class Employee(Person):
__counter = 0
def __init__(self, work_id, name, age):
Person.__init__(self, name, age)
self.work_id = work_id
Employee.__counter += 1
然后,在Main.py里,无论你用哪种方式访问counter
:
print(Employee.counter)
print(Employee.__counter)
都会触发AttributeError
异常,并提示我们对应的counter
属性不存在。
不过,你也别太当真,因为这至多也就是一层设计意图上的保障罢了,它只能提醒你自己,__counter
只能用在Employee
内部。
因为,Python中有一个魔术前缀,就是一个下划线加上类名,对于我们的例子来说,就是_Employee,通过它,你就可以访问到私有成员了。于是,在Main.py里,我们把代码改成这样:
print(Employee._Employee__counter) # 2
就可以恢复执行了,而且,读写均可 :-)