day24 内置方法,异常机制

day24 内置方法,异常机制

文章目录

  • day24 内置方法,异常机制
    • 今日内容
    • 昨日回顾
    • 今日内容详细
      • 内置方法(魔法方法)
        • `__new__(cls[, ...])`创建对象
        • `__init__(self[, ...])`构造器
        • `__del__(self)`析构器
        • `__len__(self)`获取长度
        • `__hash(self)__`消息摘要算法
        • `__str__(self)`打印方法
        • `__eq__(self, ogj)`比较
      • 异常处理
        • 常见异常
        • 处理异常
        • 多重捕获(多分支)
        • `except ... as ...`
        • 万能异常
        • `try...except...else...`组合
        • `try...except...finally...`组合
        • 主动发出异常
        • 断言
        • 自定义异常

今日内容

  1. 内置方法(魔法方法)
  2. 异常处理

昨日回顾

  1. 约束
    1. 在父类建立一种约束
    2. 抽象类
      • from abc import ABCMeta, abstractmethod
      • abc:abstract base class,抽象基类
      • ABCMeta:抽象类的元类,用来构造类
      • abstractmethod:装饰器,用来将普通的实例方法转换为抽象方法
      • 如果子类中没有重写被abstractmethod装饰过的方法,会报错
  2. 类方法
    1. 用classmethod修饰
    2. 第一个参数为类对象,一般用cls表示
    3. 实例对象和类对象都可以调用类方法,但一般用类对象调用
    4. 类方法是酱类本身作为对象进行操作的方法
  3. 静态方法
    1. 使用staticmethod修饰的方法
    2. 没有class和cls参数,不能调用类或实例的属性和方法
    3. 类和实例方法都可以调用静态方法
    4. 静态方法是独立的单纯的函数,仅仅托管于类的名称空间中
  4. property
    1. property是一种特殊的属性,访问它时会执行一段功能,然后返回值
    2. property是装饰器,装时后的方法使用起来与属性类似,不需要加括号
    3. 只有@property是只读,加上@setter定义可读可写,加上@deleter定义可读可写可删除
    4. 属性一般有三种访问方式:获取、修改、删除
    5. property(获取方法,修改方法,删除方法)
  5. 反射(自省)
    1. 通过字符串的形式操作对象中的属性
    2. 四个实现字形的函数:hasattr,getattr,setattr,delattr
    3. 反射可以应用于对象、类和模块

今日内容详细

内置方法(魔法方法)

魔法方法是Python的对象天生拥有的一些神奇的方法,它们总被一对双下划线所包围。

这些方法会在特殊情况被Python所调用,可以通过重写这些方法定义自己想要的行为。而且这一切都是自动发生的。

不过除非你知道自己在干什么,明确自己这样做的目的,否则不建议修改这些方法。因为这些方法本身已经写好了一些规则,如果随意修改可能会引发一些麻烦。

__new__(cls[, ...])创建对象

__new__()方法是一个对象实例化的时候调用的第一个方法,用来创建一个类对象。__new__()方法中的参数是类而非对象,因为调用它的时候,对象还没有被创建出来。

__new__()方法的返回值需要是一个类对象。事实上,返回值可以为任意,但是将无法实例化对象。

其基本用法如下:

class Person:
    def __new__(cls, *args, **kwargs):
        print('我是用来创建对象的方法')
        return object.__new__(cls)
xiaoming = Person()

输出结果为:
我是用来创建对象的方法

还是要强调一次,除非你知道自己在做什么并且对自己的行为非常自信,否则不要轻易重写__new__()方法。

__init__(self[, ...])构造器

__init__()方法是对象实例化的时候,继__new__()方法之后第二个被调用的方法,被称作初始化方法、构造方法或构造器。它的作用是当一个实例被创建时进行初始化,可以传入参数,设置实例属性等。

其用法如下:

class Person:
    def __init__(self):
        self.name = '小明'
        print('用来初始化对象,被称为构造方法')
xiaoming = Person()
print(xiaoming.name)

输出的结果为:
用来初始化对象,被称为构造方法
小明

__del__(self)析构器

__del__()方法会在实例被销毁的时候被调用执行,也被称作析构方法或析构器。

这个方法不建议被修改。有的时候如果修改了__del__()方法可能会和Python中的内存回收机制冲突,从而产生属性删除不干净的情况。

其基本用法为:

