day21-多态和多态性,鸭子类型,反射,内置方法,异常处理

温故而知新

昨天我们详细的介绍了一下继承和组合.我们知道了继承一般是用来抽取is-a的类,就是什么是什么的关系.而组合一般是用来抽取has-a ,即什么用什么的关系.这样才符合我们人类世界的逻辑.使我们编写程序时思路清晰一点.

今天我们建立在继承的基础上继续研究类还有什么其他的用法和特性

多态

多态是什么?

我们拆开分析一下,多指的是多个,多种,那态指的就是形态,状态.

合在一起指的就是多种形态.

继承指的又是多种事物按照is-a的方式抽象出来的同一种类型.

多种事物代表着多个子类,而那个抽象出来的类代表一个父类

我们用现实中的例子来看! 水 有液态水,气态水,还有固态水. 那水就是它们这些不同形态水的一个抽象大类.

即多态在我们python中指的就是同一类事物的多种形态.

来个具体的案例介绍

案例代码

class Animal: #同一类事物:动物
    def talk(self):
        pass
class Cat(Animal): #动物的形态之一:猫
    def talk(self):
        print('喵喵喵')
class Dog(Animal): #动物的形态之二:狗
    def talk(self):
        print('汪汪汪')
class Pig(Animal): #动物的形态之三:猪
    def talk(self):
        print('哼哼哼')

#实例化得到三个对象
cat=Cat()
dog=Dog()
pig=Pig()

那么多态给我们带来了哪些好处呢?

多态性

概念: 多态性指的是可以在不用考虑对象具体类型的情况下而直接使用对象,这就需要在设计时,把对象的使用方法统一成一种.

接着上面的案例:cat、dog、pig都是动物,但凡是动物肯定有talk方法,于是我们可以不用考虑它们三者的具体是什么类型的动物,而直接使用

代码为

cat.talk()  # 喵喵喵

dog.talk()  # 汪汪汪

pig.talk()	# 哼哼哼

更进一步,将其封装成一个函数

def Talk(animal):
    animal.talk()

调用这个功能就变成了

Talk(cat)  # 喵喵喵
Talk(dog)  # 汪汪汪
Talk(pig)  # 哼哼哼

现在我们使用这个方法的时候,我们只需要记住 这个 Talk这个方法怎么使用就可以了.而不是要去记每个不同的动物的叫的方法的使用,这就是多态性的好处.

就好比我们现实世界中考驾照一样,我们学的时候只需要学会怎么开车就好了,而之后不管你是开宝马还是奥迪还是什么.你只要学会了开这个功能.你开什么车都是用的你开车的方法.(只是个大概例子,不要钻牛角尖)

Python中一切皆对象,本身就支持多态性

比如我们之前所使用的len这个函数的时候,我们只需要将数据类型往里面传就好了,而不会管你是什么数据类型的.当然这里的数据类型包含str,list,tuple,set,dict

代码为:

# 我们可以在不考虑三者类型的情况下直接使用统计三个对象的长度
s.__len__()
l.__len__()
t.__len__()

# Python内置了一个统一的接口
len(s)
len(l)
len(t)

多态性的好处在于增强了程序的灵活性可扩展性,比如通过继承Animal类创建了一个新的类,实例化得到的对象obj,可以使用相同的方式使用obj.talk()

添加一个狼这个动物,并也为其加上叫这个功能

class Wolf(Animal): #动物的另外一种形态:狼
    def talk(self):
    print('嗷...')

那么现在我们去调用它的叫的方法

wolf = Wolf()
Talk(wolf)  # 还是用的动物那个类的叫的用法

综上我们得知,多态性的本质在于不同的类中定义有相同的方法名,这样我们就可以不考虑类而统一用一种方式去使用对象.

但是我们没有硬性的要求我们的其他动物类的子类都必须有该方法.那么当不同的类的叫的方法不一样,而你现在还是直接调用父类的方法,那么就变成了执行了父类的那个空的功能了

class Animal:

    def talk(self):
        pass

class Dog(Animal):

    def say(self):  # 现在这个狗叫的方法变为了 say
        print('汪汪汪')


class Cat(Animal):

    def tell(self):  # 现在这个猫叫的方法变为了 tell
        print('喵喵喵')


