本文来自《WhyPython》系列,请查看所有文章,好像比较模糊……不过,《流畅的Python》这本书还是值得一读再读,温故知新的。最近在书上偶然发现了一个有点奇怪的知识点,所以我就来聊聊这个话题——也许子类化内置类型有问题?!1.内置类型有哪些?在正式开始之前,首先要科普一下:Python的内置类型有哪些?根据官方文档的分类,内置类型(Built-inTypes)主要包括以下内容:详细文档:https://docs.python.org/3/lib...其中,有well-已知的数字类型、序列类型、文本类型、映射类型等等,当然还有我们之前讲过的boolean类型,……对象等等。这么多内容,本文只关注那些内置类型可调用对象(callable),也就是表面上类似于内置函数的对象:int、str、list、tuple、range、set、dict……这些类型(types)可以简单理解为类(classes)在其他语言中,但是Python在这里没有使用习惯的大驼峰命名法,所以很容易引起一些误解。在Python2.2之后,这些内置类型可以被子类化(subclassing),也就是可以被继承(inherit)。2、内置类型的子类化众所周知,对于一个普通的对象x,Python需要使用public内置函数len(x)求出它的长度,不像Java等面向对象语言,其对象一般都有自己的x.length()方法。(PS:对于这两种设计风格的分析,推荐阅读这篇文章)现在,假设我们要定义一个列表类,希望它有自己的length()方法,同时保留普通列表的所有特性应该有。实验代码如下(仅供演示):#定义一个列表子类classMyList(list):deflength(self):returnlen(self)我们制作自定义类MyList,继承list,定义一个新的length同时()方法。因此,MyList具有append()、pop()等方法,以及length()方法。#添加两个元素ss=MyList()ss.append("Python")ss.append("cat")print(ss.length())#Output:2上面提到的其他内置类型也可以这样做子类化应该不难理解。顺便问一下,子类化内置类型有哪些好处/使用场景?有一个很直观的例子,当我们需要在自定义类中频繁使用一个列表对象时(向其添加/删除元素,将其作为一个整体传递...),此时,如果我们的类继承自列表,你可以直接写self.append()、self.pop(),或者将self作为对象传递,这样就不需要额外定义list对象,写法会更简洁。还有其他好处/使用场景吗?欢迎大家留言讨论~~3.子类化内置类型的“问题”终于要进入本文的正题了:)一般来说,在我们教科书式的认知中,子类中的方法会以相同的方式重写父类中的方法name,也就是说子类方法的搜索优先级高于父类方法。让我们看一个例子。父类Cat和子类PythonCat都有一个say()方法,用来说出当前对象的inner_voice:#PythoncatisacatclassCat():defsay(self):returnself.inner_voice()definner_voice(self):return"喵"classPythonCat(Cat):definner_voice(self):return"喵喵"当我们创建子类PythonCat的对象时,它的say()方法会优先于inner_voice()自己定义的方法,不是Cat父类的inner_voice()方法:my_cat=PythonCat()#如下结果符合预期print(my_cat.inner_voice())#Output:meowprint(my_cat.say())#Output:meow这是编程语言建立的约定,是基本原则。学过面向对象程序设计基础的同学应该都知道。但是,Python在实现继承的时候,似乎并没有完全按照上面的规则来工作。分为两种情况:符合常识:对于用Python实现的类,会遵循“子类先于父类”的原则违背常识:对于真正用C实现的类(即str、list、dict等)这些内置类型),在显式调用子类方法时,遵循“子类先于父类”的原则;但是,当有隐式调用时,它们似乎遵循“父类在子类之前”的原则,即通常的继承规则在这里将失效。与PythonCat例子相比,相当于说当直接调用my_cat.inner_voice()时,会得到正确的“喵”结果,但是调用my_cat.say()时,会得到超出预期的“喵”结果.这是《流畅的Python》(第12.1节)中给出的示例:1)#{'one':1}dd['two']=2#{'one':1,'two':[2,2]}dd.update(three=3)#{'three':3,'one':1,'two':[2,2]}在本例中,dd['two']将直接调用\\_setitem\\_()方法,所以结果符合预期。如果其他测试也符合预期,则最终结果将是{'three':[3,3],'one':[1,1],'two':[2,2]}。而initialization和update()是由父类继承的\\_init\\()和\\update\\()直接调用,然后隐式调用了\setitem\\_()方法。但是并没有调用子类的方法,而是调用了父类的方法,导致意想不到的结果!Python官方实现双重规则的做法有点违背大家的常识。一不留神,可能很容易踩坑。那么为什么会出现这样的异常呢?4.内置类型方法的真面目我们知道内置类型是不会隐式调用子类重写的方法的,接下来,就是Python猫追根究底的时候了它:它为什么不叫它呢?《流畅的Python》书上没继续追问,只是胡乱猜测(应该从源码上验证):内置类型的方法都是用C语言实现的,而在事实上,它们之间没有相互作用。呼叫,所以呼叫时不存在搜索优先级问题。也就是说,前面的说法“\\_init\\()和\\update\\()会隐式调用\setitem\\_()方法”是不准确的!这些法术其实是相互独立的!\\_init\\()有自己的setitem实现,并没有调用父类的\\setitem\\(),当然和子类的\\setitem\\_()也没有关系。从逻辑上讲,字典的\\_init\\()方法包含了\\setitem\\_()的函数,所以我们认为前者会调用后者,这是惯性思维的表现,但实际调用关系可能是这样的:左边的方法打开了语言界面的大门,进入了右边的世界,完成了它所有的使命,并没有回到原来的界面去寻找下一条指令(即,图中红线路径不存在)。不重入的原因很简单,就是C语言之间的代码调用效率更高,实现路径更短,实现过程更简单。同样,dict类型的get()方法与\\_getitem\\()也没有调用关系。如果子类只覆盖\\getitem\\(),当子类调用get()方法时,实际会使用父类的get()方法。(PS:关于这一点,《流畅的Python》和PyPy文档的描述都不准确,他们误认为get()方法会调用\\getitem\\_())也就是说Python的方法本身内置类型不存在调用关系,尽管它们在底层C语言中实现时可能具有可以重用的通用逻辑或方法。我想到了我在《WhyPython》系列中分析过的《Python 为什么能支持任意的真值判断?》。我们在写ifxxx的时候,看似隐含调用了\\_bool\\()和\\len\\_()魔术方法,但实际上程序会根据POP_JUMP_IF_FALSE指令直接进入纯C代码的逻辑,这两个魔术方法都没有调用!因此,在意识到C实现的特殊方法是相互独立的之后,我们再回过头来看内置类型的子类化,就会有一个新的发现:父类的\\_init\\()魔术方法类将破坏语言接口实现。自己的任务,但是它和子类的\\setitem\\_()之间没有路径,也就是图中红线路径不可达。特殊方法各行其道,所以我们会得出与上一篇不同的结论:其实Python严格遵循“子类方法先于父类方法”的继承原则,这并不违反常识!最后但同样重要的是,\\_missing\\_()是一个特例。《流畅的Python》简单模糊地写了一句话,没有过多的展开。经过初步实验,发现子类定义该方法时,get()读取到不存在的key时,正常返回None;但是\\_getitem\\()和dd['xxx']读到一个不存在key的key,会按照子类定义的\\missing\\_()处理。没来得及深入分析,还请知道答案的朋友给我留言。5.对内置类型进行子类化的最佳实践综上所述,对内置类型进行子类化是没有问题的。只是因为我们不认识特殊方法(用C语言实现的方法)的真面目,所以结果有偏差。.那么,这就提出了一个新问题:如果必须继承内置类型,最佳实践是什么?首先,如果继承后不覆盖(overwrite)内置类型的特殊方法,子类化不会有任何问题。其次,如果继承后要重写特殊的方法,记得把要改的方法全部重写。例如,如果要更改get()方法,则必须重写get()方法。如果要改变\\_getitem\\_()方法,我们必须重写它...但是,如果我们只是想重写一些逻辑(即C语言的一部分),那么所有特殊方法使用这个逻辑都变了,比如重写\_setitem\\_()的逻辑同时改变了初始化和update()等操作,那么怎么办呢?我们知道特殊方法之间是没有复用的,也就是说仅仅定义一个new\\_setitem\\_()是不行的,那怎么会同时影响多个方法呢?Python的非官方版本PyPy通过调用内置类型的特殊方法并在它们之间建立连接路径来发现这个问题。当然Python官方也意识到了这个问题,但是并没有改变内置类型的特性,而是提供了新的解决方案:UserString、UserList、UserDict……除了名字不同,基本都可以被认为等同于内置类型。这些类的基本逻辑都是用Python实现的,相当于把之前C语言接口的一些逻辑搬到了Python接口上,在左边建立了一个调用链,这样就解决了一些特殊方法的复用。.与前面的例子相比,采用新的继承方式后,结果如预期: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库提供的类。6.总结写了这么多,该收尾了~~在本系列的上一篇文章中,Python猫从搜索顺序和运行两个方面分析了“为什么内置函数/内置类型不是万能的”speed,本文一脉相承,同时也揭示了一些神秘的、看似有缺陷的内置类型的行为特征。这篇文章虽然受到了《流畅的Python》一书的启发,但在语言表象之外,我们多问了一个“为什么”,从而进一步分析现象背后的原理。总之,内置类型的特殊方法是由C语言独立实现的,在Python语言接口中没有调用关系,所以当内置类型被子类化时,重写的特殊方法只会影响方法本身,不会影响其他特殊方法的效果。如果我们对特殊方法之间的关系有错误的理解,我们可能会认为Python打破了“子类方法先于父类方法”的基本继承原则。(不幸的是《流畅的Python》和PyPy都有这种错误的认知)为了迎合对内置类型的普遍期望,Python在标准库中提供了UserString、UserList、UserDict等扩展类,方便程序员使用继承这些基本类型。数据类型。写在最后:本文属于“WhyPython”系列(Python猫出品),主要关注Python的语法、设计、开发等话题。它以每一个“为什么”的问题为切入点,试图展示Python的魅力。魅力。如果您有其他感兴趣的话题,请填写《Python的十万个为什么? 》中的问卷。公众号【Python猫】,本号连载系列精品文章,包括whyPython系列、喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译、等欢迎关注哦。
