文章第一部分介绍了什么对象,对象有什么优点(多态,封装,继承)以及什么时候要使用对象;第二部分从类的结构入手,说明它的语法规则;第三部分介绍了类与对象的名称空间;第四部分介绍类与类之间的关系;第五部分再次谈到继承涉及多继承的顺序、抽象类等规范化开发;第六部分谈谈类中的双下划线方法和用到的一些内置函数;最后一部分是对概念,面向对象设计的总结。本文参考了《Python基础教程》《Python核心编程》。
面向对象
创建自定义对象(尤其是对象类型或类)是一个Python核心概念,这个概念非常重要,以至于Python与Smalltalk、C++、Java等众多语言一样,被视为一种面向对象的语言。
对象魔法
- 在面向对象编程中,术语对象大致意味着一系列数据(属性)以及一套访问和操作这些数据的方法。
- 使用对象的最重要的好处。:
- 多态:可对不同类型的对象执行相同的操作,而这些操作就像“被施了魔法”一样能够正常运行。
- 封装:对外部隐藏有关对象工作原理的细节。
- 继承:可基于通用类创建出专用类。
多态
术语多态(polymorphism)源自希腊语,意思是“有多种形态”。这大致意味着即便你不知道变量指向的是哪种对象,也能够对其执行操作,且操作的行为将随对象所属的类型(类)而异。
例子:假设你要为一个销售食品的电子商务网站创建在线支付系统,程序将接收来自系统另一部分(或之后设计的类似系统)的购物车。因此你只需计算总价并从信用卡扣除费用即可。
你首先想到的可能是,指定程序收到商品时必须如何表示。例如,你可能要求用元组表示收到的商品,如下所示:
('SPAM', 2.50)
但当价格可能存在一个变动的时候(打折),简单的元组就不够用了,再来一个神奇的网络方法吧,它可以得到变动后的价格。
1
2
3
4
5
6# 不要像下面这样做:
def get_price(object):
if isinstance(object, tuple):
return object[1]
else:
return magic_network_method(object)
注意:这里使用
isinstance
来执行类型/类检查旨在说明:使用类型检查通常是馊主意,应尽可能避免。补充:①要破坏多态,唯一的办法是使用诸如type、issubclass、isinstance等函数显式地执行类型检查,但你应尽可能避免以这种方式破坏多态。我们的目标是:对象按你希望的那样行事,而非它是否是正确的类型(类)。
②这里讨论的多态形式是Python编程方式的核心,有时称为鸭子类型。这个术语源自如下说法:“如果走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”
鸭子类型:所有的类型制定了规范(如不同的类有相同的方法名,如len方法的本质,含有
__len__
方法的类,这样我们使用len(对象)就可以得到不同类型对象的长度)。(另一种多态是从继承表现出来的)
前面的代码使用函数isinstance
来检查object是否是元组。如果是,就返回其第二个元素, 否则调用一个神奇的网络方法。
如果网络方法已就绪,问题就暂时解决了。但这种解决方案还是不太灵活。如果有位程序员很聪明,决定用十六进制的字符串表示价格,并将其存储在字典的’price’键下呢?没问题,你只需更新相应的函数。
1 | # 不要像下面这样做: |
你确定现在考虑到了所有的可能性吗?①平时的想法就是那我就继续增加分支呗。
假设有人决定添加一种新字典,并在其中将价格存储在另一个键下,你该如何办呢?当然,可再次更新get_price,每当有人以不同的方式实现对象时,②你都需要重新实现你的模块。
小结:面向过程处理这种情况需要:
① 当有新的类型的数据时可以增加分支
② 当这个类型的数据发生变化时,更改原来对于这个类型数据的处理方法(get_price)
解决方法:让对象自己去处理这种操作,每种新对象都能够获取或计算其价格并返回结果,而你只需向它们询问价格即可。这正是多态(从某种程度上说还有封装)的用武之地。
多态和方法
你收到一个对象,却根本不知道它是如何实现的——它可能是众多“形态”中的任何一种。 你只知道可以询问其价格,但这就够了。至于询问价格的方式,你应该很熟悉。
1
object.get_price()
像这样与对象属性相关联的函数称为方法。
抛开这个例子回顾以前,我们以前也见到很多多态的例子:
1
2
3
4'abc'.count('a')
1
1, 2, 'a'].count('a') [
1 如果有一个变量x,你无需知道它是字符串还是列表就能调用方法count:只要你向这个方法提供一个字符作为参数,它就能正常运行。
多态形式多样 (处处皆多态)
每当无需知道对象是什么样的就能对其执行操作时,都是多态在起作用。这不仅仅适用于方法,我们还通过内置运算符和函数大量使用了多态。请看下面的代码:
1
2
3
41 + 2
3
'Fish' + 'license'
'Fishlicense'上述代码表明,加法运算符(+)既可用于数(这里是整数),也可用于字符串(以及其他类 型的序列)。 本质上是因为再int类,str类中定义了
__add__
方法。 我们通常定义的函数:
1
2
3def add(x, y):
return x + y
# 当然这里有一个小细节就是x和y应该是相同的类。如 1 + '1'就会报错 这也许有点傻,但重点在于参数可以是任何支持加法的对象。很多函数和运算符都是多态的,你编写的大多数函数也可能如此,即便你不是有意为之。每当你使用多态的函数和运算符时,多态都将发挥作用,这也体现了处处是多态。
小结:
- 以上讨论的多态形式是Python编程方式的核心,有时称为鸭子类型。为了遵守某种规则/协议,我们定义了xx方法。
- 多态让你无需知道对象所属的类(对象的类型)就能调用其方法,这也是我们在面向对象思考的一个套路。
封装
封装(encapsulation)指的是向外部隐藏不必要的细节。这听起来有点像多态(无需知道对象的内部细节就可使用它)。这两个概念很像,因为它们都是抽象的原则。它们都像函数一样, 可帮助你处理程序的组成部分,让你无需关心不必要的细节。
但封装不同于多态。多态让你无需知道对象所属的类(对象的类型)就能调用其方法,而封装让你无需知道对象的构造就能使用它。听起来还是有点像?下面来看一个使用了多态但没有使用封装的示例。假设你有一个名为OpenObject的类
1
2
3
4# 对象就是这样创建的 o = OpenObject()
'Sir Lancelot') o.set_name(
o.get_name()
'Sir Lancelot' 你(通过像调用函数一样调用类)创建一个对象,并将其关联到变量o,然后就可以使用方法set_name和get_name了(假设OpenObject支持这些方法)。一切都看起来完美无缺。然而,如果 o将其名称存储在全局变量global_name中呢?
1
2global_name
'Sir Lancelot' 这意味着使用OpenObject类的实例(对象)时,你需要考虑global_name的内容。事实上,必 须确保无人能修改它。
1
2
3'Sir Gumby' global_name =
o.get_name()
'Sir Gumby' 如果尝试创建多个OpenObject对象,将出现问题,因为它们共用同一个变量。 (全局变量/类中的静态变量)
1
2
3
4
5o1 = OpenObject()
o2 = OpenObject()
'Robin Hood') o1.set_name(
o2.get_name()
'Robin Hood' 如你所见,设置一个对象的名称时,将自动设置另一个对象的名称。这可不是你想要的结果。
基本上,你希望对象是抽象的:当调用方法时,无需操心其他的事情,如避免干扰全局变量。 如何将名称“封装”在对象中呢?没问题,将其作为一个属性即可。小结:上面提到的是类的静态变量出现的问题,为了避免共用,我们使用封装的思想,将其作为对象的属性。
封装:将一些东西内容封装到一个地方,你还可以取出来使用。
这样的话:
1
2
3
4
5
6
7
8o1 = ClosedObject()
o2 = ClosedObject()
'小白') o1.set_name(
'小黑') o2.set_name(
o1.get_name()
'小白'
o2.get_name()
'小黑' ‘小白’这个名称还在!因为对象有了自己的状态。对象的的状态由其属性(如名称)描述。对象的方法可能修改这些属性,因此对象将一系列函数(方法)组合起来,并赋予它们访问一些变量(属 性)的权限,而属性可用于在两次函数调用之间存储值。
小结:当我们只使用多态而不是用封装,我们会出现问题(全局变量/类的静态变量,共用,一改都改)。封装的思想可以做的真正的隔离,如对象的属性。
继承
继承是另一种偷懒的方式(这里是褒义)。程序员总是想避免多次输入同样的代码。可以通过创建函数来达成这个目标,但现在要解决一个更微妙的问题。如果你已经有了一个类,并要创建一个与之很像的类(可能只是新增了几个方法),该如何办呢?创建这个新类时,你不想复制旧类的代码,将其粘贴到新类中。所以我们使用继承。
类
类到底是什么?
类/类型——一种对象。每个对象都属于特定的类,并被称为该类的实例。
例如,如果你在窗外看到一只鸟,这只鸟就是“鸟类”的一个实例。鸟类是一个非常通用(抽 象)的类,它有多个子类:你看到的那只鸟可能属于子类“云雀”。你可将“鸟类”视为由所有鸟组成的集合,而“云雀”是其一个子集。一个类的对象为另一个类的对象的子集时,前者就是后者的子类。因此“云雀”为“鸟类”的子类,而“鸟类”为“云雀”的超类。
注意 在英语日常交谈中,使用复数来表示类,如birds(鸟类)和larks(云雀)。在Python中, 约定使用单数并将首字母大写,如Bird和Lark。
注意 在较旧的Python版本中,类型和类之间泾渭分明:内置对象是基于类型的,而自定义对象是基于类的。因此,你可以创建类,但不能创建类型。在较新的Python 2版本中,这种差别不那么明显。在Python 3中,已不再区分类和类型了。
创建自定义类
创建自定义类
1 | # 创建一个类,里面拥有完整的结构:静态变量,动态变量,私有变量,类方法,绑定方法,静态方法,property属性。 |
这个例子包含静态变量,动态变量,私有变量,类方法,绑定方法,静态方法,property属性等全部语法。
不要慌,虽然是这么多,但总体分为两类,一类是属性,一类是方法。接下来就是对属性与方法的详细阐述,但始终是这两大类,只不过角度略有不同。
从类名的角度使用类
1 | class Person(object): |
类名调用类中的属性(类名.属性)
1 | print(Student.daily) |
特殊的类属性
对任何类C,下表显示了类C的所有特殊属性:
类属性 | 解释 |
---|---|
C.__name__ | 类C的名字(字符串) |
C.__doc__ | 类C的文档字符串 |
C.__base__ | 类C的所有父类构成的元组 |
C.__dict__ | 类C的属性 |
C.__module__ | 类C定义所在的模块 |
C.__class__ | 实例C对应的类(仅新式类中) |
类名调用对象方法
1 | p = Person('小白',23,180,65) |
它体现了一种绑定(binding)的效果,没有实例,方法不能够被调用。
①方法必须绑定(到一个实例)才能直接被调用(对象.方法)
②非绑定的方法可能可以被调用,但实例对象一定要明确给出,才能调用成功。
类方法(类名.类方法)
1 | Student.addNum() |
__num
是一个私有变量,只能在类的内部访问,设置它是为了统计班级的人数,所以将其设置为类的一个属性,接着通过类调用addNum
方法,实现班级人数加一,然后通过getNum()
打印班级人数。
what
- 使用
classmethod
装饰器装饰的,第一个参数cls
表示类对象。
how
- 类方法是通过装饰器
classmethod
装饰实现的,没有这个装饰器,类是不能调用绑定方法的,只能对象去调用绑定方法。
where
①类方法的存在是为了更有逻辑(有时候类名比对象名更适合作为主体,如上面的student的人数,比个体更合适)。
②不需要实例化,类方法通常用来对类属性的引用或修改。
③ __new__
方法中,第一个参数就是cls
④ 使用@classmethod多态性构造对象 (effective-python中)
也就是怎么可以不通过__init__
方法来实现类似于重载的构造方法的体现。
静态方法(类名.静态方法)
1 | Student.homework() |
what
- 静态方法就是一个普通的函数,只是碰巧在类的定义体中,而不是在模块层定义。
how:
- 使用
staticmethod
装饰器进行装饰,且不需要第一位置参数。
where
①不必实例化的时候可以考虑。
②在没有继承的情况下是可以与classmethod
互换使用的,当这个类被继承了成为基类,然后基类因为某些原因被删除了,那么它们就不能互换使用了,而且类方法会比静态方法更合适,静态方法可能报错。
1 | class p: |
注意:静态方法本质上是函数:
1 | from types import MethodType |
函数与方法的区别
判断一:
- 函数:全都是显性传参
- 方法:隐性传参
判断二:
利用types模块类中的FunctionType类和MethodType类
1 | from types import FunctionType |
实例化对象
实例化对象发生了三件事
1 | obj = Student() # 实例化一个对象 |
- 调用
__new__
方法在内存中创建一个对象空间,并返回地址。 - 将对象空间传给self参数,自动执行
__init__
方法。 - 执行
__init__
方法时,给对象空间封装其属性。
对象调用对象的属性(对象.属性)
增、删、查、改
1 | class Student: |
注意:如果删的这个属性在对象空间中不存在则会报错,它不会去操作类空间中的属性。
对象调用类中的属性
一边情况下,对象可以引用类中的属性(类中不存在同名属性),但是不可以修改,因为修改相当于在对象的属性中进行增加。
特殊情况:
- 当属性变量是可变数据类型,才能在类中间接的改变。
1
2
3
4
5
6
7
8
9
10
11
12class Student:
'''此类是构建学生类'''
daily = '学习' # 属性:静态属性
examination = '考试'
li = []
def __init__(self,name,sex):
pass
s1.li.append(2)
print(s1.li) # [2]
print(s1.__dict__) # {'name': '小黑', 'sex': '女'}
print(Student.li) # [2]
对象调用类中的方法(对象.绑定方法)
1 | s1 = Student('小白',18,1.8,60,'高三') # 实例/对象 |
- 对象的方法也称绑定方法或方法。为什么称为绑定?因为在定义中,我们给第一个位置参数传对象的空间地址,这样产生了一种绑定关系,所以称之为绑定方法。self是约定俗成的。它只需要占据第一个位置参数就行。
- 使用方法可以访问,操作属性。
property属性
1 | class Person(object): |
why
- 像体脂这样的名词,我们逻辑上更愿意像使用属性的方式去使用它,所以就产生了这种用法。
how
加上@property装饰器后,将方法伪装成属性使用,在调用这个方法的时候就可以不用加括号直接得到返回值。
设置属性的另一种写法
property是一个类,通过实例化对象产生属性。
1 | class Foo: |
- 注意:装饰的这个方法,不能有参数。
where
- 第一个场景:将方法伪装成属性
- 第二个场景:和私有属性合作,只能看不能改,用装饰的这种方式,让人只能看不能改。
- 第三个场景:能修改,但必须得符合我的逻辑。
所以property常用来替代私有属性以及访问私有属性的方法。
@方法名.setter
进一步伪装—>可以修改,删除这个属性
再加一个装饰器 @方法名.setter
这么做的目的在于,要求符合自己的要求才能改,增加容错能力。
三部曲:
- 必须有这个方法
- @方法名.setter
- 再次定义同名方法,它只能接受除self外的一个参数。
1 | # 例子 |
@方法名.deleter
增加了删除属性的能力:
@方法名.deleter
1
2del p1.name
并不能真的删除什么,只是调用了对应的被@price.deleter装饰的方法而已。
结论
- 类就像一个模板,它可以实例化多个对象。
- 对于方法和属性来说,它有两个角度,一个是类一个是对象,类可以使用静态属性或私有变量,类可以调用类方法和静态方法;对于对象来说,对象可以使用静态属性,动态属性,私有属性等全部属性。对象也可以调用类方法,静态方法,绑定方法等全部。
类的名称空间
下面两条语句大致等价:
1 | def foo(x): return x * x |
它们都创建一个返回参数平方的函数,并将这个函数关联到变量foo。可以在全局(模块)作用域内定义名称foo,也可以在函数或方法内定义。定义类时情况亦如此:在class语句中定义的代码都是在一个特殊的命名空间(类的命名空间)内执行的,而类的所有成员都可访问这个命名空间。类定义其实就是要执行的代码段。
类的命名空间:
第一部分:静态变量
第二部分:方法、函数
对象的空间:
类指针,存储了类所在的内存地址。
动态属性
对象和类的关系:
单向作用(类指针):这个单向是从对象到类是一个单项的过程,不能从类引用对象里的属性。
__init__()
是在解释器为你创建一个实例后调用的第一个方法。
调用父类的__new__()
来创建对象__new__()
必须返回一个合法的实例,这样解释器在调用__init__()
时,就可以把这个实例作为self参数传给它。
类与类之间的关系
一个类被定义后,目标就是要将它当成一个模块来使用,并且把这些对象嵌入到你的代码中去,同其它数据类型及逻辑执行流混合使用。有两种方法可以在你的代码中利用类。第一种是组合(composition)。就是让不同的类混合并加入到其他类中来增加功能和代码重用性。另一种方法是通过派生。
组合(合成)
一个类的对象是另外一个类对象的属性
- 对象变成了另一个对象的一个属性 (这就在一个类和其它组成类之间定义了一种“有一个”(has-a)的关系)
- 所有的对象关联这个对象,这样可以方便修改。
例子:基于圆形类实现一个圆环类,要求接收参数是外圆半径和内圆半径。
1 | from math import pi |
本题考察了含有重叠部分的两个类时,我们该如何设计,以及这样设计的优点。
- 实例化要放在init里,
- 当我们需要,修改圆的公式时,圆环,圆柱都不需要在修改了,同享这些改变。
- 同样的方法不要用两次,而使用组合来减少代码的重复率。
结论
拿到了对象就像拿到了一切(就像组合中使用的那样);
一个类的方法只能由此类的对象去调用;
一个类的方法的第一个self只接受此类的对象。
当对象之间有更接近的关系时,派生的概念可能对你的应用程序来说更有意义,特别是当你需要一些相似的对象,但却有少许不同功能的时候。
再谈继承
前面提到使用继承是为了解决代码的重复。接下来谈一下继承的语法。
1 | class A: |
上面代码中:B继承A,A是父类,B是子类;A是父类/基类/超类B是子类/派生类;子类可以使用父类中的: 方法/静态变量。
调用过程
1 | class Animal: |
上述代码中:先开辟空间,空间里有一个类对象指针指向类Cat。接着调用__init__
,对象在自己的空间中找__init__
没找到,到Cat类中找__init__
也没找到,在找父类Animal中的__init__
,完成对对象空间属性的封装。
当子类和父类的方法重名的时候,我们只使用子类的方法,而不会去调用父类的方法了;当子类想调用父类的同名方法时:
方法一:父类名.方法名(self,)
方法二:super(子类名,self).方法名()
方法二的简写:super().方法名()
注意:方法二是在继承的前提下。
1 | 例子: |
小结
上面提到了继承的语法规则;以及父类和子类方法的选择:
①子类的对象,如果去调用方法,永远优先调用自己的;
②如果自己有就用自己的方法,自己没有就会用父类的;
③如果自己有还想用父类的 : 直接在子类方法中调父类的方法:父类名.方法名(self)。
多继承
单继承就如前面提到的继承的语法规则。但对于多继承就会存在一个继承顺序。
知识铺垫:
只要继承object类就是新式类;在py3中不存在,在py2中,不主动继承object的类都是经典类。
在python3 中所有的类都继承object类;python3 中所有的类都是新式类
规则
在多继承方面:经典类使用深度优先,新式类使用mro算法。
MRO列表
事实上,对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序,我们可以通过
类名.mro()
得到类继承顺序MRO列表是由 C3 线性化算法来实现的,一个类的 MRO 列表就是合并所有父类的 MRO 列表.
计算公式:
表头:列表的第一个元素
表尾:列表中表头以外的元素集合(可以为空)
表头,表尾
[A,B,C] : 表头:A 表尾: [B,C]
[A] : 表头:A 表尾:[]
mro(Child(Base1,Base2)) = [ Child ] + merge( mro(Base1), mro(Base2), [ Base1, Base2] )
(其中Child继承自Base1, Base2)由公式得到列表,再通过下面的merge合并完成得到结果。
merge合并:
1
2
3
4
5
6
7
8
9如计算merge( [E,O], [C,E,F,O], [C] )
有三个列表 : ① ② ③
1 merge不为空,取出第一个列表列表①的表头E,进行判断
各个列表的表尾分别是[O], [E,F,O],E在这些表尾的集合中,因而跳过当前当前列表
2 取出列表②的表头C,进行判断
C不在各个列表的集合中,因而将C拿出到merge外,并从所有表头删除
merge( [E,O], [C,E,F,O], [C]) = [C] + merge( [E,O], [E,F,O] )
3 进行下一次新的merge操作 .....补充:也可以在拓扑排序的基础上加上从左到右这个规则进行。
对于新式类继承顺序,使用类名.mro()
方法进行查看。
super
super()严格意义并不是执行父类的方法(单继承是,多继承是有问题的)
super是按照self从属类的mro顺序,来寻找当前类的下一个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22例子:
class A:
def f1(self):
print('in A')
class Foo(A):
def f1(self):
super().f1()
print('in Foo')
class Bar(A):
def f1(self):
print('in Bar')
class info(Foo,Bar):
def f1(self):
super().f1()
print('in Info f1')
a = info()
print(info.__mro__) # [info,Foo,Bar,object]
a.f1()
#(<class '__main__.info'>, <class '__main__.Foo'>, <class '__main__.Bar'>, <class '__main__.A'>, <class 'object'>)
in Bar
in Foo
in Info f1
在py3中不需要传参数,自动帮我们寻找当前类的mro顺序的下一个类中的同名方法。
- 默认super() , ()里面是(当前类名,self)
在py2,新式类中,必须得加参数
super(子类的名字,子类的对象).函数名()
;而且经典类不支持super(因为它都没有mro)例子:
1
2在D类中找super的func,那么可以这样写super().func()
也可以这样写super(D,self).func()(并且在py2中的新式类中必须这样写)在单继承的程序中,super就是找父类。
推荐:是用super来调用父类的方法
1
2
3
4
5
6
7
8
9
10
11
12
13class A(object):
def func(self):
print('in A')
# class B(A):
# super().func()
# b = B()
# 上面注释部分会报错 # RuntimeError: super(): no arguments
class B(A):
def func(self):
# super().func()
super(B,self).func() # 这两句是等价的
b = B()
b.func()
通过继承实现的类的开发规范
抽象类:是一个开发的规范,约束它的所有子类必须实现一些和它同名的方法。(java中的)
方法一:(在父类建立约束)对于同名方法的规范,定义一个父类,如果子类没有这个方法,调用父类中的这种方法,就会主动引起一个错误
- 优点:不依赖于模块
- 推荐的!!!
- 缺点:约束力不够强,只有在使用这个方法的时候才会报错。
例子:
1 | 支付程序 |
补充:在我们查看源码时,很多都写着pass,这是在建议我们继承后要写同名方法。
方法二:使用abc
模块的ABCMeta
,abstractmethod
来进行强约束。
- 优点:约束力强,对于没有被
@abstractmethod
标记的父类方法,如果子类中不存在这个方法就无法创建
例子:
1 | 实现抽象类的另一种方式,约束力强,依赖abc模块 |
类中的常用双下划线方法
双下方法是特殊方法,他是解释器提供的由双下划线加方法名(__方法名__
)具有特殊意义的方法,双下方法主要是python源码程序员使用的,我们在开发中尽量不要使用双下方法,但是深入研究双下方法,更有益于我们阅读源码。
__new__
构造方法
回顾之前实例化的三步走并进行补充:
- 先创建一块对象的空间,有一个指针能指向类 ——> 它是由
__new__
方法完成的。 - 在调用init
该方法需要cls也就是类对象(有了类对象才能开辟对象空间)
1 | # 创造并返回一个新对象 |
单例模式
一个类从头到尾只会实例化一次(只创建一次self的空间)。
这个类的对象不是个性的,主要是实例化对象之后去执行类中的方法。
节省内存!
1 | class Baby: |
上述代码是一个单例模式,关键在于只需要调用一次 __new__
方法,所以我们借助flag完成单例模式的设计。
另一种单例(模块导入的方式):
1 | 单例.py |
__del__
析构方法
调用del x 不表示调用了 x.__del__()
——它只是减少了x的引用计数,当引用计数为0的时候会自动调用 __del__()
子类没有就调用父类的。
1 | class A: |
__call__
callable(对象) 用来判断这个对象能不能运行。
对象()调用的是 __call__
方法下面的逻辑。
1 | class A: |
__len__
len(对象) 能不能计算对象的长度,就看对象里有没有定义 __len__
方法。
1 | lst = [1,2,3] |
len(对象) 需要实现这个类中的 __len__
方法。
__str__
- 当你想打印对象,%s拼接对象,str(对象)的时候,必须在类内定义一个
__str__
方法,才能在打印对象的时候,打印想要的内容,而不是内存地址。
1 | lst = [1,2,3] |
只能返回 str数据类型。
当打印对象,%s拼接对象,str(对象)的时候,我们总是调用这个对象的
__str__
方法,如果找不到就调用__repr__
方法。
__repr__
__repr__
不仅是__str__
的替代品,还有自己的功能用 %r占位或repr(对象)的时候,总是调用
__repr__
方法。str与repr的关系:
- 帮助我们在打印,展示对象的时候更直观的显示对象的内容
- str打印对象,%s拼接对象,str(对象)的时候使用。
- repr是str的备胎,同时还和%r 和repr 有合作关系。
- str范围更大一些,repr更精准,范围更小
__item__ 系列
对对象进行类似于字典的操作
1 | class Foo: |
__enter__与__exit__
对一个对象类似于进行with语句上下文管理的操作,必须在类中定义 enter exit , 在enter这个方法中必须设置返回值,它将对象返回给obj。
1 | class A: |
__iter__
可迭代对象
1 | class A: |
__hash__
与 __eq__
1 | class A: |
set()函数的实现机制:
- 当两个变量的哈希值不相同时,就认为这两个变量是不同的。
- 当两个变量哈希值一样时,调用
__eq__
方法,当返回值为True时认为这两个变量是同一个,应该去除一个。返回FALSE时,不去重。
使用过的函数
函数 | 描述 |
---|---|
callable(object) | 判断对象是否是可调用的(如是否是函数或方法) |
getattr(object, name[,default]) | 获取属性的值,还可提供默认值 |
hasattr(object, name) | 确定对象是否有指定的属性 |
isinstance(object, class) | 确定对象是否是指定类的实例 |
issubclass(A, B) | 确定A是否是B的子类 |
setattr(object, name, value) | 将对象的指定属性设置为指定的值 |
type(object) | 返回对象的类型 |
元类type
按照Python的一切皆对象理论,类其实也是一个对象,那么类这个对象是从哪里实例化出来的呢?
1 | print(type([1,2,3])) |
type元类是获取该对象从属于的类,而type类比较特殊,Python原则是:一切皆对象,其实类也可以理解为’对象’,而type元类又称作构建类,python中大多数内置的类(包括object)以及自己定义的类,都是由type元类创造的。
而type类与object类之间的关系比较独特:object是type类的实例,而type类是object类的子类,这种关系比较神奇无法使用python的代码表述,因为定义其中一个之前另一个必须存在。
1 | print(type(object)) # <class 'type'> object是type的实例化对象 |
总结
概念
对象:对象由属性和方法组成。属性不过是属于对象的变量,而方法是存储在属性中的函数。相比于其他函数,(关联的)方法有一个不同之处,那就是它总是将其所属的对象作为第一个参数,而这个参数通常被命名为self。
类:类表示一组(或一类)对象,而每个对象都属于特定的类。类的主要任务是定义其实例将包含的方法。
多态:多态指的是能够同样地对待不同类型和类的对象,即无需知道对象属于哪个类就可调用其方法。
补充:为不同数据类型的实体提供统一的接口(理解:这个人可以接受各种各样的朋友)
封装:对象可能隐藏(封装)其内部状态。在有些语言中,这意味着对象的状态(属性) 只能通过其方法来访问。(将一些东西内容封装到一个地方,不受外界干扰,同时你还可以取出来。)
继承:一个类可以是一个或多个类的子类,在这种情况下,子类将继承超类的所有方法。 你可指定多个超类,通过这样做可组合正交(独立且不相关)的功能。为此,一种常见的做法是使用一个核心超类以及一个或多个混合超类。
继承的优点:重载、增强耦合、减少代码重复
缺点:耦合性增强后、会增加代码难度,牵一发动全身,慎用。
接口和内省:一般而言,你无需过于深入地研究对象,而只依赖于多态来调用所需的方 法。然而,如果要确定对象包含哪些方法或属性,有一些函数可供你用来完成这种工作。
- 补充:这种手动检查的方法是(hasattr、callable)
- 来源:历史上的大部分时间内,Python几乎都只依赖于鸭子类型,即假设所有对象都能完成其工作,同时偶尔使用hasattr来检查所需的方法是否存在。
抽象基类:使用模块abc可创建抽象基类。抽象基类用于指定子类必须提供哪些功能,却 不实现这些功能。
- 来源:很多其它语言(如Java和Go)都采用引用模块abc提供了官方的解决方案。这个模块为所谓的抽象基类提供了支持。一般而言,抽象类是不能(至少是不应该)实例化的类,其职责是定义子类应实现的一组抽象方法。
面向对象设计
设计面向对象模型
(1) 将有关问题的描述(程序需要做什么)记录下来,并给所有的名词、动词和形容词加上标记。
(2) 在名词中找出可能的类。
(3) 在动词中找出可能的方法。
(4) 在形容词中找出可能的属性。
(5) 将找出的方法和属性分配给各个类。
进一步:
(1) 记录(或设想)一系列用例,即使用程序的场景,并尽力确保这些用例涵盖了所有的功能。
(2) 透彻而仔细地考虑每个场景,确保模型包含了所需的一切。如果有遗漏,就加上;如果 有不太对的地方,就修改。不断地重复这个过程,直到对模型满意为止。
有了你认为行之有效的模型后,就可以着手编写程序了。
关于面向对象设计的一些思考
将相关的东西放在一起。如果一个函数操作一个全局变量,最好将它们作为一个类的属 性和方法。
不要让对象之间过于亲密。方法应只关心其所属实例的属性,对于其他实例的状态,让它们自己去管理就好了。
慎用继承,尤其是多重继承。继承有时很有用,但在有些情况下可能带来不必要的复杂性。要正确地使用多重继承很难,要排除其中的bug更难。
保持简单。让方法短小紧凑。一般而言,应确保大多数方法都能在30秒内读完并理解。 对于其余的方法,尽可能将其篇幅控制在一页或一屏内。