class Pig(Animal):

    def jiao(self):  # 现在这个猪叫的方法变为了 jiao
        print('吭哧吭哧')

现在我们在使用上面的animal的叫的方法,就是直接运行了一个空的功能,就达不到我们的要求了.

cat=Cat()
dog=Dog()
pig=Pig()
Talk(cat)  # 空的
Talk(dog)  # 空的
Talk(pig)  # 空的

所以我们python种提供了一种硬性要求我们的子类必须要有父类定制的方法.而且方法的名称必须一样

abc模块

那就是我们的abc模块

import abc

# 指定metaclass属性将类设置为抽象类,抽象类本身只是用来约束子类的,不能被实例化
class Animal(metaclass=abc.ABCMeta):  # 这个括号内的要求必须是
    @abc.abstractmethod # 该装饰器限制子类必须定义有一个名为talk的方法
    def talk(self): # 抽象方法中无需实现具体的功能
        pass

class Cat(Animal): # 但凡继承Animal的子类都必须遵循Animal规定的标准
    def talk(self):  # 所以此时这个子类必须得有一个功能叫做talk
        pass

cat=Cat() # 若子类中没有一个名为talk的方法则会抛出异常TypeError,无法实例化

但其实我们完全可以不依赖于继承,只需要制造出外观和行为相同对象,同样可以实现不考虑对象类型而使用对象.

鸭子类型

这正是Python崇尚的“鸭子类型”(duck typing):“如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子”。比起继承的方式,鸭子类型在某种程度上实现了程序的松耦合度,如下

class Cat: # 动物的形态之一:猫
    def talk(self):
        print('喵喵喵')
class Dog: # 动物的形态之二:狗
    def talk(self):
        print('汪汪汪')
class Pig: # 动物的形态之三:猪
    def talk(self):
        print('哼哼哼')

我们只需要在每个动物类下面都有一个方法叫,并且规定它们的名字都叫talk,那么现在这些类我就可以看成都是鸭子类型.这只是一种抽象的概念,并不是说都是鸭子.只是一种规定.是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

鸭子类型和多态是俩种编程思想.

多态的好处是可以通过abc这个模块,来强制要求子类必须要有父类被abc装饰的这个方法,没有就报错,这体现了一种硬性的要求,表面不相信人,而相信程序.缺点就是强耦合,多造了一个父类

而鸭子类型更倾向于相信人,相信你会遵循它的要求,将都设置这个方法,并且都叫一个名.缺点就是,他没有硬性要求你设计的时候鸭子类一定要有那个方法,而且那个方法一定要叫那个名字.

没有哪种思想好,哪种思想不好,看个人需求.

接下来要将的就是类的反射和内置方法

反射

python是动态语言,而反射(reflection)机制被视为动态语言的关键。

反射机制指的是在程序的运行状态中

对于任意一个类,都可以知道这个类的所有属性和方法;

对于任意一个对象,都能够调用他的任意方法和属性。

这种动态获取程序信息以及动态调用对象功能称为反射机制。

在python中实现反射非常简单,在程序运行过程中,如果我们获取一个不知道存有何种属性的对象,若想操作其内部属性,可以先通过内置方法__dir__来获取任意一个类或者对象的属性列表,列表中全为字符串格式

