我有一个朋友,他还是应届生吧, 然后去阿里面试,在学校就学了两年左右的Python吧!后来把自己在学习时做的笔记给那个面试官一看,那位HR和领导一沟通就说可以直接入职,先把公司的一些文档整理一遍。最后薪资18K。一名应届生,是否有觉得不可思议,事在人为。什么样的态度决定了什么样的高度!反正我是很庆幸的。这次呢忘了和他讲分享一部分资料了。下次再说,这次给你们分享一些进阶干货
。
本节涉及到的内容
-
面向对象的概念
-
类的封装
-
类的继承
-
类的多态
-
静态方法、类方法 和 属性方法
-
类的特殊成员方法
-
继承层级关系中子类的实例对象对属性的查找顺序问题
一、面向对象的概念
1. "面向对象(OOP)"是什么?
简单点说,“面向对象”是一种编程范式,而编程范式是按照不同的编程特点总结出来的编程方式。俗话说,条条大路通罗马,也就说我们使用不同的方法都可以达到最终的目的,但是有些办法比较快速、安全且效果好,有些方法则效率低下且效果不尽人意。同样,编程也是为了解决问题,而解决问题可以有多种不同的视角和思路,前人把其中一些普遍适用且行之有效的编程模式归结为“范式”。常见的编程范式有:
-
面向过程编程:OPP(Procedure Oriented Programing)
-
面向对象编程:OOP(Object Oriented Programing)
-
函数式编程:(Functional Programing)
面向过程编程的步骤:
1)分析出解决问题所需要的步骤;
2)用函数把这些步骤一次实现;
3)一个一个地调用这些函数来解决问题;
面向对象编程的步骤:
1)把构成问题的事务分解、抽象成各个对象;
2)结合这些对象的共有属性,抽象出类;
3)类层次化结构设计--继承 和 合成;
4)用类和实例进行设计和实现来解决问题。
关于面向对象编程 与 面向过程编程的区别与优缺点可以参考这篇文章
2. 面向对象编程的特点
面向对象编程达到了软件工程的3个目标:重用性、灵活性、扩展性,而这些目标是通过以下几个主要特点实现的:
需要说明的是,Python不像Java中又专门的“接口”定义,Python中的接口与类没有什么区别,但是我们可以通过在一个用于当做接口的类中所定义的方法体中
raise NotImplementedError
异常,来强制子类必须重新实现该方法。
3. 面向对象编程的使用场景
我们知道,Python既可以面向过程编程,也可以面向对象编程。那么什么场景下应该使用面向对象编程呢?如果我们仅仅是写一个简单的脚本来跑一些简单的任务,我们直接用面向过程编程就好了,简单,快速。当我们需要实现一个复杂的系统时,或者以下场景下,就需要使用面向对象编程:
二、类的封装
封装是面向对象的主要特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的方法的逻辑实体,它向外暴露部分数据和方法,屏蔽具体的实现细节。除此之外,在一个对象内部,某些数据或方法可以是私有的,这些私有的数据或方法是不允许外界访问的。通过这种方式,对象对内部数据提供了不同级别的保护以防止程序中无关的部分意外的改变或错误使用了对象的私有部分,比如java中修饰类变量和方法的相关关键字有:private、protected, public等。下面我们通过类的定义和实例化的实例来说明一下Python中的是如何实现对这些不同等级数据的保护的。
1. 类的定义
类的定义是对显示事务的抽象过程和能力,类是一个对象/实例的模板,也是一个特殊的对象/实例(因为Pythobn中一切皆对象,所以类本身也是一个对象)
现在我们来定义个Person类,它有以下3个属性:
-
nationality:国籍
-
name:姓名
-
id:身份证号码
假设现在我们有以下几个前提:
-
所有人的国籍基本都是相同的,且允许直接通过类或实例来访问,允许随意修改
-
大部分人的姓名是不同的,且允许直接通过类的实例来访问和随意修改
-
所有人的身份证号码都是不一样的,且不允许直接通过类或实例来访问或随意修改
2. 类的实例化
类实例化的方式:
类名([参数...])
,参数是__init__方法中除了第一个self参数之外的其他参数,上面定义的这个Person类中,实例化时需要传递的参数只有一个name。比如我们来实例化3个Person对象,他们的name分别是 tom 和 jerry:
tom = Person('tom')jerry = Person('jerry')jack = Person('jack')
3. 不同保护等级的属性说明
公有属性/类属性
直接定义在class下的属性就是公有属性/类属性 ,比如上面那个Person类中的nationality属性。“公有”的意思是这个属性是这个类的所有实例对象共同所有的,因此默认情况下这个属性值值保留一份,而不会为该类的每个实例都保存一份。
成员属性/实例属性
成员属性,又称成员变量 或 实例属性,也就是说这些属性是 该类的每个实例对象单独持有的属性。成员属性需要在类的__init__方法中进行声明,比如上面的Person类中定义的name属性就是一个成员属性。
私有属性和成员属性一样,是在__init__方法中进行声明,但是属性名需要以双下划线__开头,比如上面定义的Person中的__id属性。私有属性是一种特殊的成员属性,它只允许在实例对象的内部(成员方法或私有方法中)访问,而不允许在实例对象的外部通过实例对象或类来直接访问,也不能被子类继承。
通过实例对象访问私有属性:
print(tom.__id)
输出结果
那么要访问私有变量怎么办呢? 有两种办法:
办法1:通过一个专门的成员方法返回该私有变量的值,比如上面定义的get_id()方法,搞过java的同学很自然就会想到java类中的set和get方法。
输出结果:
46bc6b5c-9dd6-11e7-8306-208984d7aa83 46cbfe68-9dd6-11e7-b5d1-208984d7aa8346cbfe69-9dd6-11e7-9b5c-208984d7aa83
办法2:通过
实例对象._类名__私有变量名
的方式来访问
print(tom._Person__id, jerry._Person__id, jack._Person__id)
输出结果:
e1f4ee86-9dd6-11e7-a186-208984d7aa83 e1f5b1f8-9dd6-11e7-b1c3-208984d7aa83 e1f5b1f9-9dd6-11e7-b74a-208984d7aa83
总结
-
公有属性、成员属性 和 私有属性 的受保护等级是依次递增的;
-
私有属性 和 成员属性 是存放在已实例化的对象中的,每个对象都会保存一份;
-
公有属性是保存在类中的,只保存一份;
-
哪些属性应该是公有属性的,哪些属性应该是私有属性 需要根据具体业务需求来确定。
三、类的继承
1. 继承的相关概念
继成 和
组合
是类的两个最主要的关系,而
继承
关系的类之间是有层级的。被继承的类被称为
父类、基类 或 超类
;继承的类被称为
子类 或 派生类
。
2. 继承的作用
继承 是一个从一般到特殊的过程, 子类可以继承现有类的所有功能,而不需要重新实现代码。简单来说就是
继承提高了代码重用性和扩展性
。
3. 继承的分类
Python中类的继承按照父类中的方法是否已实现可分为两种:
如果是根据要继承的父类的个数来分,有可以分为:
-
单继承:
只继承1个父类
-
多继承:
继承多个父类
通常,我们都是用 单继承 ,很少用到 多继承。
4. 类继承实例
类的继承关系
父类/基类/超类--Person
继承说明
-
Teacher类 和 Student类 都继承 Person类,因此Teacher和Student是Person的子类/派生类,而Person是Teacher和Student的父类/基类/超类;
-
Teacher和Student对Person的继承属于实现继承,且是单继承;
-
Teacher类继承了Person的name和age属性,及talk()和walk()方法,并扩展了自己的level和salary属性,及teach()方法;
-
Student类继承了Person的name和age属性,及talk()和walk()方法,并扩展了自己的class属性,及study()方法;
-
Teacher和Student对Person类属性和方法继承体现了 “代码的重用性”, 而Teacher和Student扩展的属性和方法体现了 “灵活的扩展性”;
-
子类需要在自己的__init__方法中的第一行位置调用父类的构造方法,上面给出了两种方法:
-
super(子类名, self).__init__(父类构造参数),如super.(Teacher, self).__init__(name, age)
-
父类名.__init__(self, 父类构造参数),如Person.__init__(self, name, age),这是老式的用法。
-
子类 Teacher 和 Student 也可以在自己的类定义中 重新定义 父类中的talk()和walk()方法,改变其实现代码,这叫做
方法重写
。
关于多继承,以及多继承时属性查找顺序(广度优先、深度优先)的问题会在下面进行单独说明。
四、类的多态
多态是指,相同的成员方法名称,但是成员方法的行为(代码实现)却各不相同。这里所说的多态是通过 继承接口的方式实现的。Java中有interface,但是Python中没有。Python中可以通过在一个成员方法体中抛出一个
NotImplementedError
异常来强制继承该接口的子类在调用该方法前必须先实现该方法的功能代码。
实现了接口方法的子类--Dog 和 Duck
由此可知:
五、属性方法、类方法、静态方法
上面提到过,类中封装的是数据和操作数据的方法。数据就是属性,且上面已经介绍过了属性分为:
公有属性/类变量、成员属性/实例变量 和 私有属性。现在我们来说说类中的方法,类中的方法分为以下几种:
-
成员方法: 上面定义的都是成员方法,通常情况下,它们与成员属性相似,是通过类的实例对象去访问;成员方法的第一个参数必须是当前实例对象,通常写为self;实际上,我们也可以通过类名来调用成员方法,只是此时我们需要手动的传递一个该类的实例对象给成员方法的self参数,这样用明显不是一种优雅的方法,因此基本不会这样使用。
-
私有方法: 以双下划线开头的成员方法就是私有方法,与私有属性类似,只能在实例对象内部访问,且不能被子类继承;私有方法的第一个参数也必须是当前实例对象本身,通常写为self;
-
类方法: 以@classmethod来装饰的成员方法就叫做类方法 ,它要求第一次参数必须是当前类。与公有属性/静态属性 相似,除了可通过实例对象进行访问,还可以直接通过类名去访问,且第一个参数表示的是当前类,通常写为cls;另外需要说明的是,类方法只能访问公有属性,不能访问成员属性,因此第一个参数传递的是代表当前类的cls,而不是表示实例对象的self。
-
静态方法: 以@staticmethod来装饰的成员方法就叫做静态方法 ,静态方法通常都是通过类名去访问,且严格意义上来讲,静态方法已经与这个类没有任何关联了,因为静态方法不要求必须传递实例对象或类参数,这种情况下它不能访问类中的任何属性和方法。
-
属性方法: 这个比较有意思,是指可以像访问成员属性那样去访问这个方法;它的第一个参数也必须是当前实例对象,且该方法必须要有返回值。
我们先来定义这样一个类:
执行代码:
总结:
-
成员方法也可以通过类名去访问,但是有点多此一举的感觉;
-
类方法和静态方法也可以通过实例对象去访问,但是通常情况下都是通过类名直接访问的;
-
最重要的一条总结:类的各种方法,能访问哪些属性实际上是跟方法的参数有关的:
-
比如成员方法要求第一个参数必须是一个该类的实例对象,那么实例对象能访问的属性,成员方法都能访问,而且还能访问私有属性;
-
再比如,类方法要求第一个参数必须是当前类,因此它只能访问到类属性/公有属性,而访问不到成员属性 和 私有属性;
-
再比如,静态方法对参数没有要求,也就意味着我们可以任意给静态方法定义参数;假如我们给静态方法定义了表示当前类的参数,那么就可以访问类属性/公有属性;假如我们给静态方法定义了表示当前类的实例对象的参数,那么就可以访问成员属性;假如我们没有给静态方法定义这两个参数,那么就不能访问该类或实例对象的任何属性。
六、类的特殊成员属性及特殊成员方法
我们上面提到过:名称以双下划线__开头的属性是私有属性,名称以双下划线__开头的方法是私有方法。这里我们要来说明的是,Python的类中有一些内置的、特殊的属性和方法,它们的名称是以双下划线__开头,同时又以双下划线__结尾。这些属性和方法不再是私有属性和私有方法,它们是可以在类的外部通过实例对象去直接访问的,且它们都有着各自特殊的意义,我们可以通过这些特殊属性和特殊方法来获取一些重要的信息,或执行一些有用的操作。
1. 类的特殊成员属性
属性名称
|
说明
|
__doc__
|
类的描述信息
|
__module__
|
表示当前操作的对象对应的类的定义所在的模块名
|
__class__
|
表示当前操作的对象对应的类名
|
__dict__
|
一个字典,保存类的所有的成员(包括属性和方法)或实例对象中的所有成员属性
|
现在来看一个实例:
在dog.py模块定义一个Dog类
总结:
2. 类的特殊成员方法
方法名称
|
说明
|
__init__
|
类构造方法,通过类创建对象时会自动触发执行该方法
|
__del__
|
析构方法,当对象在内存中被什邡市,会自动触发执行该方法。比如实例对象的作用域退出时,或者执行
del 实例对象
操作时。
|
__str__
|
如果一个类中定义了__str__方法,那么在打印对象时默认输出该方法的返回值,否则会打印出该实例对象的内存地址。
|
__xxxitem__
|
是指__getitem__、__setitem__、__delitem这3个方法,它们用于索引操作,比如对字典的操作,分别表示 获取、设置、删除某个条目。 数据。可以通过这些方法来定义一个类对字典进行封装,从而可以对字典中key的操作进行控制,尤其是删除操作。
|
__new__
|
该方法会在__init__方法之前被执行,该方法会创建被返回一个新的实例对象,然后传递给__init__。另外需要说明的是,这不是一个成员方法,而是一个静态方法。
|
__call__
|
源码中的注释是"Call self as a function." 意思是把自己(实例对象)作为一个函数去调用,而函数的调用方式是
函数名()
。也就是说,当我们执行
实例对象()
或者
类名()()
这样的操作时会触发执行该方法。
|
示例1
先来定义这样一个类:
执行下面的代码:
可以看到,所有代码都执行完后,进程退出时实例对象的__del__方法才被调用,这是因为对象要被销毁了。