返回顶部
首页 > 资讯 > 后端开发 > Python >为什么继承Python内置类型会出问题
  • 442
分享到

为什么继承Python内置类型会出问题

2023-06-16 00:06:29 442人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

这篇文章主要讲解了“为什么继承python内置类型会出问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“为什么继承Python内置类型会出问题”吧! 1、内置类型有哪些?在正式开始

这篇文章主要讲解了“为什么继承python内置类型会出问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“为什么继承Python内置类型会出问题”吧!

 1、内置类型有哪些?

在正式开始之前,我们首先要科普一下:哪些是 Python 的内置类型?

根据官方文档的分类,内置类型(Built-in Types)主要包含如下内容:

详细文档:https://docs.python.org/3/library/stdtypes.html

其中,有大家熟知的数字类型、序列类型、文本类型、映射类型等等,当然还有我们之前介绍过的布尔类型、...对象 等等。

在这么多内容里,本文只关注那些作为可调用对象(callable)的内置类型,也就是跟内置函数(built-in  function)在表面上相似的那些:int、str、list、tuple、range、set、dict……

这些类型(type)可以简单理解成其它语言中的类(class),但是 Python 在此并没有用习惯上的大驼峰命名法,因此容易让人产生一些误解。

在 Python 2.2 之后,这些内置类型可以被子类化(subclassing),也就是可以被继承(inherit)。

2、内置类型的子类化

众所周知,对于某个普通对象 x,Python 中求其长度需要用到公共的内置函数 len(x),它不像 Java  之类的面向对象语言,后者的对象一般拥有自己的 x.length() 方法。(PS:关于这两种设计风格的分析,推荐阅读 这篇文章)

现在,假设我们要定义一个列表类,希望它拥有自己的 length() 方法,同时保留普通列表该有的所有特性。

实验性的代码如下(仅作演示):

# 定义一个list的子类 class MyList(list):     def length(self):         return len(self)

我们令 MyList这个自定义类继承 list,同时新定义一个 length() 方法。这样一来,MyList 就拥有 append()、pop()  等等方法,同时还拥有 length() 方法。

# 添加两个元素 ss = MyList() ss.append("Python") ss.append("猫")  print(ss.length())   # 输出:2

前面提到的其它内置类型,也可以这样作子类化,应该不难理解。

顺便发散一下,内置类型的子类化有何好处/使用场景呢?

有一个很直观的例子,当我们在自定义的类里面,需要频繁用到一个列表对象时(给它添加/删除元素、作为一个整体传递……),这时候如果我们的类继承自  list,就可以直接写 self.append()、self.pop(),或者将 self  作为一个对象传递,从而不用额外定义一个列表对象,在写法上也会简洁一些。

还有其它的好处/使用场景么?欢迎大家留言讨论~~

3、内置类型子类化的“问题”

终于要进入本文的正式主题了:)

通常而言,在我们教科书式的认知中,子类中的方法会覆盖父类的同名方法,也就是说,子类方法的查找优先级要高于父类方法。

下面看一个例子,父类 Cat,子类 PythonCat,都有一个 say() 方法,作用是说出当前对象的 inner_voice:

# Python猫是一只猫 class Cat():     def say(self):         return self.inner_voice()     def inner_voice(self):         return "喵" class PythonCat(Cat):     def inner_voice(self):         return "喵喵"

当我们创建子类 PythonCat 的对象时,它的 say() 方法会优先取到自己定义出的 inner_voice() 方法,而不是 Cat 父类的  inner_voice() 方法:

my_cat = PythonCat() # 下面的结果符合预期 print(my_cat.inner_voice()) # 输出:喵喵 print(my_cat.say())         # 输出:喵喵

这是编程语言约定俗成的惯例,是一个基本原则,学过面向对象编程基础的同学都应该知道。

然而,当 Python 在实现继承时,似乎不完全会按照上述的规则运作。它分为两种情况:

  • 符合常识:对于用 Python 实现的类,它们会遵循“子类先于父类”的原则

  • 违背常识:对于实际是用 C  实现的类(即str、list、dict等等这些内置类型),在显式调用子类方法时,会遵循“子类先于父类”的原则;但是,**在存在隐式调用时,**它们似乎会遵循“父类先于子类”的原则,即通常的继承规则会在此失效

对照 PythonCat 的例子,相当于说,直接调用 my_cat.inner_voice() 时,会得到正确的“喵喵”结果,但是在调用  my_cat.say() 时,则会得到超出预期的“喵”结果。

下面是《流畅的Python》中给出的例子(12.1章节):

class DoppelDict(dict):      def __setitem__(self, key, value):          super().__setitem__(key, [value] * 2)  dd = DoppelDict(one=1)  # {'one': 1} dd['two'] = 2           # {'one': 1, 'two': [2, 2]} dd.update(three=3)      # {'three': 3, 'one': 1, 'two': [2, 2]}

为什么继承Python内置类型会出问题

在这个例子中,dd['two']  会直接调用子类的__setitem__()方法,所以结果符合预期。如果其它测试也符合预期的话,最终结果会是{'three': [3, 3], 'one':  [1, 1], 'two': [2, 2]}。