class People:
    country = 'china'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def tell_info(self):
        print(f"{self.name}
{self.age}")


p1 = People('jkey', 18)
# __dict__ 来查看这个对象,或者这个类的方法和属性,即数据和功能
print(p1.__dict__)  # {'name': 'jkey', 'age': 18}

接下来就是想办法通过字符串来操作对象的属性了.

我们有了这个字典,那么我们就可以用这个字典来对这个类和方法来进行一些增删改查的方法.(和前面介绍类的实例化一样)

这里就来个简单的判断,name这个属性在不在这个类或者对象当中

if 'name' in People.__dict__ or 'name' in p1.__dict__:
    return True
else:
    reruen False

这样写就有点不尊重python对类的封装程度了.

于是python就给我们提供了内置函数hasattr、getattr、setattr、delattr的使用了(Python中一切皆对象,类和对象都可以被这四个函数操作,用法一样)

用上方的人们这个类来举例子

class People:
    country = 'china'

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def tell_info(self):
        print(f"{self.name}
{self.age}")


p1 = People('jkey', 18)
print(p1.__dict__)  # {'name': 'jkey', 'age': 18}

hasattr(obj,str) 表示的就是判断一个字符串在不在这个对象(或者类)中,即判断这个对象(或者类)中有没有这个属性

  • obj:表示的是对象,或者类
  • str:表示的是一个字符串
print(hasattr(p1, 'country'))  # True

getattr(obj, key) 根据key这个属性名称去对象或者类中的取值,返回值为 str在对象中的__dict__对应的值

print(getattr(p1, 'country'))  # 等同于 p1.country 返回 china

setattr(obj, key, new_value) 根据key这个属性名称去对象或者类中的取值 并将这个值修改为 new_value

print(getattr(p1, 'name'))  # jkey
setattr(p1, 'name', 'liu')  # 等同于 p1.name = "liu"
print(getattr(p1, 'name'))  # liu

delattr(obj, key) 删除对象或者类中的属性名key

# delattr(p1, 'country')  # 等同于 del p1.country
# print(p1.country)  # 删除之后再访问就会报错了

类中和对象一样使用

print(hasattr(People, 'tell_info'))  # True
res = getattr(People, 'tell_info')  # liu
res(p1)  # 18

基于反射可以十分灵活地操作对象的属性,比如将用户交互的结果反射到具体的功能执行

案例:模拟一个ftp的上传下载功能(用反射)

class FtpServer:

    @staticmethod
    def get(file):
        print('Downloading %s 中...' % file)

    @staticmethod
    def put(file):
        print('Uploading %s 中...' % file)

    def run(self):
        while True:
            i = input('cmd>>>:').strip()
            try:
                cmd, file = i.split(' ')
            except ValueError as e:
                print('输入不合法! 格式应为:get/put file_name')
                continue
            if hasattr(self, cmd):  # 根据用户输入的cmd,判断对象self有无对应的方法属性
                func = getattr(self, cmd)  # 根据字符串cmd,获取对象self对应的方法属性
                func(file)


Ftp = FtpServer()
Ftp.run()

为什么用反射之反射的好处

好处一:实现可插拔机制

可以事先定义好接口,接口只有在被完成后才会真正执行,这实现了即插即用,这其实是一种‘后期绑定’,什么意思?即你可以事先把主要的逻辑写好(只定义接口),然后后期再去实现接口的功能

反射到这就讲完啦,一般用于和用户交互,一个蛮强大的功能

你可能会发现上面的案例中用到了 try 和 except这个我们今天最后讲.那么接下来久讲我们的内置方法

一 isinstance(obj,cls)和issubclass(sub,super)

isinstance(obj,cls)检查是否obj是否是类 cls 的对象

class Foo:
    pass


obj = Foo()

print(isinstance(obj, Foo))  # True
print(isinstance([1, 2, 3], list))  # True

issubclass(sub, super)检查sub类是否是 super 类的派生类

print(issubclass(Foo, object))  # True

凡是在类内部定义,"__名字__"的方法都称之为魔法方法,又称类的内置方法

这些方法都不是给对象或者类去调用的,一般是满足某种条件时,自动触发.

__init__:初始化函数,在类调用时自动触发

__str__:打印对象的时候自动触发

__del__:删除对象的名称空间,最后才执行

__getattr__:对象.属性获取属性,没有获取到值自动触发

__setattr__:对象.属性赋值的时候自动触发

__call__:在对象被调用时自动触发

__new__:在__init__前自动触发,产生一个空的对象

列举__str____del__方法案例

class People:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        """
        为类内的内置方法,当打印用该类实例化的对象时.触发函数体代码
        必须要有 返回指
        :return: 返回的必须是一个字符串的类型
        """
        return f"名字是:{self.name}
年龄为:{self.age}"


p1 = People('jkey', 18)
print(p1)  # 当打印该 People类实例化的时候,默认触发 People类的__str__方法,将__str__方法的返回值给print这个方法


class Del:

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.f = open('00今日内容', 'rt', encoding='utf-8')

    def __del__(self):
        self.f.close()
        print('已将文件关闭')


del_obj = Del('abc', 16)

# del del_obj  # 手动删除对象, 触发Del类里面的__del__方法
# print('======>')  # 当是手动删除时,这行代码会在__del__方法执行后执行


print('======>')  # 当没有手动删除时,python解释器最后也会将这个对象回收,此时会先打印这个===>再执行__del__内的代码

最后讲下我们的异常处理

一 什么是异常

异常是程序发生错误的信号。程序一旦出现错误,便会产生一个异常,若程序中没有处理它,就会抛出该异常,程序的运行也随之终止。在Python中,错误触发的异常如下

而错误分成两种,一种是语法上的错误SyntaxErro

如:print( 少了一个右括号,

这种语法错误了,它不会执行代码,你要知道python运行一个文件时会先检测语法,语法有问题,就不会执行文件代码体了

print(11111)
print(22222)
print(3333

前面的打印11111和22222也不会执行,会在检测语法时就报错.这种错误应该在程序运行前就修改正确

另一类就是逻辑错误,常见的逻辑错误如

# TypeError:数字类型无法与字符串类型相加
1+’2’

# ValueError:当字符串包含有非数字的值时,无法转成int类型
num=input(">>: ") #输入hello
int(num)

# NameError:引用了一个不存在的名字x
x

# IndexError:索引超出列表的限制
l=['egon','aa']
l[3]

# KeyError:引用了一个不存在的key
dic={'name':'egon'}
dic['age']

# AttributeError:引用的属性不存在
class Foo:
    pass
Foo.x

# ZeroDivisionError:除数不能为0
1/0

二 异常处理

为了保证程序的容错性与可靠性,即在遇到错误时有相应的处理机制不会任由程序崩溃掉,我们需要对异常进行处理,处理的基本形式为

try:
    被检测的代码块
except 异常类型:
    检测到异常,就执行这个位置的逻辑

三.如何处理

语法错误-》程序运行前就必须改正确
    逻辑错误
        针对可以控制的逻辑错误,应该直接在代码层面解决
        针对不可以控制的逻辑错误,应该采用try...except...

四、try...execpet一种异常产生之后的补救措施

下面这个段代码为一个完整版的异常处理

print('start...')
try:
    被监测的代码块1
    被监测的代码块2
    被监测的代码块3
    被监测的代码块4
    被监测的代码块5
except 异常的类型1 as e:
    处理异常的代码
except 异常的类型2 as e:
    处理异常的代码
except 异常的类型3 as e:
    处理异常的代码
except (异常的类型4,异常的类型5,异常的类型6) as e:
    处理异常的代码
except Exception:
    处理异常的代码
else:
    没有发生异常时要执行的代码
finally:
    无论异常与否,都会执行该代码,通常用来进行回收资源的操作
print('end...')

我们之前处理逻辑异常是通过if 判断 来避免的

比如:之前的猜年龄

age = input(">>>: ").strip()
if age.isdigit():  # 如果不加这层if判断 当用户输入的是一个非数字的字符串就会抛异常,终止程序
    age = int(age)

    if age > 19:
        print('too big')
    elif age < 19:
        print('too small')
    else:
        print('you got it')
else:
    print('必须输入数字')

模拟一下异常发生

案例1: IndexError 超出索引范围异常

try:
	print(111)
    print(222)
    l=[11,22,33]
    l[100]  # 此时 我的 取值已经超过了 l 这个的最大索引了
    print(3333)
except IndexError as e:  # 匹配的 异常 应该为 IndexError 匹配成功了会执行其里面的代码体
    print(e)

print('end...')

案例2: 当except没匹配上时,还是会抛异常 终止程序的运行

print('start...')

try:
    print(111)
    print(222)
    l=[11,22,33]
    l[100]
    print(3333)
except KeyError as e:  # 拿着 IndexError异常 去匹配 KeyError 发现没匹配上 就接着往下匹配了
    print(e)
# 下面没有匹配机制了,就直接将异常抛出来了.

print('end...')

案例3: KeyError 字典的key引发的异常

print('start...')

try:
    print(111)
    print(222)
    l=[11,22,33]
    l[0]
    dic = {'k1':111}
    # dic['kkkkk']
    print(3333)
except Exception as e:
    print(e)
# 自己想一下 下面的 else 和 finally 内的代码会不会执行
else:
    print('else的代码')
finally:
    print('====>finally的代码')

print('end...')

常见的一些异常类型

AttributeError 试图访问一个对象没有的树形,比如foo.x,但是foo没有属性x
IOError 输入/输出异常;基本上是无法打开文件
ImportError 无法引入模块或包;基本上是路径问题或名称错误
IndentationError 语法错误(的子类) ;代码没有正确对齐
IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]
KeyError 试图访问字典里不存在的键
KeyboardInterrupt Ctrl+C被按下
NameError 使用一个还未被赋予对象的变量
SyntaxError Python代码非法,代码不能编译(个人认为这是语法错误,写错了)
TypeError 传入对象类型与要求的不符合
UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另有一个同名的全局变量,
导致你以为正在访问它
ValueError 传入一个调用者不期望的值,即使值的类型是正确的

先介绍一下 raise自定义异常

在不符合Python解释器的语法或逻辑规则时,是由Python解释器主动触发的各种类型的异常,而对于违反程序员自定制的各类规则,则需要由程序员自己来明确地触发异常,这就用到了raise语句,raise后必须是一个异常的类或者是异常的实例

class Student:
    def __init__(self,name,age):
        if not isinstance(name,str):
            raise TypeError('name must be str')
        if not isinstance(age,int):
            raise TypeError('age must be int')

        self.name=name
        self.age=age

stu1=Student(4573,18) # TypeError: name must be str
stu2=Student('jkey','18') # TypeError: age must be int

在内置异常不够用的情况下,我们可以通过继承内置的异常类来自定义异常类

Python还提供了一个断言语句assert expression,断定表达式expression成立,否则触发异常AssertionError

l = [111,222]
if len(l) != 3:
    raise Exception('必须达到三个值')
assert len(l) == 3  # 我断言这个l列表的长度为3 如果不是就抛异常AssertionError,是就当这行代码不存在
print('后续代码。。。')

python文件在执行的时候python解释器翻译的过程中就会将不用的代码过滤调,像是注释啊,这些断言成功的语句啊.再去执行.

如果错误发生的条件“不可预知”,即异常一定会触发,那么我们才应该使用try...except语句来处理。例如我们编写一个下载网页内容的功能,网络发生延迟之类的异常是很正常的事,而我们根本无法预知在满足什么条件的情况下才会出现延迟,因而只能用异常处理机制了

def get(url):
    try:
        response = requests.get(url, timeout=3)  # 超过3秒未下载成功则触发ConnectTimeout异常
        res = response.text
    except ConnectTimeout:
        print('连接请求超时')
        res = None
    except Exception:
        print('网络出现其他异常')
        res = None
    return res


print(get('https://www.cnblogs.com/jkeykey/'))

总结

1.多态表示的是一个类的多种不同的形态,即一个父类有多个子类,这个子类就是父类的不同形态的表现

1.2.多态性,只需要记住父类的使用方法,就可以使用不同子类的相同方法.

2.鸭子类型,一个事物只要长的像鸭子,走路像鸭子,那么这就是鸭子

3.反射

  • 3.1:hasattr(obj,str) 判断一个对象内有没有该属性
  • 3.2:getattr(obj,key) 获取到一个对象内的key属性的值
  • 3.3:setattr(obj,key,new_value) 给 这个对象的key这个属性 赋值
  • 3.4:delattr(obj,key) 删除这个对象的key属性

4.内置方法

  • 4.1isinstance(obj,cls) 判断一个obj是不是通过那个cls的实例的
  • 4.2 issubclass(obj,type) 判断 obj类是不是继承type类
  • 4.3 __str__:在打印对象时,自动触发代码体执行
  • 4.4__del__:在删除对象时,自动触发代码体执行,一般用来回收系统资源

5.异常处理

  • 5.1异常处理分为:语法异常和逻辑异常

    • 语法异常一般在程序执行前就应该解决
    • 在程序执行前就应该解决
    • 逻辑异常在重新中运行时不可避免
  • 5.2异常处理是对数据进行补救的行为,应该少用.

  • 5.3:完整语法

    try:
        检测的代码1
        检测的代码2
        检测的代码3
        检测的代码4
    except 错误类型1 as e:
        出错后对代码进行处理1
    except 错误类型2 as e:
        出错后对代码进行处理1
    except Exception as e:  # 万能异常匹配
        出错后对代码进行处理2
    else:
        当检测的语法没有报错时执行
    finally:
        无论检测的语法有没有异常都会执行
    
    
原文地址:https://www.cnblogs.com/jkeykey/p/14273560.html