Java之类的编程语言还带有静态方法,Python类也拥有与静态方法明确对应的方法。此外,Python还拥有类方法,要比静态方法更高级一些。
静态方法与Java一样,即便没有创建类的实例,静态方法也是可以调用的,当然通过类实例来调用也是可以的。请用@staticmethod装饰器来创建静态方法,如代码清单15-1所示。
因为Python的动态性,对语言没有加太多限制,所以其继承机制要比Java和C ++等编译型语言更加简单灵活。为了了解如何在Python中使用继承,可先从本章之前讨论过的Circle类开始,再推而广之。不妨再定义一个正方形类Square:
现在,如果要在绘图程序中使用这些类,必须定义每个实例在绘图表面的位置信息。在每个实例中定义x、y坐标,即可实现这一点:
python">class Square:
def __init__(self, side=1, x=0, y=0):
self.side = side
self.x = x
self.y = y
class Circle:
def __init__(self, radius=1, x=0, y=0):
self.radius = radius
self.x = x
self.y = y
这种方式能起作用,但如果要扩展大量的形状类,就会产生大量重复代码,因为可能要让每种形状类都具备这种位置的概念。毫无疑问,这正是在面向对象语言中使用继承的标准场景。不用在每个形状类中都定义变量x和y,而可以将各种形状抽象为一个通用的Shape类,并让定义具体形状的类继承自该通用类。在Python中,定义方式如下:
在Python中使用继承类通常有两个要求,在Circle类和Square类的粗体代码中可以看到这两个要求。第一个要求是定义继承的层次结构,在用class关键字定义类名之后的圆括号中,给出要继承的类即可。在上述代码中,Circle和Square都继承自Shape。第二个要求比较微妙一些,就是必须显式调用被继承类的__init__方法。Python不会自动执行初始化操作,但可以用super函数让Python找到被继承的类。初始化的工作在示例中由super().__ init__(x, y)这行代码来完成,这里将调用Shape的初始化函数,用适当的参数初始化实例。如果没有显式调用父类的初始化方法,则本例中的Circle和Square的实例就不会给实例变量x和y赋值。
可以不用super来调用Shape的__init__,而是用Shape.init(self, x, y)显式给出被继承类的名字,同样能够实现在实例初始化完毕后调用Shape的初始化函数。从长远来看,这种做法不够灵活,因为对被继承类名进行了硬编码。如果日后整体设计和继承架构发生了变化,这就可能成为问题。但在继承关系比较复杂的时候,采用super会比较麻烦。因为这两种方案无法完全混合使用,所以请把代码中采用的方案清楚地记录在文档中备查。如果方法未在子类或派生类中定义,但在父类中有定义,继承机制也会生效。为了查看这种继承的效果,请在Shape类中再定义一个move方法,表示移动到指定位置。该方法将会把实例的x和y坐标修改为参数指定的值。Shape的定义现在变成了:
python">class Shape:
def __init__(self, x, y):
self.x = x
self.y = y
def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.y = self.y + delta_y
如果这个Shape定义与之前的Circle、Square一起输入完毕,就可以进行以下的交互式会话:
python">>>> c = Circle(1)
>>> c.move(3, 4)
>>> c.x
3
>>> c.y
4
如果在交互式会话中执行上述代码,请务必在新定义的Shape类之后将Circle类的代码重新录入一遍。以上示例中的Circle类本身没有定义move方法,但由于继承自实现move的类,因此Circle的所有实例都可以使用move方法。用比较传统的OOP术语来描述,就是所有Python方法都是虚方法。也就是说如果方法在当前类中不存在,则会在父类中逐级搜索,并采用第一个找到的方法。
实例可以继承类的属性。实例变量是和对象实例关联的,某个名称的实例变量在一个实例中只会存在一个。看一下以下示例,这里会用到以下类的定义,
python">class P:
z = "Hello"
def set_p(self):
self.x = "Class P"
def print_p(self):
print(self.x)
class C(P):
def set_c(self):
self.x = "Class C"
def print_c(self):
print(self.x)
执行以下代码:
python">>>> c = C()
>>> c.set_p()
>>> c.print_p()
Class P
>>> c.print_c()
Class P
>>> c.set_c()
>>> c.print_c()
Class C
>>> c.print_p()
Class C
上述示例中的对象c是类C的实例。C继承自P,但c并非继承自类P的某个不可见的实例,而是直接从P继承方法和类变量的。因为只存在一个实例c,在c的方法调用中,对实例变量x的任何引用都只能指向c.x。在c上无论调用哪个类定义的方法,均会如此。如上所示,由c调用的set_p和print_p,都是在类P里定义的,且都引用了同一个变量,在c上调用set_c和print_c时,引用的也是这个变量。通常这正是实例变量应有的表现,因为对同一个名称的实例变量的引用,就应该指向同一个变量。不过有时也会有不同需求,可通过私有变量来实现(参见15.9节)。类变量是支持继承的,但应该避免命名冲突,并小心类变量一节中提及的种种现象。在以下示例中,父类P中定义了类变量z,并且通过以下3种方式都能被访问到:实例c、派生类C或直接用父类P:
python">>>> c.z; C.z; P.z
'Hello'
'Hello'
'Hello'
但如果通过类C来对类变量z赋值,就会在类C中创建一个新的类变量。这对P的类变量本身(通过P访问)没有影响。但以后通过类C或其实例c看到的,将会是这个新的变量,而不是原来的变量:
python">>>> C.z = "Bonjour"
>>> c.z; C.z; P.z
'Bonjour'
'Bonjour'
'Hello'
如果通过实例c来对z赋值,同样也会创建一个新的实例变量,最终会得到3个不同的变量
python">>>> c.z = "Ciao"
>>> c.z; C.z; P.z
'Ciao'
'Bonjour'
'Hello'