然而,初始化和 update()  直接调用的分别是从父类继承的__init__()和__update__(),再由它们隐式地调用__setitem__()方法,此时却并没有调用子类的方法,而是调用了父类的方法,导致结果超出预期!

官方 Python 这种实现双重规则的做法,有点违背大家的常识,如果不加以注意,搞不好就容易踩坑。

那么,为什么会出现这种例外的情况呢?

4、内置类型的方法的真面目

我们知道了内置类型不会隐式地调用子类覆盖的方法,接着,就是Python猫的刨根问底时刻:为什么它不去调用呢?

《流畅的Python》书中没有继续追问,不过,我试着胡乱猜测一下(应该能从源码中得到验证):内置类型的方法都是用 C  语言实现的,事实上它们彼此之间并不存在着相互调用,所以就不存在调用时的查找优先级问题。

也就是说,前面的“__init__()和__update__()会隐式地调用__setitem__()方法”这种说法并不准确!

这几个魔术方法其实是相互独立的!__init__()有自己的 setitem  实现,并不会调用父类的__setitem__(),当然跟子类的__setitem__()就更没有关系了。

从逻辑上理解,字典的__init__()方法中包含__setitem__()的功能,因此我们以为前者会调用后者,**这是惯性思维的体现,**然而实际的调用关系可能是这样的:

为什么继承Python内置类型会出问题

左侧的方法打开语言界面之门进入右侧的世界,在那里实现它的所有使命,并不会折返回原始界面查找下一步的指令(即不存在图中的红线路径)。不折返的原因很简单,即  C 语言间代码调用效率更高,实现路径更短,实现过程更简单。

同理,dict 类型的 get() 方法与__getitem__()也不存在调用关系,如果子类只覆盖了__getitem__()的话,当子类调用  get() 方法时,实际会使用到父类的 get() 方法。(PS:关于这一点,《流畅的Python》及 PyPy 文档的描述都不准确,它们误以为 get()  方法会调用__getitem__())

也就是说,Python 内置类型的方法本身不存在调用关系,尽管它们在底层 C 语言实现时,可能存在公共的逻辑或能被复用的方法。

我想到了“Python为什么”系列曾分析过的《Python 为什么能支持任意的真值判断?》。在我们写if  xxx时,它似乎会隐式地调用__bool__()和__len__()魔术方法,然而实际上程序依据 POP_JUMP_IF_FALSE 指令,会直接进入纯 C  代码的逻辑,并不存在对这俩魔术方法的调用!

因此,在意识到 C 实现的特殊方法间相互独立之后,我们再回头看内置类型的子类化,就会有新的发现:

为什么继承Python内置类型会出问题

父类的__init__()魔术方法会打破语言界面实现自己的使命,然而它跟子类的__setitem__()并不存在通路,即图中红线路径不可达。

特殊方法间各行其是,由此,我们会得出跟前文不同的结论:实际上 Python 严格遵循了“子类方法先于父类方法”继承原则,并没有破坏常识!

最后值得一提的是,__missing__()是一个特例。《流畅的Python》仅仅简单而含糊地写了一句,没有过多展开。

经过初步实验,我发现当子类定义了此方法时,get() 读取不存在的 key 时,正常返回 None;但是 __getitem__() 和  dd['xxx'] 读取不存在的 key 时,都会按子类定义的__missing__()进行处理。

为什么继承Python内置类型会出问题

我还没空深入分析,恳请知道答案的同学给我留言。

5、内置类型子类化的最佳实践

综上所述,内置类型子类化时并没有出问题,只是由于我们没有认清特殊方法(C 语言实现的方法)的真面目,才会导致结果偏差。

那么,这又召唤出了一个新的问题:如果非要继承内置类型,最佳的实践方式是什么呢?

首先,如果在继承内置类型后,并不重写(overwrite)它的特殊方法的话,子类化就不会有任何问题。

其次,如果继承后要重写特殊方法的话,记得要把所有希望改变的方法都重写一遍,例如,如果想改变 get() 方法,就要重写 get() 方法,如果想改变  __getitem__()方法,就要重写它……

但是,如果我们只是想重写某种逻辑(即 C  语言的部分),以便所有用到该逻辑的特殊方法都发生改变的话,例如重写__setitem__()的逻辑,同时令初始化和update()等操作跟着改变,那么该怎么办呢?

我们已知特殊方法间不存在复用,也就是说单纯定义新的__setitem__()是不够的,那么,怎么才能对多个方法同时产生影响呢?

PyPy 这个非官方的 Python 版本发现了这个问题,它的做法是令内置类型的特殊方法发生调用,建立它们之间的连接通路。

官方 Python  当然也意识到了这么问题,不过它并没有改变内置类型的特性,而是提供出了新的方案:UserString、UserList、UserDict……

为什么继承Python内置类型会出问题

除了名字不一样,基本可以认为它们等同于内置类型。

这些类的基本逻辑是用 Python 实现的,相当于是把前文 C 语言界面的某些逻辑搬到了 Python  界面,在左侧建立起调用链,如此一来,就解决了某些特殊方法的复用问题。

