Python 类、继承与多态机制深度解析
本文深入解析 Python 面向对象编程核心机制。从类的定义与 __init__ 构造函数入手,详解特殊方法如 __repr__、__str__ 及运算符重载 __add__ 的应用。探讨继承中的方法重写与动态绑定特性,分析 Python 缺乏严格访问控制与静态类型检查对多态实现的影响,并介绍通过约定模拟接口及 super 函数调用父类方法的最佳实践。
类的定义
若需定义一个表示二维坐标点的类 Point,具体实现如下:
# point.py
class Point:
def __init__(self, x=0, y=0):
self.x, self.y = x, y
最基础的是 __init__ 方法,其功能类似于 C++ 或 Java 中的构造函数。所有以双下划线 __ 开头和结尾的方法均属于特殊方法,除了 __init__ 外还有诸多其他特殊方法,后续章节将逐一介绍。
参数 self 的作用类似于 C++ 中的 this,用于指向当前实例。所有实例方法均包含此参数,但在调用时无需显式指定该参数。
>>> from point import * >>> p = Point(10, 10) # __init__ 被调用 >>> type(p) <class 'point.Point'> >>> p.x, p.y (10, 10)
绝大多数特殊方法(含 __init__)均为隐式调用,开发者不应直接调用它们。
在 Python “一切皆对象”的理念下,类本身也是一个对象:
>>> type(Point) <class 'type'> >>> dir(Point) ['__class__', '__delattr__', '__dict__', ..., '__init__', ...] >>> Point.__class__ <class 'type'>
Point 是 type 类的实例,这与 p 是 Point 类的实例在本质上是相同的。
现为类添加一个 set 方法:
class Point:
...
def set(self, x, y):
self.x, self.y = x, y
>>> p = Point(10, 10) >>> p.set(0, 0) >>> p.x, p.y (0, 0)
调用 p.set(...) 实际上是一种语法糖,等价于写成 Point.set(p, ...),这种写法能更直观地看出 p 即对应 self 参数:
>>> Point.set(p, 0, 0) >>> p.x, p.y (0, 0)
值得注意的是,self 并非 Python 关键字,甚至可以使用其他名称替代,例如 this:
class Point:
...
def set(this, x, y):
this.x, this.y = x, y
与 C++ 不同,Python 中“成员变量”必须添加 self. 前缀,否则该变量将变为类的属性(类似于 C++ 的静态成员),而非对象的属性。
访问控制
Python 不提供 public、protected 或 private 等访问控制修饰符。若需表示“私有”,惯例是添加双下划线前缀。
class Point:
def __init__(self, x=0, y=0):
self.__x, self.__y = x, y
def set(self, x, y):
self.__x, self.__y = x, y
def __f(self):
pass
__x、__y 和 __f 即被视为私有成员:
>>> p = Point(10, 10) >>> p.__x ... AttributeError: 'Point' object has no attribute '__x' >>> p.__f() ... AttributeError: 'Point' object has no attribute '__f'
__repr__
尝试打印 Point 实例:
>>> p = Point(10, 10) >>> p <point.Point object at 0x000000000272AA20>
通常,这并非我们期望的输出结果,我们更希望看到如下格式:
>>> p Point(10, 10)
通过添加特殊方法 __repr__ 即可实现该效果:
class Point:
def __repr__(self):
return 'Point({}, {})'.format(self.__x, self.__y)
可以看出,在交互模式中打印 p 时,实际上调用了 repr(p):
>>> repr(p) 'Point(10, 10)'
__str__
若未提供 __str__,str() 函数默认使用 repr() 的结果。 两者均用于表示对象的字符串形式,但存在细微差别。简而言之,repr() 的结果面向解释器,通常需为合法的 Python 代码,例如 Point(10, 10);而 str() 的结果面向用户,力求简洁,例如 (10, 10)。
依据上述原则,我们为 Point 类定义 __str__ 如下:
class Point:
def __str__(self):
return '({}, {})'.format(self.__x, self.__y)
__add__
两个坐标点相加是一项合理的需求。
>>> p1 = Point(10, 10) >>> p2 = Point(10, 10) >>> p3 = p1 + p2 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'Point' and 'Point'
通过添加特殊方法 __add__ 即可实现:
class Point:
def __add__(self, other):
return Point(self.__x + other.__x, self.__y + other.__y)
>>> p3 = p1 + p2 >>> p3 Point(20, 20)
这类似于 C++ 中的操作符重载。 Python 的内建类型,如字符串和列表,均已“重载”了 + 操作符。
特殊方法种类繁多,此处不再逐一列举。
继承
举一个教科书式的常见例子。Circle 和 Rectangle 类继承自 Shape 类,不同图形的面积(area)计算方式各不相同。
# shape.py
class Shape:
def area(self):
return 0.0
class Circle(Shape):
def __init__(self, r=0.0):
self.r = r
def area(self):
return math.pi * self.r * self.r
class Rectangle(Shape):
def __init__(self, a, b):
self.a, self.b = a, b
def area(self):
return self.a * self.b
其用法较为直接:
>>> from shape import * >>> circle = Circle(3.0) >>> circle.area() 28.274333882308138 >>> rectangle = Rectangle(2.0, 3.0) >>> rectangle.area() 6.0
若 Circle 类未定义自己的 area 方法:
class Circle(Shape): pass
那么它将继承父类 Shape 的 area 方法:
>>> Shape.area is Circle.area True
一旦 Circle 定义了自身的 area 方法,从 Shape 继承而来的 area 方法即被重写(overwrite):
>>> from shape import * >>> Shape.area is Circle.area False
通过查看类的字典可以更清晰地观察到这一点:
>>> Shape.__dict__['area'] <function Shape.area at 0x0000000001FDB9D8> >>> Circle.__dict__['area'] <function Circle.area at 0x0000000001FDBB70>
因此,子类重写父类方法,实质上是将相同的属性名绑定到不同的函数对象。由此可见,Python 中并不存在传统意义上的覆写(override)概念。
同理,即使 Shape 未定义 area 方法也是可行的,Shape 在此处充当“接口”角色,但无法获得语法层面的保证。
甚至可以动态地添加方法:
class Circle(Shape):
...
# def area(self):
# return math.pi * self.r * self.r
# 为 Circle 添加 area 方法。
Circle.area = lambda self: math.pi * self.r * self.r
动态语言通常具备这种灵活性,Python 亦不例外。
Python 官方教程「9. Classes」开篇即指出:
Compared with other programming languages, Python's class mechanism adds classes with a minimum of new syntax and semantics.
Python 以极少的语法和语义新增实现了类机制,这一点确实令人惊叹,但也常令熟悉 C++ 或 Java 的程序员感到不适。
多态
如前所述,Python 不存在传统意义上的覆写(override)概念。严格而言,Python 并不直接支持“多态”。
为了解决继承结构中接口与实现的问题,或为了更好地践行 Python 面向接口编程(设计模式所倡导的理念),我们需要人为设定一些规范。
请思考 Shape.area() 除了简单返回 0.0 外,是否有更优的实现方式?
以内建模块 asyncio 为例,AbstractEventLoop 原则上是一个接口,类似于 Java 中的接口或 C++ 中的纯虚类,但 Python 并无语法强制保证这一点。为尽量体现 AbstractEventLoop 的接口属性,首先在命名上标识其为抽象(Abstract),然后让每个方法均抛出异常 NotImplementedError。
class AbstractEventLoop:
def run_forever(self):
raise NotImplementedError
...
尽管如此,你仍无法禁止用户实例化 AbstractEventLoop:
loop = asyncio.AbstractEventLoop() try: loop.run_forever() except NotImplementedError: pass
C++ 可通过纯虚函数或将构造函数设为 protected 来避免接口被实例化,Java 则提供了完整的语法支持。
你也无法强制子类必须实现“接口”中定义的每一个方法,C++ 的纯虚函数可强制这一点(Java 更不必说)。
即使子类“自以为”实现了“接口”中的方法,也无法保证方法名拼写无误,C++ 的 override 关键字可确保这一点(Java 更不必说)。
静态类型的缺失,使得 Python 难以实现如 C++ 或 Java 那般严格的多态检查机制。因此,面向接口编程在 Python 中更多依赖于程序员的素养。
回到 Shape 的例子,仿照 asyncio 的做法,我们将“接口”修改如下:
class AbstractShape:
def area(self):
raise NotImplementedError
这样,它才更像一个接口。
super
有时,需要在子类中调用父类的方法。
例如,图形均具有颜色属性,因此不妨在 __init__ 中添加 color 参数:
class AbstractShape:
def __init__(self, color):
self.color = color
那么子类的 __init__() 方法也需相应调整:
class Circle(AbstractShape):
def __init__(self, color, r=0.0):
super().__init__(color)
self.r = r
通过 super 将 color 传递给父类的 __init__()。其实不使用 super 也是可行的:
class Circle(AbstractShape):
def __init__(self, color, r=0.0):
AbstractShape.__init__(self, color)
self.r = r
但 super 是推荐的做法,因为它避免了硬编码,并能处理多继承的情况。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













