实现对属性编辑操作的拦截
在上回中,我们谈到在Python中利用基于类的描述器(descriptor)和函数修饰器(decorator),可以实现针对对象属性访问的拦截操作。那么我们很容易想到,既然可以拦截对属性的访问操作,那么也就一定能够实现针对属性的编辑操作的拦截。
下面我先直接上具体的代码:
class my_property:
def __init__(self, *args, **kwargs):
pass
def __get__(self, obj, cls):
return self.func(obj)
def setter(self, fset):
self.fset = fset
return self
def __set__(self, obj, value):
if not self.fset:
raise AttributeError("can't set attribute")
return self.fset(obj, value)
def __call__(self, func, *args, **kwargs):
self.func = func
return self
class SysOptions:
def __init__(self):
self.cache = dict()
@my_property()
def website_base_url(self):
if 'website_base_url' in self.cache:
return self.cache['website_base_url']
else:
return None
@website_base_url.setter
def website_base_url(self, value):
self.cache['website_base_url'] = value
return value
mySys = SysOptions()
# 输出None
print(mySys.website_base_url)
# 输出"http://www.jiangnangame.com"
mySys.website_base_url = 'http://www.jiangnangame.com'
print(mySys.website_base_url)
这个代码对于认真看了上篇文章的同学来说难度应该不大,因此这里我仅作简单的分析。
为了实现对属性编辑操作的拦截,我这里在描述器类my_property中定义了setter
和__set__
这两个方法。
-
setter
负责接收实际实现属性编辑操作的函数fset
并将其保存到描述器对象中,此外还将可供调用的my_property实例对象再次返回回来,替换掉我们要实现拦截功能的类中原先的方法,实现函数的柯里化。 -
__set__
作为描述器对象的方法,显然用于拦截针对描述器所属类的属性的编辑操作,并触发之前保存下来的fset
函数进行编辑操作的处理。
这里需要说明的是,有的同学看到我在类SysOptions中似乎定义了两个名为website_base_url
的方法,可能会感到疑惑:你这定义了两次,难道不会发生某些冲突吗?
首先必须澄清的是,就Python自身的语言特性而言,在class中重复定义属性或者方法,后者会自动覆盖前者,并不会引发任何报错,如下例所示:
class MyClass:
name = 'Trump'
name = 'Biden'
def foo(self):
print('hello world');
def foo(self):
print('Hello World');
obj = MyClass()
# 输出Biden
print(obj.name)
# 输出Hello World
obj.foo()
于是乎,当代码解析到用@website_base_url.setter
装饰的第二个website_base_url
方法时,实际上是会覆盖掉之前的定义的。但是没有任何关系,因为之前定义的website_base_url
经过装饰器的包装已经变成了一个可供调用的my_property
对象,而其setter
方法在接收处理属性值编辑操作的函数后,仍然会将这个对象再次返回回来,因此最终挂在MyClass类上的website_base_url
属性就是我们需要的这个作为描述器的my_property
对象,这种写法是不会出任何问题的。
一个小改进
在前面的例子中,在修饰website_base_url
方法时我们用的是依赖魔法方法__call__实现的@my_property()
。虽然在前面的例子中没有体现,但聪明的同学或许已经猜到了,刻意设计这么一种写法是为了便于在实例化my_property
对象时可以传入某些自定义参数,例如@my_property(year = 2024)
。但有时候我们只想直接使用默认设置,并不想传参,这时候这对括号就显得多余了。
在下面这个编造的例子中,我进行了一些改进:
class my_property:
def __init__(self, func = None, *arg, **kwargs):
self.func = func
self.year = 2023
if 'year' in kwargs:
self.year = kwargs['year']
def __get__(self, obj, cls):
print('year = ', self.year)
return self.func(obj)
# 省略set相关代码...
def __call__(self, func, *args, **kwargs):
if self.func is None:
self.func = func
return self
class SysOptions:
def __init__(self):
self.cache = dict()
@my_property
def website_base_url(self):
return 'http://www.jiangnangame.com'
@my_property(year = 2024)
def website_name(self):
return 'JiangNanGame'
mySys = SysOptions()
# 输出:
# year = 2023
# http://www.jiangnangame.com
print(mySys.website_base_url)
# 输出:
# year = 2024
# JiangNanGame
print(mySys.website_name)
在本例中,@my_property
和@my_property(year = 2024)
的主要区别在于描述器对象挂载fset的时机不同。
对于前者而言,在Python解释器碰到@符号需要调用装饰器函数构造新函数(在本例中就是实例化my_property
对象)的时候,我们要修饰的函数对应形参func
传入描述器类的构造函数__init__
并完成了挂载。
对于后者而言,首先my_property(year = 2024)
主动实例化并返回了我们需要的my_property
对象,同时将自定义的year参数传入__init__
;此时描述器对象func
由于没有传入任何对应的值,仍为None。等到Python解释器碰到@符号的时候,描述器对象的魔法方法__call__
被触发,此时被修饰的函数才被传入该魔法方法并完成挂载。
Python原生实现的property修饰器
原本这部分内容我写到这里应该已经结束了,但我现在才发现Python已经原生实现了property修饰器的功能:
class SysOptions:
def __init__(self):
self.cache = dict()
@property
def website_base_url(self):
if 'website_base_url' in self.cache:
return self.cache['website_base_url']
else:
return None
@website_base_url.setter
def website_base_url(self, value):
self.cache['website_base_url'] = value
return value
mySys = SysOptions()
# 输出<class 'property'>
print(property)
# 输出None
print(mySys.website_base_url)
# 输出"http://www.jiangnangame.com"
mySys.website_base_url = 'http://www.jiangnangame.com'
print(mySys.website_base_url)
在Python内部,原生的property修饰器是利用C语言在底层实现的,感兴趣的同学可以自行去阅读其源码:github.com/python/cpyt…
必须指出的是,虽然Python提供了原生实现,但如前文展示的需要传入自定义参数或者更加复杂的情形,原生接口就无能为力了。因此我们还是需要掌握自定义的property修饰器的实现方法。
使用装饰器导致原始函数属性丢失问题
关于描述器对象和函数装饰器结合的话题讲完了。在文章的最后,让我们回到函数装饰器本身,来聊一聊另外一个比较重要的知识点。
在上回的文章中,我们已经知道Python中函数装饰器机制的本质,是对函数通过装饰器进行包装,并利用包装后的新函数替换掉原来的被包装函数。
那么细心的同学肯定注意到了,在包装过程中原函数的某些属性会丢失,比如函数的所属模块__module__、函数的名称__name__、嵌套函数和类中方法的嵌套结构__qualname__、函数注释__doc__,以及可能存在的程序员自行为原函数定义的自有属性。
这可能会导致原函数的功能出现异常,因此我们需要这些可能丢失的属性一并复制到包装后得到的新函数上。
幸运的是,Python原生提供了实现这个功能的函数functools.update_wrapper
来解决这个我们编写装饰器的常见需求。
下面我直接把源代码(位于Python解释器安装目录/Lib/functools.py)贴出来,由于代码很简单,我这里就不作额外说明了。
# update_wrapper() and wraps() are tools to help write
# wrapper functions that can handle naive introspection
WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__qualname__', '__doc__',
'__annotations__')
WRAPPER_UPDATES = ('__dict__',)
def update_wrapper(wrapper,
wrapped,
assigned = WRAPPER_ASSIGNMENTS,
updated = WRAPPER_UPDATES):
"""Update a wrapper function to look like the wrapped function
wrapper is the function to be updated
wrapped is the original function
assigned is a tuple naming the attributes assigned directly
from the wrapped function to the wrapper function (defaults to
functools.WRAPPER_ASSIGNMENTS)
updated is a tuple naming the attributes of the wrapper that
are updated with the corresponding attribute from the wrapped
function (defaults to functools.WRAPPER_UPDATES)
"""
for attr in assigned:
try:
value = getattr(wrapped, attr)
except AttributeError:
pass
else:
setattr(wrapper, attr, value)
for attr in updated:
getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
# Issue #17482: set __wrapped__ last so we don't inadvertently copy it
# from the wrapped function when updating __dict__
wrapper.__wrapped__ = wrapped
# Return the wrapper so this can be used as a decorator via partial()
return wrapper
下面通过一个简单的例子来说明该函数的效果:
import functools
def decorator(func):
def wrapper():
func()
return wrapper
def decorator_fixed(func):
def wrapper():
func()
# 把原函数的各种属性复制过去
functools.update_wrapper(wrapper, func)
return wrapper
@decorator
def foo1():
print("I'm foo1")
@decorator_fixed
def foo2():
print("I'm foo1")
# 输出wrapper,foo1的元数据丢失!
print(foo1.__name__)
# 正确输出foo2
print(foo2.__name__)
题外话
当下这个大数据时代不掌握一门编程语言怎么跟的上脚本呢?当下最火的编程语言Python前景一片光明!如果你也想跟上时代提升自己那么请看一下.
感兴趣的小伙伴,赠送全套Python学习资料,包含面试题、简历资料等具体看下方。
一、Python所有方向的学习路线
Python所有方向的技术点做的整理,形成各个领域的知识点汇总,它的用处就在于,你可以按照下面的知识点去找对应的学习资源,保证自己学得较为全面。
二、Python必备开发工具
工具都帮大家整理好了,安装就可直接上手!
三、最新Python学习笔记
当我学到一定基础,有自己的理解能力的时候,会去阅读一些前辈整理的书籍或者手写的笔记资料,这些笔记详细记载了他们对一些技术点的理解,这些理解是比较独到,可以学到不一样的思路。
四、Python视频合集
观看全面零基础学习视频,看视频学习是最快捷也是最有效果的方式,跟着视频中老师的思路,从基础到深入,还是很容易入门的。
五、实战案例
纸上得来终觉浅,要学会跟着视频一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
六、面试宝典
简历模板

若有侵权,请联系删除