Python 的类、继承和多态详解


Posted in Python onJuly 16, 2017

类的定义

假如要定义一个类 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 并不是关键字,甚至可以用其它名字替代,比如 this:

class Point:
  ...
  def set(this, x, y):
    this.x, this.y = x, y

与 C++ 不同的是,“成员变量”必须要加 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

有时候,需要在子类中调用父类的方法。

比如图形都有颜色这个属性,所以不妨加一个参数 color 到 __init__:

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 是推荐的做法,因为它避免了硬编码,也能处理多继承的情况。

Python 相关文章推荐
python中的hashlib和base64加密模块使用实例
Sep 02 Python
Python中不同进制的语法及转换方法分析
Jul 27 Python
python里使用正则的findall函数的实例详解
Oct 19 Python
为什么选择python编程语言入门黑客攻防 给你几个理由!
Feb 02 Python
对YOLOv3模型调用时候的python接口详解
Aug 26 Python
详解在Python中以绝对路径或者相对路径导入文件的方法
Aug 30 Python
python 实现任务管理清单案例
Apr 25 Python
python使用自定义钉钉机器人的示例代码
Jun 24 Python
python3获取控制台输入的数据的具体实例
Aug 16 Python
Python程序慢的重要原因
Sep 04 Python
什么是Python包的循环导入
Sep 08 Python
Restful_framework视图组件代码实例解析
Nov 17 Python
PyQt 线程类 QThread使用详解
Jul 16 #Python
Pycharm技巧之代码跳转该如何回退
Jul 16 #Python
Python基础教程之浅拷贝和深拷贝实例详解
Jul 15 #Python
Python利用flask sqlalchemy实现分页效果
Aug 02 #Python
Python实现发送QQ邮件的封装
Jul 14 #Python
python记录程序运行时间的三种方法
Jul 14 #Python
python运行其他程序的实现方法
Jul 14 #Python
You might like
用Json实现PHP与JavaScript间数据交换的方法详解
2013/06/20 PHP
删除html标签得到纯文本可处理嵌套的标签
2014/04/28 PHP
PHP实现将HTML5中Canvas图像保存到服务器的方法
2014/11/28 PHP
ThinkPHP通过AJAX返回JSON的两种实现方法
2014/12/18 PHP
PHP数组中头部和尾部添加元素的方法(array_unshift,array_push)
2017/04/10 PHP
删除PHP数组中头部、尾部、任意元素的实现代码
2017/04/10 PHP
PHP缓存工具XCache安装与使用方法详解
2018/04/09 PHP
PHP getDocNamespaces()函数讲解
2019/02/03 PHP
jQuery让控件左右移动的三种实现方法
2013/09/08 Javascript
jquery.autocomplete修改实现键盘上下键自动填充示例
2013/11/19 Javascript
Javascript设置对象的ReadOnly属性(示例代码)
2013/12/25 Javascript
js实现jquery的offset()方法实例
2015/01/10 Javascript
Bootstrap富文本组件wysiwyg数据保存到mysql的方法
2016/05/09 Javascript
JS验证图片格式和大小并预览的简单实例
2016/10/11 Javascript
在javascript中,null>=0 为真,null==0却为假,null的值详解
2017/02/22 Javascript
详解Vue中使用Axios拦截器
2019/04/22 Javascript
微信小程序开发常见问题及解决方案
2019/07/11 Javascript
js String.prototype.trim字符去前后空格的扩展
2020/08/23 Javascript
Vue路由 重定向和别名的区别说明
2020/09/09 Javascript
vue使用echarts图表自适应的几种解决方案
2020/12/04 Vue.js
[01:31:22]Ti4 循环赛第四日附加赛LGD vs Mouz
2014/07/13 DOTA
[02:17]《辉夜杯》TRG战队巡礼
2015/10/26 DOTA
Python脚本处理空格的方法
2016/08/08 Python
利用python模拟实现POST请求提交图片的方法
2017/07/25 Python
使用python爬取B站千万级数据
2018/06/08 Python
Django ImageFiled上传照片并显示的方法
2019/07/28 Python
python实现大学人员管理系统
2019/10/25 Python
Python&amp;&amp;GDAL实现NDVI的计算方式
2020/01/09 Python
工商管理专业实习大学生自我鉴定
2013/09/19 职场文书
甜品蛋糕店创业计划书范文
2014/02/06 职场文书
上班打牌检讨书
2014/02/07 职场文书
保护动物倡议书
2014/04/15 职场文书
保研推荐信
2014/05/09 职场文书
音乐教育专业自荐信
2014/09/18 职场文书
2015最新婚礼司仪主持词
2015/06/30 职场文书
小学数学新课改心得体会
2016/01/22 职场文书