面向对象三大特性之封装、多态
一、派生方法实战演练
举例:时间对象序列化报错
当一个字典类型的数据中有datetime产生的时间对象的时候,想要把此类对象通过json模块进行序列化,则会报错。
因为,json模块并不支持所有数据类型的对象都进行序列化,在python中,只有如下对应的数据类型才能被json模块序列化:
python数据类型 | JSON数据类型 |
---|---|
int | int |
float | float |
bool(True,False) | bool(true,false) |
None | null |
str | str(必须双引号) |
list([])、tuple(()) | Array([]) |
dict({}) | Object({})(键必须是双引号) |
import json
import datetime
d = {
't1': datetime.date.today(),
't2': datetime.datetime.today()
}
res = json.dumps(d)
print(res)
--------------打印res的结果会报错-------------
raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type date is not JSON serializable
当我们点开dumps
方法的源码的时候,会看到JSONEncoder
相关json编码的cls类,再点开JSONEncoder
的源码,则会看到python提供的与json格式之间互相转换的数据类型,如图
dumps()
方法中的ensure_ascii=True
参数的含义:这个是因为
json
在进行序列化时,默认使用的是编码是ASCII,而中文为Unicode编码,ASCII中不包含中文,所以出现了乱码。
想要json.dumps()
能正常显示中文,只要加入参数ensure_ascii=False
即可,这样json在序列化的时候,就不会使用默认的ASCII编码。
遇到序列化时报错,可以有两种方式将datetime的时间对象进行序列化:
1 转换方式一:手动转类型
先将时间对象都转换成str字符串格式,由于在python中万物都可转换成str,这样再转换为json格式,就不会报错
d2 = {
't1':str(datetime.date.today()),
't2':str(datetime.datetime.today())
}
# str()方法转换为字符串格式
res = json.dumps(d2) # 再进行序列化转成json格式数据
print(res)
------结果-------
{"t1": "2022-11-07", "t2": "2022-11-07 15:09:59.268513"}
2 转换方式二:派生方法
当我们查看dumps()
源码,注意看cls
参数,默认传JsonEncoder
;查看该类的源码,发现default
方法是报错的发起者。
那么我们可以,编写类继承JsonEncoder
并重写default
方法,之后掉用dumps()
手动传cls=我们自己写的类
,使datetime类型处理成可以被序列化的类型从而成功序列化
class MyJsonEncoder(json.JSONEncoder):
def default(self, o):
"""
:param o: 接收无法被序列化的数据
:return: 返回可以被序列化的数据
"""
if isinstance(o, datetime.datetime): # 判断是否是datetime类型,如果是则处理成可以被序列化的类型:strftime格式化时间字符串
return o.strftime('%Y-%m-%d %X')
elif isinstance(o, datetime.date):
return o.strftime('%Y-%m-%d ')
return super().default(o) # 将json中默认的的default()方法,通过派生方法super()改写成了将datetime产生的时间对象转换成strftime格式化时间字符串,这样就可以成功序列化了
rst = json.dumps(d, cls=MyJsonEncoder)
print(rst)
--------------------
{"t1": "2022-11-07 ", "t2": "2022-11-07 15:19:53"}
这样通过对将json中默认的的default()
方法,通过派生方法super()
改写成了将datetime
产生的时间对象转换成strftime
格式化时间字符串,这样就可以成功序列化了
当我们使用python中一些内置的模块或者函数,会报错的时候,我们可以通过派生方法,再原有基础扩展出新的功能,满足我们的需求。
二、面向对象三大特性之封装
0.封装简介
封装:就是将数据和功能‘封装’起来
封装的两种使用方法:
隐藏:将数据和功能隐藏起来,不让数据直接调用,而开发一些接口间接调用,从而可以在接口内添加额外的操作
伪装:将类里面的方法伪装成类里面的数据
1.隐藏
类在定义阶段,名字前面有两个下划线,那么该名字会被应藏起来,无法直接访问
class MyClass:
name = 'duoduo'
_ = 'hhh'
__age = 18
def choice_name(self): # 方法名前面加__也可以隐藏方法
print('nihao')
当我们使用未隐藏的属性时,正常显示
print(MyClass.name)
-----------------
'duoduo'
当我们去使用隐藏的名字时,会直接报错
print(MyClass.__age)
# AttributeError: type object 'MyClass' has no attribute '__age'
"""
在python中没有真正的隐藏,仅仅是换了个名字而已,换成了_类名__名字,
隐藏真的功能是不让用户直接调用,而去开放接口
"""
print(MyClass.__dict__) # '_MyClass__age'
print(MyClass._MyClass__age) # 18
__
俩下划线隐藏名字的功能,仅可以再类定义阶段使用才有效
MyClass.__hobby = 'dbj'
print(MyClass.__hobby) # dbj无法隐藏,不在定义阶段
ps:在python中没有真正的隐藏,仅仅是换了个名字而已,换成了_类名__名字
,隐藏真的功能是不让用户直接调用,而去开放接口
当我们去调用 __dict__方法,查看类中的名字的时候,可以看到其中被我们隐藏的名字,现在变成了_MyClass__age
print(MyClass.__dict__) # '_MyClass__age'
# 通过这个名字,还是可以得到我们隐藏的属性
print(MyClass._MyClass__age) # 18
案例:隐藏真的功能是不让用户直接调用,而是去开放接口
# 1 定义类的阶段,隐藏一些名字
class Person:
def __init__(self, name, age, hobby):
self.__name = name # __变量名让对象也可以拥有隐藏的属性
self.__age = age
self.__hobby = hobby
def get_info(self):
# 类体代码中,可以直接只用隐藏的名字
print(f"""
姓名:{self.__name}
年龄:{self.__age}
爱好:{self.__hobby}
""")
# 隐藏的属性开放修改的接口,可以自定义很多功能
def set_name(self, new_name):
# 类体代码中,可以直接只用隐藏的名字
if len(new_name) == 0:
raise ValueError('名字太短')
if new_name.isdigit():
raise ValueError('名字是数字')
self.__name = new_name
obj = Person('json', 18, 'read')
obj.get_info()
obj.set_name('13d')
obj.get_info()
以后我在编写面向对象代码 类的定义的时候,也会看到很多单下划线开头的名字,这也是提醒我们,不要直接使用这些名字
2.伪装属性
(1)装饰器@property:将方法伪装成属性
案例:将计算BMI指数的方法,伪装成属性
BMI指数:衡量一个人的体重与身高对健康影响的一个指标
体脂指数(BMI)=体重(kg)÷身高^2(m)
EX:70kg÷(1.75×1.75)=22.86
class Person(object):
def __init__(self, name, height, weight):
self.name = name
self.height = height
self.weight = weight
@property # 利用装饰器property来将方法伪装成数据
def BMI(self):
return self.weight / self.height ** 2
p1 = Person('jason', 1.83, 78)
print(p1.BMI)
装饰器@property可以将一个方法伪装成数据,通过句点符来直接调用该方法的结果
@property 可以将python定义的函数“当做”属性访问,从而提供更加友好访问方式,但是有时候 setter/deleter 也是需要的。
(2)修改伪装的属性:@function.setter
和 删除提醒伪装的属性:@function.deleter
(1)只有 @property
表示 只读 。
(2)同时有 @property
和@*.setter
表示 可读可写 。
(3)同时有 @property
和 @*.setter
和@*.deleter
表示可读可写可删除。
class Foo:
def __init__(self, val):
self.__NAME = val # 将属性隐藏起来
@property
def name(self):
return self.__NAME
# @*.setter 装饰,表示类的属性可修改
@name.setter
def name(self, value):
if not isinstance(value, str): # 在设定值之前进行类型检查
raise TypeError('%s must be str' % value)
self.__NAME = value # 通过类型检查后,将值value存放到真实的位置self.__NAME
# @*.deleter 装饰,表示类的属性可删除,删除后,该类无此属性
@name.deleter
def name(self):
raise PermissionError('Can not delete')
f = Foo('jason')
print(f.name) # jason
f.name = 'jason123'
print(f.name) # jason123
del f.name
# f.name = 'jason' # 触发name.setter装饰器对应的函数name(f,’jason')
# f.name = 123 # 触发name.setter对应的的函数name(f,123),抛出异常TypeError
# del f.name # 触发name.deleter对应的函数name(f),抛出异常PermissionError
三、面向对象三大特性之多态
1.多态的含义
多态:一种事物的多种形态
面向对象中多态的意思是,一种事物可以有多种形态,但是针对相同的功能应该定义相同的方法
python提倡自由简洁大方,不约束程序员的行为,但是多态提供了约束的方法
2.多态的案例
(1) 引入案例
比如用代码模仿动物届,叫声spark对应不同的动物都应该有spark方法,都用来表示叫声,尽管不同动物的叫声不同,也就是print的值不同,但是方法是一致的,可以表示面向对象中多态的含义。
class Animal:
def spark(self):
pass
class Cat(Animal):
# def miao(self):
# print('miaomiao')
def spark(self):
print('miaomiao')
class Dog(Animal):
# def wang(self):
# print('wangwang')
def spark(self):
print('wangwang')
"""
面向对象中多态的意思是,一种事物可以有多种形态,但是针对相同的功能应该定义相同的方法
这样无论我们拿到的是哪个具体的事物,都可以通过相同的方法调用功能
"""
(2)len()
函数中多态的体现
其实我们常用的len()
函数也用到了多态的思想
# len方法也体现了多态性
s1 = 'jsong'
l1 = [11, 33, 334, 4]
d = {'name': 'duoduo', 'pwd': 123}
print(s1.__len__())
print(l1.__len__())
print(d.__len__())
# linux系统
"""
文件 能读取数据也能保存数据
内存 能读取数据也能保存数据
硬盘 能读取数据也能保存数据
。。。。
一切皆文件
"""
综上,我们可以得出了经典的鸭子理论:
鸭子类型:只要你看上去像鸭子,走路像鸭子,说话像鸭子,那么你就是鸭子。
也就是只要事物的种类是一致的,那么他们就应该有相同的属性和方法
(3)Linux
中多态的体现
文件 : 能读取数据也能保存数据
内存 : 能读取数据也能保存数据
硬盘 : 能读取数据也能保存数据
按照鸭子理论得出结论==>>一切皆文件
# 比如 文件 内存 硬盘都应该有读取和写入的功能
class File:
def read(self): pass
def write(self): pass
class Memory:
def read(self): pass
def write(self): pass
class Disk:
def read(self): pass
def write(self): pass
# 所以可以将他们认为是同一种类型的对象
(4)abc模块和metaclass
python永远提倡自由简介大方 不约束程序员行为 但是多态提供了约束的方法
ABC(抽象基类),主要定义了基本类和最基本的抽象方法,可以为子类定义共有的API,不需要具体实现。
abc模块,Python 对于ABC的支持模块,定义了一个特殊的metaclass—— ABCMeta 还有一些装饰器—— @abstractmethod 和 @abstarctproperty 。
abc.ABCMeta 是一个metaclass,用于在Python程序中创建抽象基类。
metaclass是“类的类”,秉承Python“一切皆对象”的理念,Python中的类也是一类对象,metaclass的实例就是类(class),自己写metaclass时需要让其继承自type对象。也就是说metaclass的实例化结果是类,而class实例化的结果是instance。
可以这么理解的: metaclass是创建类的模板,所有的类都是通过他来create的(调用__new__),这使得你可以自由的控制 创建类的那个过程,实现你所需要的功能。
metaclass主要用处:
- 你可以自由的、动态的修改/增加/删除 类的或者实例中的方法或者属性
- 批量的对某些方法使用decorator,而不需要每次都在方法的上面加入@decorator_func
- 当引入第三方库的时候,如果该库某些类需要patch的时候可以用metaclass
- 可以用于序列化(参见yaml这个库的实现,我没怎么仔细看)
- 提供接口注册,接口格式检查等
- 自动委托(auto delegate)
import abc
# 指定metaclass属性将类设置为抽象类,抽象类本身只是用来约束子类的,不能被实例化
class Animal(metaclass=abc.ABCMeta):
@abc.abstractmethod # 该装饰器限制子类必须定义有一个名为talk的方法
def talk(self): # 抽象方法中无需实现具体的功能
pass
class Cat(Animal): # 但凡继承Animal的子类都必须遵循Animal规定的标准
def talk(self):
pass
cat = Cat() # 若子类中没有一个名为talk的方法则会抛出异常TypeError,无法实例化