6th Python面向对象进阶
(这部分可能会感觉突然难度上来了,大家慢慢琢磨,懂了就简单了,摸索的过程就是提升)
self,那我们就可以理解这个方法和对象绑定在一块,只能调用对象的东西。
6.1 类方法 classmethod
类方法通过@classmethod装饰器实现,类方法和普通方法区别是,类方法只能访问类变量,不能访问实例变量。可以说@classmethod就是让这个方法和类绑定在一块,只能调用类里面的东西。

上面的代码就会报错AttributeError: type object 'Dog' has no attribute 'name',找不到self.name,也就是访问不了__init__()实例化传进的参数。
我们打印一下类方法传入的self:

输出的是
<class '__main__.Dog'> # 类本身
<__main__.Dog object at 0x0DA79D00> # 实例对象 后面的这个16进制的地址会各有不同
<class '__main__.Dog'> # 类本身
可以看出,类方法的self返回的是类本身,而不是像普通方法那样,self返回的是实例对象。
这个类方法应用场景不多,但我们也需要知道一下,下面就设计了一个场景:
现在要实现 每生成一个学生,num就要+1的一个需求。

我们发现类变量根本就没有变,还是0,这是为什么呢?因为self.stu_num+=1是对实例的变量进行赋值,而不是类本身的变量,这个是需要注意的一个点,很多初学者可能会犯这样的错误。

这样还是有一点问题的,因为你现在在外面直接Student.stu_num就可以修改了,这样安全性就很垃圾了。。。所以我们可以用类方法来实现封装。

哎,等等,你试试在外面调用add_stu()这个类方法,不是还能在外面改么?对,确实还能改,但是和直接修改类变量不同的是,我现在可以判断是否真的生成了实例(或是其他条件来约束)。

这样就会实现简单的防止直接修改,你必须传入带有name属性的实例对象进来才可以在外面修改类变量。有人说这样的话,我随便创建一个带name的实例不就好了?
但你要想想,我们这个只是简单的防止外部修改,如果真正的开发过程中,我的if判断再改的复杂一些,那就不好创建实例对象从外部改了。
6.2 静态方法 staticmethod
和类方法类似,是用@staticmethod装饰器来实现的,不能访问类变量、也不能访问实例变量,这里的两种变量包括变量和方法。可以说,类和实例的所有东西静态方法都访问不了,既不绑定类又不绑定对象,和类外面的方法一样和类与对象没什么关系。
我们知道,类里面的普通方法调用会默认传实例对象:s.fly() -> s.fly(s),而静态方法就是去掉了这个步骤,所以fly(self)方法的self没有任何东西传给他,除非你这样调用s.fly(s),也就是自己手动传进self。
可以说,静态方法就是被类孤立出来的方法,他从类里面什么都得不到,和类外面的方法没区别。
使用场景的话,基本没有,可能在某些特殊场景中会用,我们知道即可。
6.3 属性方法 property
把一个方法变成一个静态的属性(变量),也是用装饰器@property来实现的。

我们可以看到,如果fly()就报错,fly才是真的调用fly()方法。这样我们就实现了调用方法名就可以调用方法。
比如我们在去哪儿官网查询机票,他们查机票的步骤有三步:
连接各机场的航班系统;
查询信息;
对返回的信息进行处理,解析,显示给用户。
用户当然只看飞机的状态和价格,这些都是一些静态的信息,但是分析的过程都是根据各机场航班系统动态数据解析的,那我们模拟写一段代码展现这一过程:

用户看到的就是flight_status这一静态变量,但是每次调用这一变量时,程序都动态进行了上面三步。
问题又来了,既然他是个变量,那我们可以给它赋值么?实际是不行的,如果赋值,Python解释器会报错的。那真的想改怎么办?那我们就要再加一个属性方法,格式如下:

那我现在想删了这个属性方法,还是一样的方法,增加一个方法:

6.4 反射
这个很重要。主要指程序可以访问、检测和修改本身状态或行为的一种能力(自省),简单来说,就是可以通过字符串的形式来操作(增删改查)对象的属性。