对照前文的例子,采用新的继承方式后,结果就符合预期了:

from collections import UserDict  class DoppelDict(UserDict):     def __setitem__(self, key, value):          super().__setitem__(key, [value] * 2)  dd = DoppelDict(one=1)  # {'one': [1, 1]} dd['two'] = 2           # {'one': [1, 1], 'two': [2, 2]} dd.update(three=3)      # {'one': [1, 1], 'two': [2, 2], 'three': [3, 3]}

显然,如果要继承 str/list/dict 的话,最佳的实践就是继承collections库提供的那几个类。

感谢各位的阅读,以上就是“为什么继承Python内置类型会出问题”的内容了,经过本文的学习后,相信大家对为什么继承Python内置类型会出问题这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

--结束END--

本文标题: 为什么继承Python内置类型会出问题

本文链接: https://lsjlt.com/news/281868.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 为什么继承Python内置类型会出问题
    这篇文章主要讲解了“为什么继承Python内置类型会出问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“为什么继承Python内置类型会出问题”吧! 1、内置类型有哪些在正式开始之...
    99+
    2023-06-16
  • python内置数据类型使用方法和继承关系
    目录1、python包含的内置序列2、使用内置的数据类型2.1 创建2.2 索引2.3 使用for遍历3、可变与不可变4、小练习题 前言: python之父Guido van Ros...
    99+
    2024-04-02
  • 为什么TypeScript的Enum会出现问题
    目录发生了什么呢?什么时候用控制枚举的数字Bit值控制索引非数字枚举结论TypeScript引入了很多静态编译语言的特性,比如class(现在是JavaScript的一部分了),in...
    99+
    2024-04-02
  • 为什么会出现无权限访问phpmyadmin的问题
    小编给大家分享一下为什么会出现无权限访问phpmyadmin的问题,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!由于我安装wam...
    99+
    2024-04-02
  • python中内置类型添加属性问题详解
    目录python中内置类型添加属性问题?普通对象绑定属性解决方法总结python中内置类型添加属性问题? 最近项目 进行重构一些代码: 写代码过程中会遇到一个问题,我希望通过内置类型...
    99+
    2024-04-02
  • golang出现内存逃逸会导致什么问题
    当Golang程序出现内存逃逸时,会导致以下几个问题: 垃圾回收开销增加:内存逃逸会导致对象分配在堆上,而不是在栈上。这意味着垃...
    99+
    2023-10-26
    golang
  • 为什么没有内置的set容器类型?
    ...
    99+
    2024-04-02
  • 深度剖析为什么Python中整型不会溢出
    前言本次分析基于 CPython 解释器,python3.x版本在python2时代,整型有 int 类型和 long 长整型,长整型不存在溢出问题,即可以存放任意大小的整数。在python3后,统一使用了长整型。这也是吸引科研人员的一...
    99+
    2023-01-31
    深度 整型 Python
  • 腾讯云服务器:为什么会出现卡顿问题?
    1. 硬件配置不足 腾讯云服务器的性能与硬件配置直接相关。如果你选择的服务器配置较低,例如CPU、内存或存储空间不足,那么在处理大量请求或运行复杂应用程序时,服务器可能会出现卡顿现象。 2. 网络延迟 网络延迟是另一个可能导致腾讯云服务器...
    99+
    2023-10-27
    腾讯 服务器
  • 腾讯云服务器为什么会出现卡顿问题?
    1. 资源配置不足 腾讯云服务器的性能与所选的配置密切相关。如果你选择的配置不足以支撑你的应用程序或网站的需求,就容易出现卡顿问题。在购买服务器时,需要根据实际需求选择适当的配置,包括 CPU、内存、存储和带宽等。 2. 网络延迟 网络延...
    99+
    2023-10-27
    腾讯 服务器
  • 为什么 Windows 上的 PHP 重定向同步会出现问题?
    在 Windows 上使用 PHP 进行重定向同步时,可能会遇到一些问题。本文将深入探讨这些问题的原因,并提供一些解决方案。 首先,让我们看一下重定向同步的概念。重定向同步是指服务器将客户端的请求重定向到另一个 URL,然后等待该 URL...
    99+
    2023-08-20
    重定向 同步 windows
  • 亚马逊云服务器为什么会出现卡顿问题?
    1. 资源配置不足 亚马逊云服务器(Amazon EC2)提供了多种不同规格的实例供用户选择。如果您选择的实例规格不足以满足您的应用程序的需求,就可能导致服务器卡顿。在选择实例规格时,您需要考虑应用程序的负载、并发用户数以及所需的计算和存...
    99+
    2023-10-27
    亚马逊 服务器
  • 浅析为什么会出现无法生存Git仓库中的公钥的问题
    作为一名开发者,你可能会遇到无法生存 Git 仓库中的公钥的问题,这可能会给你的工作造成相当的麻烦。Git 仓库中的公钥是必要的,它可用于身份验证,保证安全性。那么,为什么会出现无法生存 Git 仓库中的公钥的问题?可能有以下几个原因:生成...
    99+
    2023-10-22
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作