面向对象三大特性之封装、多态

一、派生方法实战演练

举例:时间对象序列化报错

​ 当一个字典类型的数据中有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格式之间互相转换的数据类型,如图

image-20221107150155294 image-20221107150435425

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主要用处

  1. 你可以自由的、动态的修改/增加/删除 类的或者实例中的方法或者属性
  2. 批量的对某些方法使用decorator,而不需要每次都在方法的上面加入@decorator_func
  3. 当引入第三方库的时候,如果该库某些类需要patch的时候可以用metaclass
  4. 可以用于序列化(参见yaml这个库的实现,我没怎么仔细看)
  5. 提供接口注册,接口格式检查等
  6. 自动委托(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,无法实例化
posted @   Duosg  阅读(122)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示