代码中的hasattr()就是反射的四个方法之一,是检测属性是否存在的。还有其他三种方法,分别是:getattr()、setattr()、delattr(),分别对应了获取、设置(赋值)、删除。接下来,我们就来试试这些方法:

通过上面的代码可以看出,我们可以操作的东西包括属性和方法。
那我们上面讲的是实现在程序自身寻找属性,那在大型程序中,代码是分很多文件存放的,现在我们需要在一个py文件中寻找属性,怎么办?getattr(filename, 属性)肯定是不行的。说到这里,我们不得不提一个知识点。
我们输出__name__,我们可以看到返回的是__main__,__main__可以代表模块本身(就是该py文件),可以类似理解为self可以代表对象;但它又不完全是self,因为self可以通过.来调用对象中的属性和方法,而__mian__不可以。
那我们再往下说,我们直接看下面两段代码:

我们可以得出,code1和code2输出都是一样的,那区别是什么呢?
其实,code1会在别的模块(别的py文件)调用自身模块(自身所在的py文件)时发挥作用。如果自己执行代码,那if外面和if里面的全部执行;如果外部导入执行,就只执行if外部的代码,内部的不执行。

我们运行test2.py,可以看到,输出的是test,所以我们得出,__name__在当前模块主动执行的情况下(即不是被导入的情况下),等于__main__;在被其他模块导入执行的情况下,就是模块名了。
在sys模块中的modules属性(数据类型为字典)中,就有键为__main__的键值对,我们上面说了__mian__的作用不能完全等同于self的功能是因为不能通过.运算来调用,那这里键为__main__的键值对所对应的值就能完全等同于self的功能。

像这些东西现在我们写程序不怎么用,但是学到后面像Django那些框架,里面就是不断用反射,实现不同模块的调用。
动态加载模块
我们知道,程序运行时,类就会自动的全部加载。那我们就基于反射机制,来实现动态加载模块的功能,让用户输入什么模块,我们再加载该模块,而不是刚运行程序就全部加载了。

这里需要注意的是,挎包导入模块时,要确保当前工作路径下有你要导入的包。比如
反射应用场景的再举例
比如我们有个网站,用户点进去那个网站就加载那个网站,我们没学反射之前写出来的代码可能是这样的:

这样很不方便,如果有100个界面,那我们就要写100个if分支。
那我们学过反射之后,就可以进行修改:

这样用户输入什么就加载相应的模块,当模块增多时,就不用修改代码,增加分支判断了。
6.6 类的双下划线方法
需要大概了解的方法
len方法

len方法必须返回值为整数,我们前面通过len()来看列表或字典等数据的长度就是重写定义列表和字典等数据类的__len__()方法来实现的。
除非我们以后需要实现一个类,这个类要靠自己实现len方法的话,我们才会用到,这种情况遇到的不多,所以我们知道就可以了。
hash方法
hash方法是每个类都会自带的,功能就是我们前面说到的hash的基本功能。

当然,我们也可以重写,hash方法规定返回值也必须是integer类型的数据。

eq方法
==就会触发eq方法,eq必须带一个参数other,就是==右侧的传入对象。

通过eq方法,我们就可以自定义等于的评判标准。
item系列
这里不得不说一下每个对象自带的__dict__属性,它里面存放了对象的所有属性,以键值对(key: 属性名, value: 属性值)存储在__dict__这个字典中。

可以把一个对象变成dict,可以像dict一样,增删改查。

可以从代码中看出,都是对__dict__属性的操作,其中需要注意的就是__setitem__(self, key, value)和__delitem__(self, key)与__delattr__(self, item)的区别。
__setitem__(self, key, value)如果key不存在,那么它的功能就相当于新建(增);若key存在,那么它的功能就是修改了(改)。
__delitem__(self, key)与__delattr__(self, item)的区别:前者是通过del p['website']触发,后者是del p.website触发,只有这个区别,其他都是一样的。
另外还有一个__getitem__(),与之相似的__getattr__()是在获取一个没有的属性时候触发,可以用来提示失败操作的。
重点需要会的方法
上面除了item系列的方法之外基本不怎么用,item也就是偶尔用用。接下来说的一些双下线的方法就是我们需要重点掌握的方法了。
str方法 & repr方法
这两个方法功能比较像,均可以改变对象的字符串显示格式。
我们可以先看看如果我们不重写这两个下线方法,我们输出的是什么:

那我们重写这两个方法,再来看看输出:

那肯定会问了,这两个方法有什么区别?
str函数或者obj.__str__();
repr函数或者交互式解释器(比如Windows的cmd)中调用时触发obj.__repr__();如果触发
__str__()的时候,__str__()没有被定义,那么就会触发__repr__()来代替输出;注意:这俩方法的返回值必须是字符串,否则抛出异常。
PS:str比repr常用
del 析构方法
析构方法就是,当对象在内存中被释放时,自动触发执行。

但是这里我们要清楚一点,释放内存资源是Python解释器的事,我们程序员不能修改,所以__del__()里面不用实现怎么释放内存或者阻止释放资源,你只能写一些释放资源时做一些其他事情,比如print('该资源已释放'),告诉我们已经释放。
当然,如果我们什么都不写,还是会被释放掉。

我们会看到,程序结束执行之后,会有一个对象被释放了的输出,这个是因为程序运行结束的时候,Python解释器会将该程序中所有资源都会被释放,所以会有这个输出。
new方法
我们知道实例化对象的时候,init会自动执行。但是实际上在init方法之前,还会执行一个new方法(也是自动执行的),你可以在new里执行一些实例化前的定制动作。(new方法可以进行一些实例初始化前的工作)

运行这段代码,我们会发现,init方法不执行了。。只执行了重写的new方法。
这样的原因是,new方法不光在init之前执行,还负责执行init,所以我们重写的new方法必须返回一个父类new方法,使init方法开始执行。

new实现单例模式
new方法最多的应用就是单例模式了,那我们先来说说什么是单例模式。
程序员开发网站会有一个设计模式,开发app也会有一个设计模式,那所有的设计模式共有23种,那单例模式也是其中之一。
单例模式就是确保这个类只能实例化一次。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
应用的场景也有很多,比如你打开电脑的设置窗口,无论你点击多少次,他就只会打开一个窗口。
我们模拟一下打印机的过程:

可以看到,我们创建了三个对象(内存地址各不相同),这样虽然实现了效果,但是很浪费资源,我们现在来把它修改成单例模式。

这里我们输出三个对象的内存地址,可以发现,他们的内存地址都是一样的,说明我们已经实现了单例模式。可以说,表面上看起来实例化了p1,p2,p3三个对象
值得一提的是,我们可以输出一下p1和p2的name属性,会发现他们的name都变成了excel exe,原因就是init在每次创建实例的时候都执行了,所以实例化的单例对象的name属性就只保留最新的name值了,所以才是excel exe。
call方法
我们要讲的最后一个双下线方法了,call方法会在对象后面加()时触发。这里得要注意一下,构造方法new的执行是由创建对象触发的,即:对象 = 类名();而对于call方法的执行是由对象后加括号触发的,即:对象() or 类()()。不能混淆了。

call方法一般很少用,python有个web框架用到过,所以就提一下。
python的双下线方法还有很多,我们不一定都要去搞明白,有的都是Python解释器用的,知道这些基本就够用了。
6.7 type 动态创建类
我们先来看看下面这段代码:

运行之后我们发现,type(p)显示类型为Person,这个我们理解,因为p是从Person中实例化出来的对象;但是type(Person)显示的类型为type,按照我们刚才理解type(p)的思路来理解,type应该就是Person的父类,我们创建的所有类都是从type中继承出来的。
type()不光可以查询类型,还可以动态创建一个类:

那么这个只是简单的类,复杂的类我们也可以通过type()动态创建:

动态创建类很有用,在Django中也经常使用。要加什么参数可以自由的支配,不用像前面静态创建类那样用一些方法去实现对类的操作。
6.8 类的判别
这里我们主要说两个方法:
isinstance()
这个方法我们前面也说过,但是前面只是说了用来判断数据类型,比如:

这里我们还可以用这个方法来判断obj是否是一个类的对象:

需要注意的是,如果A类创建的对象与A类的父类在一块isinstance(),返回的也是True。

issubclass()
上面的方法是检测对象和类的,这里这个方法呢,就是来检测类与类之间的父类关系的,对于issubclass(sub, super),如果sub是super类的子类,就会返回True;反之则返回False。

通过上面的例子可以看出,参数的顺序不能反,子类要在前,父类要在后。
当然,这两个方法不怎么重要,知道就好,关键是下面要说的异常处理。
6.9 异常处理
常规异常处理
我们前面写各种代码的时候,不免会报错,但是这些错误是给程序员看的,如果给用户看的话,用户会看不懂的。所以就需要我们将代码中可能发生的错误捕捉(监测),格式如下:

像我们断网时打开网页,输入网址进入后,网页会显示找不到服务器,并且提示你可以怎么做来恢复正常。这就是我们异常处理需要实现的目的。
比如我们写一个两个数相加的程序,然后让程序可以在报错时不报错,而是输出我们写的异常信息。

那我们对用户友好一点就可以改成下面这样的代码:

这样我们就可以让程序出错了之后可以继续运行,并且告诉用户出了什么错,这样就可以提高程序的容错性,还能让用户的程序体验提高。
异常类型
刚刚代码中的Exception是万能异常,无论出什么类型的错误都会显示一个异常处理的情况,所以万能异常是我们不推荐的。
我们异常处理考虑的越细越全面就会使程序的使用性更高。
那我们就需要了解一些常见的异常类型:
AttributeError:试图访问一个对象没有的属性,比如foo.x,但是对象foo中没有属性x;IOError:输入/输出异常;基本上是无法打开文件或者文件操作无权限等等;ImportError:无法引入模块或包;基本上是路径问题或名称错误;IndentationError:语法错误(的子类) 或代码没有正确对齐(代码是否对齐是捕捉不到的,因为Python解释器初次编译的时候就会报错,不会进入到捕捉异常的步骤);IndexError:下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5];KeyError:试图访问字典里不存在的键;KeyboardInterrupt:Ctrl+C被按下;NameError:使用一个还未被赋予对象的变量;SyntaxError:Python代码非法,语法错误(也捕捉不到,因为Python解释器初次编译的时候就会报错,不会进入到捕捉异常的步骤);TypeError:传入对象类型与要求的不符合;UnboundLocalError:试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,导致你以为正在访问它;ValueError:传入一个调用者不期望的值,即使值的类型是正确的。
Python也提供一次捕获多个异常:

我们将万能异常可以放在程序的最后,这样可以确保程序出错也不会停止运行,然后我们慢慢找bug修改。(万能异常捕获不到自定义异常)
那么其实try的语法还有一个else语句和finally语句:

当然,还有其他很多的异常类型,我们就不一一说了,我们只要知道怎样捕捉异常就可以了。
主动触发异常
我们利用raise来主动触发异常:raise ValueError(什么类型的异常自己设定),一般和自定义异常一起用。
自定义异常
Python中的所有异常都是一个对象,他们都继承一个父类——BaseException。

6.10 assert断言
assert语法用于判断代码是否符合执行预期

一般用于接口测试,我们写完一个大项目,需要一个一个检测所有接口是否正常工作,这时候我们就写一个脚本用assert检测接口调用结果是否和预期结果数据相同。
assert在别人调用你的接口时,还可以检测传递的数据是否是指定的关键参数,比如下面代码所演示的:

6.11 自检
Question
静态方法、类方法、属性方法的区别,通过代码来体现。
如何通过反射往一个类里添加一个方法?
请通过代码实现反射判断当前模块里是否有一个Dog类?
写一个小脚本,不断接收用户指令,根据指令动态导入指定模块。
自己手动写出一个单例模式,不要看文中给出的代码,写完再看。
写一个可以像字典一样操作的实例。
用type动态创建一个Person类, 包含构造函数、walk\talk方法。