class Person:
    def __del__(self):
        print('析构器,也叫析构方法,在对象被删除时,自动调用')
xiaoming = Person()
print('我没有删除对象呀~')

输出的结果为:
我没有删除对象呀~
析构器,也叫析构方法,在对象被删除时,自动调用

我们并没有使用del方法删除实例对象,但是__del__()似乎也被执行了。这是因为当程序运行结束时,内存回收机制会自动删除对象,这时,__del__()方法中的代码也会自动执行

如果我们手动删除对象,__del__()方法就会在我们删除的时候自动执行了:

class Person:
    def __del__(self):
        print('析构器,也叫析构方法,在对象被删除时,自动调用')
xiaoming = Person()
del xiaoming
print('这次我把对象删除啦~')

输出的结果为:
析构器,也叫析构方法,在对象被删除时,自动调用
这次我把对象删除啦~

虽然我们没有在__del__()写入任何与删除对象有关的代码,Python解释器仍然会把对象删除掉。如果再次调用对象时,会因找不到对象而报错。程序结束时也不会再次运行__del__()中的代码,因为对象已经被删除。

__len__(self)获取长度

当使用len(obj)函数调用的时候会自动执行__len__(self)方法中的代码。其返回值必须是int类型,且不能为负:

class Person:
    def __len__(self):
        return 666
xiaoming = Person()
print(len(xiaoming))

输出的结果为:
666

__hash(self)__消息摘要算法

__hash__()方法通过hash(obj)方法调用。hash是一种消息摘要算法,用于获取一个对象(非可变数据类型)的哈希值。对于相同的对象来说,哈希值是一样的,不同的对象哈希值则不同。哈希值可以用来判断对象是否有被篡改。

同样,不建议修改__hash__()方法

__str__(self)打印方法

__str__()方法会在使用print打印对象的时候被调用。我们可以自定义打印出来的内容:

class A:
    pass
class B:
    def __str__(self):
        return '这时B对象'
a = A()
b = B()
print(a)
print(b)

输出的结果为:
<__main__.A object at 0x0000026D9B52B6D8>
这时B对象

__eq__(self, ogj)比较

当我们使用==进行比较运算时,会调用==左边的对象的__eq__()方法,==右边的对象会作为参数传入。两个对象比较之后将结果返回:

class A:
    def __init__(self):
        self.name = 'xiaoming'
        self.age = 10
    def __eq__(self, obj):
        if self.name == obj.name and self.age == obj.age:
            return True
        else:
            return False
class B:
    def __init__(self):
        self.name = 'xiaoming'
        self.age = 10
a = A()
b = B()
print(a == b)

返回的结果为:
True

在上面的例子中,B类并没有写__eq__方法,因为只会调用等号左边对象的__eq__方法,等号右边的对象没有也无所谓。

异常处理

异常(Exception)是一个事件,该事件可能会在程序执行过程中发生,影响程序正常执行。

通常情况下,出现异常的原因有两种:

  1. raise语句抛出的异常
  2. Python解释器自己检测到异常并引发

在Python无法正常处理程序时,就会发生异常。

异常时Python对象,表示一个错误,一般继承自类Exception

异常和错误是有区别的。异常是我们可以预测,可以通过一些操作来避免的麻烦。而错误往往是无法预料和避免的问题。

当Python程序发生异常时,我们需要对它即使捕获和处理,否则程序会终止运行。

用户没有程序异常的概念,只有程序能用和不能用。在用户界面,不能有任何能造成程序终止的异常出现。

