(一)、什么是魔术方法?
在python中像__init__这类双下划线开头和结尾的方法,我们把它称为魔术方法。
注意点: 魔术方法都是python内部定义的,自己不要去定义__init__这类双下划线开头的方法。
(二)、例子
①、__new__
由打印结果我们可以看出来,我们只创建了一个对象,没有手动去调用这个__init__方法。由此可以说明,__init__方法在类去创建对象的时候可以自动去调用。
疑惑1:那么问题来了:是先创建对象然后再去调用__init__方法,还是先调用__init__方法再去创建对象?
答:答案很明显,肯定是先创建对象然后再去调用__init__方法,因为__init__魔术方法第一个参数是self,说明啥?说明这个init魔术方法是实例方法,self接收的是什么?就是实例对象。你只有先有了对象,你才能在初始化方法里面对对象进行初始化。
class MyClass(object):
def __init__(self):
"""初始化方法"""
print('--init--方法')
if __name__ == '__main__':
MyClass()
# 打印结果
--init--方法
了解清楚之后,我们看下MyClass这个类的父类是什么?是不是一个叫object这个父类(object可以省略不写),因为我们所有的类都是集成这个object这个父类,那么object这个父类里面有什么呢?里面就有一个__new__这个方法,我们ctrl+鼠标左键object看下
我们看下他的注释:创建并且返回一个新的对象。
那如果说我们自己定义一个__new__方法呢?我们执行看下打印结果,发现new方法调用了,但是init魔术方法没有调用。
class MyClass(object):
def __init__(self):
print('--init--方法')
def __new__(cls, *args, **kwargs):
print("--new方法--")
if __name__ == '__main__':
m = MyClass()
print(m)
# 打印结果
--new方法--
None
疑惑2:那么为什么init魔术方法没有调用呢?刚才不是说创建对象会自动调用init魔术方法的吗?
我们看打印结果对象m打印结果是None,没有创建出来对象。为什么没有创建出来对象?因为我们的new魔术方法里面没有返回出来对象。我们刚才看的源代码显示new方法是创建一个对象并且返回,所以找到问题了(必须要在__new__方法中实现类创建对象并且返回该对象的功能),我们改下代码试试,我们在我们创建的new魔术方法中取调用父类object里面的__new__魔术方法并且把他返回出去试试。我们看下修改后的代码:
class MyClass(object):
def __init__(self):
print('--init--方法')
def __new__(cls, *args, **kwargs):
print("--new方法--")
return super().__new__(cls)
if __name__ == '__main__':
m = MyClass()
print(m)
# 打印结果
--new方法--
--init--方法
<__main__.MyClass object at 0x0000024D12748C70>
从上面打印结果看出来了,先调用new方法,然后创建一个对象并且返回之后,再去调用__init__方法去初始化对象。
疑惑3(经典实际运用场景之单例模式):__new__魔术方法在什么运用场景下才会去重写?
如果要控制类里面对象创建的过程,可以通过自定义new方法去实现,比方说:单例模式。
需求:类每次实例化的时候都会创建一个新的对象,如果要求类只能被实例化一次该怎么做呢?
我们看下代码怎么实现:
先设置一个类的属性obj,默认值为None,然后判断如果obj为空,我们就去调用父类__new__方法去生成对象,然后重新赋值给类的属性obj并且return返回出去,如果obj不是None,说明他已经被赋值过了,不是第一次调用创建对象了,那我们就直接返回obj第一次创建对象。我们看打印结果两次创建出来的对象的内存地址指向同一个地方,说明他是同一个对象。
class Demo:
obj = None
def __new__(cls, *args, **kwargs):
if cls.obj is None:
instance = super().__new__(cls)
cls.obj = instance
return cls.obj
else:
return cls.obj
if __name__ == '__main__':
d1 = Demo()
print(d1)
d2 = Demo()
print(d2)
# 打印结果
<__main__.Demo object at 0x000002C0D7F68C70>
<__main__.Demo object at 0x000002C0D7F68C70>
这里还可以改进一下,这个单例模式不是很安全,为什么?因为我们看下obj这个类属性我们可以修改他的值。修改为None之后它第二次调用去创建对象的时候,又会重新生成一个对象。
class Demo:
obj = None
def __new__(cls, *args, **kwargs):
if cls.obj is None:
instance = super().__new__(cls)
cls.obj = instance
return cls.obj
else:
return cls.obj
if __name__ == '__main__':
d1 = Demo()
print(d1)
Demo.obj = None
d2 = Demo()
print(d2)
# 打印结果
<__main__.Demo object at 0x000001A648B58C70>
<__main__.Demo object at 0x000001A648B58700>
那这个时候我们怎么去处理呢?我们可以把这个obj这个类属性设置为私有属性。 提示同事这个是私有属性,不要去改。但是如果有人非要去改也是可以改的 当然了不会有人去做,除非你在公司跟同事结仇拉。
class Demo:
__obj = None
def __new__(cls, *args, **kwargs):
if cls.__obj is None:
instance = super().__new__(cls)
cls.__obj = instance
return cls.__obj
if __name__ == '__main__':
d1 = Demo()
print(d1)
Demo.obj = None
d2 = Demo()
print(d2)
# 打印结果
<__main__.Demo object at 0x000002BD76978C70>
<__main__.Demo object at 0x000002BD76978C70>
疑惑4:那么,又有童鞋问了,这个单例模式实际运用场合又是什么叻?
import os
import logging
from logging.handlers import TimedRotatingFileHandler, BaseRotatingHandler
import colorama
colorama.init()
class Logger:
__instance = None
sh = logging.StreamHandler()
def __new__(cls, path=None, level='DEBUG'):
"""
:param path: report path
:param args:
:param kwargs:
:return:
"""
if not cls.__instance:
cls.__instance = super().__new__(cls)
log = logging.getLogger('apin-ui')
log.setLevel(level)
cls.__instance.log = log
return cls.__instance
def set_level(self, level):
"""设置日志输出的等级"""
self.log.setLevel(level)
def set_file_handle(self, level, path):
if path:
if not os.path.isdir(path):
os.mkdir(path)
fh = TimedRotatingFileHandler(os.path.join(path, 'apin-ui.log'), when='d',
interval=1, backupCount=7,
encoding="utf-8")
fh.setLevel(level)
self.log.addHandler(fh)
# 定义handler的输出格式
formatter = logging.Formatter("%(asctime)s | 【%(levelname)s】 | : %(message)s")
fh.setFormatter(formatter)
def debug(self, message):
self.fontColor('\033[0;34m{}\033[0;34m{}\033[0;34m{}')
self.log.debug(message)
def info(self, message):
self.fontColor('\033[0;32m{}\033[0;32m{}\033[0;32m{}')
self.log.info(message)
def warning(self, message):
self.fontColor('\033[0;33m{}\033[0;43m{}\033[0;33m{}')
self.log.warning(message)
def error(self, message):
self.fontColor('\033[0;31m{}\033[0;41m{}\033[0;31m{}')
self.log.error(message)
def exception(self, message):
self.fontColor('\033[0;31m{}\033[0;41m{}\033[0;31m{}')
self.log.exception(message)
def critical(self, message):
self.fontColor('\033[0;35m{}\033[0;45m{}\033[0;35m{}')
self.log.critical(message)
def fontColor(self, color):
# 不同的日志输出不同的颜色
formatter = logging.Formatter(color.format("%(asctime)s| ", "【%(levelname)s】", " | : %(message)s"))
self.sh.setFormatter(formatter)
self.log.addHandler(self.sh)
def print_info(msg):
print('\033[0;32m{}'.format(msg))
def print_waring(msg):
print('\033[0;33m{}'.format(msg))
def print_error(msg):
print('\033[0;31m{}'.format(msg))
if __name__ == '__main__':
logger = Logger()
logger.debug('debu等级日志')
logger.info('info日志')
logger.warning('warning日志')
logger.error('error日志')
logger.critical('CRITICAL日志')
打印结果:
②、上下文协议
问题思考:with打开文件为何会自动关闭?
上下文管理器的概念:上下文管理器是一个python对象,为操作提供了额外的上下文信息。这种额外的信息,在使用with语句初始化上下文,以及完成with块中的所有代码时,才用可调用的形式。
- object.__enter__(self)
输入与此对象相关的运行时上下文。如果存在的话,with语句将绑定该方法的返回值到该语句的as字句中指定的目标 - object.__exit(self,exc_type,exc_val,exc_tb)
exc_type: # 异常类型
exc_val: # 异常值
exc_tb: # 异常回溯追踪
退出与此对象相关的运行时上下文。参数描述导致上下文退出的异常。如果该上下文退出时没有异常,三个参数都将为None。如果提供了一个异常,并且该方法希望抑制该异常(即防止他被传播),它应该返回一个真值。否则,在退出此方法后,异常将被正常处理。注意__exit__()方法不应该重新抛出传递进去的异常,这是调用者的责任。
上下文管理器实现:如果在一个类中实现了__enter__,__exit__这2个方法,那么这个类就可以当做一个上下文管理器来用。
我们看下open的源代码,大概意思就是打开一个文件并且返回一个对象给你。所以当我们执行with 的时候,with后面的对象会先去执行 __enter__,然后执行完这个方法之后,把这个方法的返回值赋值给as后面的变量f。当with里面的代码全部执行完毕之后,他会执行__exit__这个魔术方法,这个exit魔术方法已经实现了关闭文件这个操作,所以通过with操作文件的时候,不需要手动去调用关闭文件这个操作。
with open("logger.py", 'r', encoding='utf-8') as f:
print(f)
下面我们手动实现一个文件操作的上下文
class OpenFile(object):
'''手动实现文件操作的上下文'''
def __init__(self, filename, method):
# 初始化打开文件
self.file = open(filename, method)
def __enter__(self):
# 启动上下文时,将打开的对象返回出去
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
# 退出上下文时,将文件关闭
self.file.close()
print()
with OpenFile("python.txt", 'r') as f:
f.write('hello')
我们打个断点调试一下(print()这一行目的就是为了方便调试的,实际代码中这一行可以去掉),看看代码的运行流程是啥。
- with后面跟的是一个对象 OpenFile("python.txt", 'r'),所以第一步先初始化对象,代码执行__init__魔法方法,里面代码做了打开文件,并将文件对象赋值给了file。
- 因为实现了上下文协议,所以当打开文件的时候,代码执行到__enter__这个魔术方法,并且把打开的对象返回出去。(with代码执行之前先走enter魔术方法)
- 然后执行 f.write('hello') ,最后再执行__exit__这个魔术方法,关闭文件。(with代码执行完毕之后然后跑enter魔术方法)
注意:我们这里在with代码里故意写错了让他报错,看看代码执行结果。
我们通过debug调试发现了,即使with代码里报错了,他还是会去执行exit魔术方法(只要不是报语法错误)。
我们打个断点调试一下exit这个魔术方法里传的三个参数是啥。
-
exc_type: NameError with中的代码如果执行出错,则会将异常类型传递给exc_type
-
exc_val: name is not defined with中的代码如果执行出错,则会将异常信息传递给exc_val
-
exc_tb: 异常的溯源 with中的代码如果执行出错,则会将异常溯源对象传递给exc_tb
既然出错了,那么我们可以在exit魔术方法中干什么?当然是记录日志。
③、__call__
作用:__call__是用来实现类的对象 调用的行为
实际运用场景:通过类去实现装饰器
疑惑1:python中万物皆对象,函数也是对象,为什么函数可以调用,而其他的对象不行?
我们先上一个demo给大家看一下:
class Demo:
pass
def func():
print('-----func-----')
if __name__ == '__main__':
c = Demo()
print(callable(func))
print(callable(c))
# 打印结果
True
False
由此看出来,func这个函数是可调用的,但是这个c是不能调用的,为什么呢?
答:是因为实现了可调用协议,就是魔术方法__call__
那我们把代码改一下,在Demo这个类里面实现__call__这个魔术方法试试看。很明显,这个类所创建的对象可以调用了。
class Demo:
def __call__(self, *args, **kwargs):
print('---demo---call')
def func():
print('-----func-----')
if __name__ == '__main__':
c = Demo()
print(callable(func))
print(callable(c))
c()
# 打印结果
True
True
---demo---call
疑惑2:如果想让类创建出来的对象,可以像函数一样被调用可以实现吗?
下面我们上demo:
因为Demo实例化对象不需要传参数,但是你给他传了一个参数。
那么我们就给他这个类定义一个init方法来接收我们这个func参数,
- 我们这个时候打印出来这个init里面传的参数func是什么?很明显,是func函数。这个应该能理解。
- 最外面print(func)打印出来的是什么?是不是Demo这个类的对象?很明显,是的。
这个时候我们在外面调用func(),还能不能正常去调用?很明显不能了(报错Demo这个类的对象不能被调用),为什么?因为这个类里面没有实现__call__魔术方法。
那么问题来了,如果你想你的函数被类装饰器装饰之后,你还想你的函数func能够正常被调用,那我们该怎么办?答:实现__call__方法
我们该怎么做呢?上代码。
- 既然我们init魔术方法中传进去的func是外面这个func函数,那我们先把他保存为属性。
- 然后手写一个__call__方法,外部执行func()的时候,上述已经讲了这个func实际上就是Demo这个类创建出来的对象,我们现在已经实现了__call__方法,所以外部调用func()的时候实际上代码跑的是类里面这个__call__方法,那call魔术方法中执行的是什么呢?执行的是原功能函数(因为此时类里面func指向的是原功能函数名)
下面上代码方便大家复制,自己实际操作一下
class Demo:
def __init__(self, func):
self.func = func
print('init方法中接收到的func', func)
def __call__(self, *args, **kwargs):
print("---装饰器扩展的代码---start")
self.func()
print("---装饰器扩展的代码---end")
# 通过Demo这个类去实例化一个对象,然后返回给func这个函数
# func实际上是类的一个对象
@Demo # func = Demo(func)
def func():
print('-------func-------')
print(func)
func()
# 打印结果
init方法中接收到的func <function func at 0x0000024CA583DAF0>
<__main__.Demo object at 0x0000024CA5AB8280>
-------func-------
(三)实战题目练习
"""
1、通过装饰器实现单例模式,只要任意一个类使用该装饰器装饰,
那么就会变成一个单例模式的类。(面试真题)
2、请实现一个类,前五次创建对象,每次都可以返回一个新的对象,
第六次开始,每次创建,都随机返回前5个对象中的一个
3、通过类实现一个通用装饰器,既可以装饰函数,又可以装饰器类,不管函数和类需不需要传参都可以装饰
"""
# 第一题:
# 方式1:函数实现装饰器
import random
#
# def single(cls):
# def wrapper(*args, **kwargs):
# if not hasattr(cls, '__instance'):
# cls.__instance = cls(*args, **kwargs)
# return cls.__instance
#
# return wrapper
class single:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
if not hasattr(self.cls, '__instance'):
self.cls.__instance = self.cls(*args, **kwargs)
return self.cls.__instance
@single # Musen = single(Musen)
class Musen:
pass
# 第二题:
class MyTest(object):
__obj_list = []
def __new__(cls, *args, **kwargs):
# 判断列表中对象的个数是否少于5
if len(cls.__obj_list) < 5:
obj = super().__new__(cls)
cls.__obj_list.append(obj)
return obj
else:
# 大于5则随机返回一个
return random.choice(cls.__obj_list)
# 第三题:
class Decorator(object):
def __init__(self, item):
self.item = item
def __call__(self, *args, **kwargs):
res = self.item(*args, **kwargs)
return res
def deco2(item):
def wrapper(*args, **kwargs):
res = item(*args, **kwargs)
return res
return wrapper
if __name__ == '__main__':
m1 = MyTest()
m2 = MyTest()
m3 = MyTest()
m4 = MyTest()
m5 = MyTest()
m6 = MyTest()
print(m1, m2, m3, m4, m5, m6, sep='\n')