常见异常

  • SyntaxError 语法错误
  • IndentationError 缩进错误(空格数目不正确)
  • NameError 使用一个尚为被赋予对象的遍历
  • TypeError 传入对象类型与要求的不符合(int + str)
  • IOError 输入/输出异常,基本上是无法打开文件(FileNotFoundError 也属于 IOError
  • ImportError 无法引入模块或包,基本上是路径问题或名称错误
  • IndexError 下标索引超出序列边界
  • KeyError 试图访问字典里不存在的键

处理异常

程序员编写特定的代码,专门用来捕捉这个异常(这段代码与程序逻辑无关,只是为了处理异常)。如果捕捉成功,则会进入另外一个处理异常的分支,执行为指定异常定制的逻辑。这样即便出现了异常,程序也不会崩溃。

其实我们从前已经进行过异常处理的操作了。当时我们使用if语句来实现的:

num = input('>>>')    # 让用户输入
if num.isdecimal():    # 这是我们想要的结果,其他的条件都是在进行异常处理
    print(int(num) + 10)
elif num.isspace():
    print('如果输入的是空格,就执行这里的代码')
elif len(num) == 0:
    print('如果输入内容为空,就执行这里的代码')
else:
    print('其他所有情况,都会执行这里的代码')

这确实实现了我们预期的功能,但是却也存在很多问题:

  1. 使用if的方式,我们职位第一段代码加上了异常处理,但这些if,跟代码逻辑毫无关联,会严重降低代码的可读性
  2. 这只是我们代码中的一个小逻辑。如果类似地逻辑还有很多,每一次都要判断这些内容,会让我们的代码特别冗长
  3. if判断的异常处理方法只能针对某一段代码,对于不同的代码段的相同类型的错误,需要写重复的if来进行处理

这就需要使用Python中自带的异常处理方式,其基本结构为:

try:
    被检测的代码块
except 异常类型:
    被检测的代码块出现异常后,会执行这里的代码

如果你不想子啊异常发生时结束你的程序,只需在try里捕获它

其基本流程为:

  • 首先,执行try子句(在关键字try和except之间的部分)
  • 如果没有异常发生,except子句将不会被执行
  • 如果在try自居执行的过程中出现了异常,那么改子句中出错部分之后的代码将不会被执行
  • 如果出现的异常与except关键字后面指定的异常类型相同,就会执行对应的except子句,然后继续执行后续代码
  • 如果出现的异常与except关键字后面指定的代码不同,将会抛出错误,程序终止

例如,我们可以这样处理除数不能为零的错误:

try:
    print(1/0)
# 注意:如果写明异常类型,异常类型要与上面发生的异常匹配,否则捕捉不到
except ZeroDivisionError:
    print('0不能作为除数')
print('后续语句')

输出的结果为:
0不能作为除数
后续语句

多重捕获(多分支)

except可以指定捕获的类型,捕获多种异常。

具体的操作方式是,使用多个except。需要注意的是,每个except后面仍然只能有匹配一个异常。

如果没有任何一个except能够捕获到异常,异常将会向外抛出,使程序终止。

我们可以这样处理多异常:

try:
    print(1/0)
    raise FileNotFoundError
except ZeroDivisionError:
    print('0不能作为除数')
except FileNotFoundError:
    print('异常2')
print('后续语句')

输出结果为:
0不能作为除数
后续语句

如果try的子句中有多处错误,只会引发第一个错误的异常,且子句中发生异常后面的代码将不会被执行。

except ... as ...

使用except...as...语句可以查看异常,以及错误是否按要求被捕获到:

try:
    print(1/0)
    raise FileNotFoundError
except Exception as e:    # 万能异常,稍后会有讨论
    print(e)
    
输出的结果为:
division by zero

万能异常

如果想要的效果是,无论出现什么异常,我们都要捕获,统一处理,我们就可以使用一个Exception来代指任意错误(事实上,Exception还不能代指Python中所有异常,不过对于我们来说,还写不出Exception指代不了的异常)。

万能异常往往可以和多分枝结合使用,这样就可以对不同的一场定制不同的处理逻辑。同时,也不会有任何错误被忽略而导致程序终止:

# 多分枝 + 万能异常
# 发生的异常中,有一些异常是需要不同的逻辑处理的,剩下的同意处理掉即可
dic = {
     '1': 'a', '2': 'b', '3': 'c'}
try:
    choice = int(input('请输入序号:'))
    print(dic[choice])
except ValueError:
    print('请输入数字...')
except KeyError:
    print('你输入的选项超出范围')
except Exception as e:
    print(f'遇到不明错误,错误原因:{e}')

try...except...else...组合

try代码中,只要出现了异常,就不会执行else语句;如果不出现异常,则会执行else中的语句。例如:

try:
    a = 1
    b = int(input('请输入数字:'))
    print(a/b)
except ZeroDivisionError:
    print('除数不能为0')
except ValueError:
    print('只能输入数字!')
else:
    print('程序运行正常,没有出错!')

当输入的数字为0时,输出的内容为:
请输入数字:0
除数不能为0

当输入的数字为2时,输出的内容为:
请输入数字:2
0.5
程序运行正常,没有出错!

try...except...finally...组合

不管try中的语句是否发生错误,也不管出现的错误是否被except捕获到,finally中的语句一定会被执行。

如果try中没有发生错误,程序顺利执行,如果有else先执行else中的子句,然后执行finally中的子句。

如果出现了错误,而且成功被捕获到,先执行except子句中的代码,然后执行finally中的代码。

如果错误没有被捕获到,在报错之前,会执行finally中的代码。

其示例如下:

a = input('请输入数字:')
try:
    print(1/int(a))
except ValueError:
    print('错误成功被捕获!')
finally:
    print('无论如何都会打印的内容~')
    
用户输入1,程序正常运行,输出的结果为:
请输入数字:1
1.0
无论如何都会打印的内容~

用户输入a,程序异常被成功捕获,不会报错,输出的结果为:
请输入数字:a
错误成功被捕获!
无论如何都会打印的内容~

用户输入0,程序异常不能被捕获,程序报错,报错前finally中的代码仍然被执行,输出的结果为:
请输入数字:0
无论如何都会打印的内容~
Traceback (most recent call last):
  File "C:/Users/Sure/PyProject/week07/day24/exercise.py", line 122, in <module>
    print(1/int(a))
ZeroDivisionError: division by zero

finally的应用场景之一是,当我们进行文件操作时,使用finally关闭文件,确保文件操作内容被保存到硬盘中:

f = open('test', 'a', encoding='utf-8')
try:
    """各种操作"""
    print(f.read())
    """期间发生了错误,如果不关闭文件,文件中的数据将因为未保存而丢失"""
finally:
    f.close()

在函数中,finally会在return之前被执行:

def func():
    try:
        return 1
    finally:
        print('finally')
print(func())

输出的结果为:
finally
1

在循环中,finally还会在break之前被执行:

while True:
    try:
        print('还没有结束~')
        break
    finally:
        print('finally')
        
输出的结果为:
还没有结束~
finally

总结起来就是,finally用来做一些收尾的工作。在一些重要操作环节之前,为避免出错而造成重要数据的损失,有必要做一些比如关闭链接之类的处理。这时候,就可以用finally作为最后一道防线来收尾。

主动发出异常

有的时候,我们需要自己在代码中触发一些异常。

主动抛出异常的语法结构为:

raise 错误类型('错误描述')

我们在类的约束中,已经用到了这个方法。我们要求子类重写父类的方法,如果子类没有重写,则会报错:

class Girl:
    def play(self):
        raise Exception('子类必须重写play方法')
class Nurse(Girl):
    pass
xiaoli = Nurse()
xiaoli.play()

程序运行报错,报错信息为:
Traceback (most recent call last):
  File "C:/Users/Sure/PyProject/week07/day24/exercise.py", line 148, in <module>
    xiaoli.play()
  File "C:/Users/Sure/PyProject/week07/day24/exercise.py", line 144, in play
    raise Exception('子类必须重写play方法')
Exception: 子类必须重写play方法

断言

断言也是一种主动抛出异常的语句。

断言表示一种强硬态度,只要assert后面的代码不成立,直接报错,下面的代码将不会被执行。

断言的基本结构为:

assert 条件

例如:

name = 'xiaoming'
print(1)
print(1)
assert name == 'lalala'
print(1)
print(1)
print(1)

当输出两个1之后,程序报错,输出的内容为:
1
1
Traceback (most recent call last):
  File "C:/Users/Sure/PyProject/week07/day24/exercise.py", line 154, in <module>
    assert name == 'lalala'
AssertionError

自定义异常

我们之前提到过,Python中提供的错误类型可能并不能解决所有错误。只不过以目前来说,我们或许还不能发现那些异常。

如果以后在工作中,出现了某种无法用一直错误异常捕获到的异常(万能异常只能捕获Python中存在的异常),那么就可以尝试自定义异常。新定义的异常也需要继承自Exception类:

class HelloError(Exception):
    def __init__(self, n):
        self.n = n
try:
    n = input('请输入数字:')
    if not n.isdecimal():
        raise HelloError(n)
except HelloError as hi:
    print('HelloError: 请输入数字。\n你输入的是:%s' % hi.n)
else:
    print('未发生异常')
    
输入的值为a,输出的内容为:
请输入数字:a
HelloError: 请输入数字。
你输入的是:a

你可能感兴趣的:(Python全栈开发26期笔记,Python,全栈开发)