cook book:9:元编程
1:在函数上添加包装器 在函数上添加一个包装器,增加额外的操作处理(比如日志、计时等)。
# 使用额外的代码包装一个函数,可以定义一个装饰器函数 import time from functools import wraps def timethis(func): """报告执行时间的装饰程序""" # @wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。 # 这可以让我们在装饰器里面访问在装饰之前的函数的属性 @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end - start) return result return wrapper @timethis def countdown(n): total = 0 while n > 0: total += n n -= 1 return total print(countdown(10000000)) # 打印 countdown 0.8188097476959229 50000005000000
# 一个装饰器就是一个函数,它接受一个函数作为参数并返回一个新的函数 @timethis def countdown(n): pass # 上面等同于下面 def countdown(n): pass countdown = timethis(countdown)
@timethis 等同 countdown = timethis(countdown)
# 内置的装饰器比如 @staticmethod, @classmethod,@property 原理也是一样的 class A: @classmethod def method(cls): pass class B: # 类方法的等价定义 def method(cls): pass method = classmethod(method)
# 上面的 wrapper() 函数中, 装饰器内部定义了一个使用 *args 和 **kwargs 来接受任意参数的函数。
在这个函数里面调用了原始函数并将其结果返回,不过你还可以添加其他额外的代码(比如计时)。
然后这个新的函数包装器被作为结果返回来代替原始函数
# 需要强调的是装饰器并不会修改原始函数的参数签名以及返回值。 使用 *args 和 **kwargs 目的就是确保任何参数都能适用。
而返回结果值基本都是调用原始函数 func(*args, **kwargs) 的返回结果,其中func就是原始函数
# 使用 @wraps(func) 注解是很重要的, 它能保留原始函数的元数据,新手经常会忽略这个细节。
2:创建装饰器时保留函数元信息 functools
库中的 @wraps
装饰器
# 写了一个装饰器作用在某个函数上,但是这个函数的重要的元信息比如名字、文档字符串、注解和参数签名都丢失了 # 任何时候定义装饰器的时候,都应该使用 functools 库中的 @wraps 装饰器来注解底层包装函数 import time from functools import wraps def timethis(func): """报告执行时间的装饰程序""" # @wraps接受一个函数来进行装饰,并加入了复制函数名称、注释文档、参数列表等等的功能。 # 这可以让我们在装饰器里面访问在装饰之前的函数的属性 @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end - start) return result return wrapper @timethis def countdown(n: int): """countdown doc""" while n > 0: n -= 1
# 使用这个被包装后的函数countdown并检查它的元信息: countdown(100000) print(countdown.__name__) # countdown print(countdown.__doc__) # countdown doc print(countdown.__annotations__) # {'n': <class 'int'>}
# 在编写装饰器的时候复制元信息是一个非常重要的部分。 # 忘记了使用 @wraps,那么你会发现被装饰函数丢失了所有有用的信息。如果忽略 @wraps 后的效果是下面这样的 import time def timethis(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end - start) return result return wrapper @timethis def countdown(n: int): """countdown doc""" while n > 0: n -= 1 return n countdown(100000) print(countdown.__name__) # wrapper print(countdown.__doc__) # None print(countdown.__annotations__) # {}
import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(func.__name__, end - start) return result return wrapper @timethis def countdown(n: int): """countdown doc""" while n > 0: n -= 1 return n # @wraps 有一个重要特征是它能让你通过属性 __wrapped__ 直接访问被包装函数 print(countdown.__wrapped__) # <function countdown at 0x000001FD64BFC4C0>:返回的是countdown这个函数的内存地址 print(countdown.__wrapped__(10000)) # 0 # countdown.__wrapped__(10000) 调用的是countdown原本的函数,调用的是没有被timethis装饰器装饰过的函数,
所以只打印0,没有运行timethis装饰器里面的代码 # __wrapped__ 属性还能让被装饰函数正确暴露底层的参数签名信息 from inspect import signature print(signature(countdown)) # (n: int),显示的是底层被装饰函数countdown的参数信息 # 如果装饰没有@wraps(func)的话 print(signature(countdown)) 打印的是(*args, **kwargs) # 怎样让装饰器去直接复制原始函数的参数签名信息, 如果想自己手动实现的话需要做大量的工作, 最好就简单的使用 @wraps 装饰器。 通过底层的 __wrapped__ 属性访问到函数签名信息
3:解除一个装饰器 装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数 @wraps:访问 __wrapped__
属性来访问原始函数
# 访问 __wrapped__ 属性来访问原始函数: @somedecorator def add(x, y): return x + y orig_add = add.__wrapped__ orig_add(3, 4) # 7
# 直接访问未包装的原始函数在调试、内省和其他函数操作时是很有用的。
但是我们这里的方案仅仅适用于在包装器中正确使用了 @wraps 或者直接设置了 __wrapped__ 属性的情况 # 多个包装器,那么访问 __wrapped__ 属性的行为是不可预知的,应该避免这样做。
在Python3.3中,它会略过所有的包装层, from functools import wraps def decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper def decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper @decorator1 @decorator2 def add(x, y): return x + y print(add(2, 3)) # 打印:Decorator 1,Decorator 2,5 add.__wrapped__(2, 3) # 打印:5
Python3.4中:
print(add(2, 3)) # 打印:Decorator 1,Decorator 2,5 add.__wrapped__(2, 3) # 打印:Decorator 2,5
py3.4和py3.9一样的机制
# 当前最新的py 3.9中 from functools import wraps def decorator1(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper def decorator2(func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper @decorator1 @decorator2 # 先调用decorator2再调用decorator1 def add(x, y): return x + y # print(add(2, 3)) # print(add.__wrapped__.__wrapped__) # <function add at 0x00000148C28ED5E0> print(add.__wrapped__(2, 3)) # 打印:Decorator 2, 5 print(add.__wrapped__.__wrapped__(2, 3)) # 打印:5
@decorator1
@decorator2
func()
如上一个func函数调用多个装饰器,先进后院,先调用@decorator2装饰器生成一个函数1后
生成的函数1再调用@decorator1这个装饰器,类似一个嵌套
func函数调用decorator2装饰器后生成一个函数1,func这个变量名指向新的函数1,func=1函数
1这个函数由于使用了wraps装饰器,所以函数1的__wrapped__信息还是func这个最底层函数的信息
新的函数1又被@decorator1这个装饰器装饰生成一个函数2,func这个变量名指向新的函数2,func=2函数
2这个函数使用了wraps装饰器,使用函数1的信息,但是函数1的__wrapped__信息还是来自func这个最底层函数的信息
所以func.__wrapped__的信息是add函数:<function add at 0x00000148C28ED5E0>
但是func.__wrapped__返回的函数的内存地址指向的是函数1,因为被@decorator1装饰的函数是函数1
被@decorator2装饰的函数是func,@decorator2装饰的函数func才生成函数1
所以例子:add.__wrapped__(2, 3)调用的是被@decorator2装饰器装饰的函数func
add.__wrapped__.__wrapped__(2, 3) 这个才是调用最底层的func
# 并不是所有的装饰器都使用了 @wraps ,
# 所以这里的方案并不全部适用。
# 特别的,内置的装饰器 @staticmethod 和 @classmethod 就没有遵循这个约定 (它们把原始函数存储在属性 __func__ 中)
4:定义一个带参数的装饰器 定义一个可以接受参数的装饰器(三层装饰器)
# 装饰器,给函数添加日志功能 # 同时允许用户指定日志的级别和其他的选项 from functools import wraps import logging def logged(level, name=None, message=None): """ 向函数添加日志记录。 level是日志记录级别,name是记录器名称,message是日志消息。 如果未指定名称和消息,它们默认为函数的模块和名称。 """ def decorate(func): log_name = name if name else func.__module__ # __main__ log = logging.getLogger(log_name) log_msg = message if message else func.__name__ # add ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) log.addHandler(ch) @wraps(func) def wrapper(*args, **kwargs): log.log(level, log_msg) return func(*args, **kwargs) return wrapper return decorate @logged(logging.DEBUG) def add(x, y): return x + y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!') add(2, 3) spam() # 运行这个终端打印乐日志spam # 带参数的装饰器核心思想很简单,最外层的函数 logged() 接受参数并将它们作用在内部的装饰器函数上面。
内层的函数 decorate() 接受一个函数作为参数,然后在函数上面放置一个包装器。
这里的关键点是包装器是可以使用传递给 logged() 的参数的
@decorator(x, y, z) def func(a, b): pass @decorator(x, y, z)和func = decorator(x, y, z)(func)等效 @decorator(x, y, z)本质上就是个语法糖 decorator(x, y, z) 的返回结果必须是一个可调用对象,它接受一个函数作为参数并包装它
5:可自定义属性的装饰器 写一个装饰器来包装一个函数,并且允许用户提供参数在运行时控制装饰器行为。
# 引入一个访问函数,使用 nonlocal 来修改内部变量,然后这个访问函数被作为一个属性赋值给包装函数 from functools import wraps, partial import logging # 将函数作为obj的属性附加的实用程序装饰器 def attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return func def logged(level, name=None, message=None): """ 向函数添加日志记录。级别是日志记录 级别,name是记录器名称,message是 日志消息。如果未指定名称和消息, 它们默认为函数的模块和名称。 """ def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) # 附加setter函数 # 等同:set_level = attach_wrapper(wrapper)(set_level) # attach_wrapper(wrapper)先调用此时obj=wrapper而func=None # 走if里面的语句返回一个partial(attach_wrapper, wrapper), # 返回的本质上还是attach_wrapper函数但是绑定了第一个参数wrapper,然后这个函数调用(set_level) # 本质上还是调用attach_wrapper函数,attach_wrapper(wrapper, set_level) # setattr(wrapper, 'set_level', set_level),往wrapper这个函数里写set_level函数属性+名称 # wrapper['set_level'] = set_level return set_level # 外部add.set_level(logging.WARNING) add本质指向wrapper函数, # add.set_level(logging.WARNING)就是wrapper.set_level(logging.WARNING) # 本质上就是调用set_level函数改变level属性,从而影响wrapper这个函数里面的参数 @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg return wrapper return decorate @logged(logging.DEBUG) # 等同:add = logged(logging.DEBUG)(add)——add = wrapper函数 def add(x, y): return x + y @logged(logging.CRITICAL, 'example') def spam(): print('Spam!') logging.basicConfig(level=logging.DEBUG) add(2, 3) # 输出:DEBUG:__main__:add # 更改日志消息 add.set_message('Add called') add(2, 3) # 输出:DEBUG:__main__:Add called # 更改日志级别 add.set_level(logging.WARNING) add(2, 3) # 输出:WARNING:__main__:Add called
# 访问函数(如 set_message() 和 set_level() ),它们被作为属性赋给包装器。
每个访问函数允许使用 nonlocal 来修改函数内部的变量 # 访问函数会在多层装饰器间传播(如果你的装饰器都使用了 @functools.wraps 注解) @timethis @logged(logging.DEBUG) def countdown(n): while n > 0: n -= 1 # 访问函数依旧有效: >>> countdown(10000000) DEBUG:__main__:countdown countdown 0.8198461532592773 >>> countdown.set_level(logging.WARNING) >>> countdown.set_message("Counting down to zero") >>> countdown(10000000) WARNING:__main__:Counting down to zero countdown 0.8225970268249512 >>> # 即使装饰器像下面这样以相反的方向排放,效果也是一样的 @logged(logging.DEBUG) @timethis def countdown(n): while n > 0: n -= 1 # 还能通过使用lambda表达式代码来让访问函数的返回不同的设定值
在装饰器logged函数的decorate函数内加上下面两个表达式(两者可以返回装饰器内level的设置值)
@attach_wrapper(wrapper) def get_level(): return level # 可供替代的 wrapper.get_level = lambda: level
上面等同往wrapper这个命名空间存储函数变量:get_level
使得wrapper.get_level()可以运行函数,外面add指向wrapper
add.get_level()也是等同调用wrapper.get_level()可以返回
level变量的值知道当前等级
# 对于访问函数的首次使用 # 考虑另外一个方法直接访问函数的属性
from functools import wraps import logging def logged(level, name=None, message=None): def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): wrapper.log.log(wrapper.level, wrapper.logmsg) return func(*args, **kwargs) # 附加可调整属性 wrapper.level = level wrapper.logmsg = logmsg wrapper.log = log return wrapper return decorate @logged(logging.WARNING) def add(x, y): return x + y
add.logmsg = 'ZZZZ'
add(2, 3) # 打印 ZZZZ# 这个方法也可能正常工作,但前提是它必须是最外层的装饰器才行。 如果它的上面还有另外的装饰器(比如上面提到的 @timethis 例子), 那么它会隐藏底层属性,使得修改它们没有任何作用。 而通过使用访问函数就能避免这样的局限性
from functools import wraps import logging def logged(level, name=None, message=None): def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(wrapper.level, wrapper.logmsg) return func(*args, **kwargs) # 附加可调整属性 wrapper.level = level wrapper.logmsg = logmsg return wrapper return decorate def test(func): @wraps(func) def inner(*args, **kwargs): print('测试的装饰器') return func(*args, **kwargs) print(inner.__dict__) return inner @test @logged(logging.WARNING) def add(x, y): return x + y # add = wrapper # add = inner inner里面的func=wrapper # 因为有@wraps(func),所以inner对象会继承wrapped对象里面所有的信息 # inner.__dict__ = {'__wrapped__': <function add at 0x0000021187A80D30>, 'level': 30, 'logmsg': 'add'} # add.logmsg = '你是傻逼吗' 在inner函数的空间存储了个字典 logmsg = '你是傻逼吗' # add(2, 3) 本质上调用inner(), # 运行func(*args, **kwargs),func=wrapper——> wrapper() # log.log(wrapper.level, wrapper.logmsg),找上层函数的logmsg=add,所以打印add # 这里的add.logmsg本质是修改inner内存空间里面的logmsg属性 # add()调用的是inner()函数,inner()里面func()调用wrapper() # wrapper()里面的logmsg属性没有被修改,还是一开始的,所以调用wrapper()函数的时候 # log.log(wrapper.level, wrapper.logmsg)打印的还是一开始的add而不是最新的logmsg # 需要:add.__wrapped__.logmsg = '你是傻逼吗' , # 在wrapped的内存空间里修改logmsg的值log.log(wrapper.level, wrapper.logmsg)才会打印'你是傻逼吗' add.logmsg = '你是傻逼吗' # add.__wrapped__.logmsg = '你是傻逼吗' add(2, 3)
from functools import wraps, partial import logging def attach_wrapper(obj, func=None): if func is None: return partial(attach_wrapper, obj) setattr(obj, func.__name__, func) return func def logged(level, name=None, message=None): def decorate(func): logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) @attach_wrapper(wrapper) def set_level(newlevel): nonlocal level level = newlevel @attach_wrapper(wrapper) def set_message(newmsg): nonlocal logmsg logmsg = newmsg return wrapper return decorate def test(func): @wraps(func) def inner(*args, **kwargs): print('测试的装饰器') return func(*args, **kwargs) print(inner.__dict__) return inner @test @logged(logging.WARNING) def add(x, y): return x + y # add = wrapper # add = inner inner里面的func=wrapper # 因为有@wraps(func),所以inner对象会继承wrapped对象里面所有的信息 # {'__wrapped__': <function add at 0x000002116F2F0E50>, # 'set_level': <function logged.<locals>.decorate.<locals>.set_level at 0x000002116F2F0EE0>, # 'set_message': <function logged.<locals>.decorate.<locals>.set_message at 0x000002116F2F0F70>} # add.set_message('你是傻逼吗'),因为add指向inner函数,所以inner.set_message('你是傻逼吗') # 调用inner内存空间里的set_message函数,inner空间里的set_message函数指向的是wrapper这个空间里的set_message函数 # 所以add.set_message('你是傻逼吗')等同调用了wrapper.set_message('你是傻逼吗'),修改了wrapper空间里面的message属性为'你是傻逼吗' # 所以后面add(2, 3)调用wrapper()然后log.log(level, logmsg)打印logmsg的时候打印的是最新修改的属性:你是傻逼吗 add.set_message('你是傻逼吗') add(2, 3)
# 所以需要修改属性,这种使用访问函数的方法是最好的方案
6:带可选参数的装饰器 写一个装饰器,既可以不传参数给它: @decorator
, 也可以传递可选参数给它:@decorator(x,y,z)
from functools import wraps, partial import logging def logged(func=None, *, level=logging.DEBUG, name=None, message=None): if func is None: return partial(logged, level=level, name=name, message=message) logname = name if name else func.__module__ log = logging.getLogger(logname) logmsg = message if message else func.__name__ @wraps(func) def wrapper(*args, **kwargs): log.log(level, logmsg) return func(*args, **kwargs) return wrapper # add = logged(add) # 这样调用会把add传递进去,所以func不是None,@logged不会走if里面的语句 # 直接走后面的求logname,log和logmsg等信息,然后wreturn wrapper # add指向wrapper @logged # 不带参数 def add(x, y): return x + y # spam = logged(level=logging.CRITICAL, name='example', message='你是傻逼吗')(spam) # logged(level=logging.CRITICAL, name='example', message='你是傻逼吗')这样调用没有把func传递进去,func=None, # 调用if里面的逻辑return返回 partial绑定了参数后的logged函数,绑定了三个参数level=level, name=name, message=message # 现在调用logged()(spam)== logged(spam, level=level, name=name, message=message) # spam = logged(spam, level=level, name=name, message=message) 返回wrapper # spam 指向wrapper,spam()等同调用wrapper(),使用传递进去的logname,logmsg 参数 @logged(level=logging.CRITICAL, name='example', message='你是傻逼吗') # 带参数 def spam(): print('Spam!') spam() # 这样定义@logged 装饰器可以同时不带参数或带参数。
# 大部分程序员习惯了要么不给它们传递任何参数,要么给它们传递确切参数。 # 从技术上来讲,我们可以定义一个所有参数都是可选的装饰器 @logged() def add(x, y): return x+y # 这种写法并不符合我们的习惯, # 有时候忘记加上后面的括号会导致错误。 # 需要以一致的编程风格来同时满足没有括号和有括号两种情况 # 熟悉装饰器是如何作用到函数上以及它们的调用规则 @logged def add(x, y): return x + y 等同下面:调用序列跟下面等价 def add(x, y): return x + y add = logged(add) # 这时候,被装饰函数会被当做第一个参数直接传递给 logged 装饰器。
因此,logged() 中的第一个参数就是被包装函数本身。所有其他参数都必须有默认值 @logged(level=logging.CRITICAL, name='example') def spam(): print('Spam!') 等同下面:调用序列跟下面等价 def spam(): print('Spam!') spam = logged(level=logging.CRITICAL, name='example')(spam) # 初始调用 logged() 函数时,被包装函数并没有传递进来。 因此在装饰器内,它必须是可选的
。这个反过来会迫使其他参数必须使用关键字来指定。 并且,但这些参数被传递进来后,
装饰器要返回一个接受一个函数参数并包装它的函数。 为了这样做,我们使用了一个技巧,
就是利用 functools.partial 。 它会返回一个未完全初始化的自身,除了被包装函数外其他参数都已经确定下来了
7:利用装饰器强制函数上的类型检查 作为某种编程规约,你想在对函数参数进行强制类型检查
from inspect import signature from functools import wraps def typeassert(*ty_args, **ty_kwargs): def decorate(func): # 如果处于优化模式,请禁用类型检查 # __debug__:如果 Python 没有以 -O 选项启动,则此常量为真值 if not __debug__: return func # 将函数参数名称映射到提供的类型 sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) print(type(bound_values)) # 跨提供的参数的强制类型断言 for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate @typeassert(int, z=int) # spam = typeassert(int, z=int)(spam) # typeassert(int, z=int)返回decorate # decorate(spam)——>spam传递给func变量,返回wrapper # bound_types根据传递进来的参数*ty_args, **ty_kwargs 得到 = {'x': <class 'int'>, 'z': <class 'int'>} # spam(1, 2, 3) 本质上运行wrapper函数 # wrapper(1, 2, 3),把1,2,3传递给*args, **kwargs,所以args=(1, 2, 3),kwargs={} # bound_values = sig.bind(*args, **kwargs) = <BoundArguments (x=1, y=2, z=3)>和<BoundArguments (x=1, y='hello', z=3)> # 断言bound_values这个类似字典的东西里面的参数类型和指定的bound_types里面的类型是不是相等 def spam(x, y, z=42): print(x, y, z) spam(1, 2, 3) spam(1, 'hello', 3) # spam(1, 'hello', 'world') # TypeError: Argument z must be <class 'int'> # 这个装饰器非常灵活,既可以指定所有参数类型,也可以只指定部分 # 可以通过位置或关键字来指定参数类型
# 装饰器只会在函数定义时被调用一次。 # 有时候想去掉装饰器的功能,只需要简单的返回被装饰函数即可,
如果全局变量 __debug__ 被设置成了False(当你使用-O或-OO参数的优化模式执行程序时),
那么就直接返回未修改过的函数本身:如下装饰器 def decorate(func): # 如果处于优化模式,请禁用类型检查 if not __debug__: return func
# 对被包装函数的参数签名进行了检查,使用了 inspect.signature() 函数。 # inspect.signature允许你提取一个可调用对象的参数签名信息 from inspect import signature def spam(x, y, z=42): pass sig = signature(spam) print(sig) # (x, y, z=42) print(sig.parameters) # OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)]) print(sig.parameters['z'].name) # z print(sig.parameters['z'].default) # 42 print(sig.parameters['z'].kind) # POSITIONAL_OR_KEYWORD # 装饰器的开始部分,我们使用了 bind_partial() 方法来执行从指定类型到名称的部分绑定 bound_types = sig.bind_partial(int, z=int) print(bound_types) # <BoundArguments (x=<class 'int'>, z=<class 'int'>)> print(bound_types.arguments) # {'x': <class 'int'>, 'z': <class 'int'>} # 这个部分绑定中,缺失的参数被忽略了(并没有对y进行绑定)。 # 最重要的是创建了一个有序字典 bound_types.arguments 。
这个字典会将参数名以函数签名中相同顺序映射到指定的类型值上面去。 装饰器例子中,这个映射包含了我们要强制指定的类型断言 # 在装饰器创建的实际包装函数中使用到了 sig.bind() 方法。 bind() 跟 bind_partial() 类似,
bind方法不允许忽略任何参数,bind_partial方法允许忽略参数 bound_values = sig.bind(1, 2, 3) bound_values.arguments # OrderedDict([('x', 1), ('y', 2), ('z', 3)]) # 使用这个映射我们可以很轻松的实现我们的强制类型检查: for name, value in bound_values.arguments.items(): if name in bound_types.arguments: if not isinstance(value, bound_types.arguments[name]): raise TypeError()
# 上面的方案有点小瑕疵 # 对于有默认值的参数并不适用。 # 下面的代码可以正常工作,尽管items的类型是错误的 from inspect import signature from functools import wraps def type_assert(*ty_args, **ty_kwargs): def decorate(func): if not __debug__: return func sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments print(bound_types) # 期望类型:{'x': <class 'int'>, 'items': <class 'list'>} @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) print(bound_values.arguments) # 实际传递进来的参数类型:{'x': 2},校验的是传参的类型,没有对默认参数做校验 for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate @type_assert(int, list) def bar(x, items=None): if items is None: items = [] items.append(x) return items print(bar(2)) # bar(2, 3) # 报错:TypeError: Argument items must be <class 'list'>
# 为什么不写一个装饰器来查找函数中的注解呢 @typeassert def spam(x:int, y, z:int = 42): print(x,y,z) # 如果使用了函数参数注解,那么就被限制了 # 如果注解被用来做类型检查就不能做其他事情了 # 而且 @typeassert 不能再用于使用注解做其他事情的函数了。 而使用上面的装饰器参数灵活性大多了
8:将装饰器定义为类的一部分 类中定义装饰器,并将其作用在其他函数或方法上
# 类里面定义装饰器,首先要确认它的使用方式,是作为一个实例方法还是类方法 from functools import wraps class A: # 作为实例方法的装饰器 def decorator1(self, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 1') return func(*args, **kwargs) return wrapper
# 作为类方法的装饰器 @classmethod def decorator2(cls, func): @wraps(func) def wrapper(*args, **kwargs): print('Decorator 2') return func(*args, **kwargs) return wrapper # 作为实例方法 a = A() @a.decorator1 def spam(): pass # 作为类方法 @A.decorator2 def grok(): pass # 一个是实例调用,一个是类调用。
# 类中定义装饰器初看上去好像很奇怪,但是在标准库中有很多这样的例子。 特别的,@property 装饰器实际上是一个类,
它里面定义了三个方法 getter(), setter(), deleter() , 每一个方法都是一个装饰器 class Person: # 创建属性实例 first_name = property() # 应用装饰器方法 @first_name.getter def first_name(self): return self._first_name @first_name.setter def first_name(self, value): if not isinstance(value, str): raise TypeError('Expected a string') self._first_name = value p = Person() p.first_name = 'liergou' print(p._first_name) # liergou # 为什么要这么定义的主要原因是各种不同的装饰器方法会在关联的 property 实例上操作它的状态。
因此,任何时候只要你碰到需要在装饰器中记录或绑定信息,那么这不失为一种可行方法 # 在类中定义装饰器有个难理解的地方就是对于额外参数 self 或 cls 的正确使用。
尽管最外层的装饰器函数比如 decorator1() 或 decorator2() 需要提供一个 self 或 cls 参数,
但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个 self 参数。
你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它 # 对于类里面定义的包装器还有一点比较难理解,就是在涉及到继承的时候。
例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写 class B(A): @A.decorator2 def bar(self): pass # 也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。
你不能使用 @B.decorator2 ,因为在方法定义时,这个类B还没有被创建
9:将装饰器定义为类 使用一个装饰器去包装函数,希望返回一个可调用的实例,需要让装饰器可以同时工作在类定义的内部和外部
# 函数使用wraps详解
from functools import wraps
def decorator1(func): # @wraps(func) == wraps(func)(wrapper) # 相当于在wrapper这个变量对象内存储一些字典信息比如:'__wrapped__' = 函数func的内存空间 # wrapper.__dict__输出:{'__wrapped__': <function add at 0x0000020DD15AB4C0>} @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper @decorator1 def add(x, y): return x + y print(add.__name__) # 输出:add
# 将装饰器定义成一个实例,你需要确保它实现了 __call__() 和 __get__() 方法。 # 定义了一个类,它在其他函数上放置一个简单的记录层 import types from functools import wraps class Profiled: def __init__(self, func): wraps(func)(self) # wraps(func)(self) 本质是使用了wraps这个装饰器: @wraps(self) # 相当于在self这个实例对象里面写属性:'__wrapped__': <function add at 0x0000023AEC7EB4C0> # 所以后面的self.__wrapped__属性指向add这个原本的函数,加()就是调用 # 等同上面这样的写法,wraps(func)(self)本质上就是调用装饰器@wraps(self) print(self.__dict__) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 # self.__wrapped__返回的是函数func的内存地址,这里返回调用 return self.__wrapped__(*args, **kwargs) def __get__(self, instance, cls): if instance is None: return self else: return types.MethodType(self, instance) # 当做一个普通的装饰器来使用,在类里面或外面都可以: # 类外面使用 @Profiled # @Profiled 等同 add = Profiled(add) # 相当于把add函数传到Profiled类里实例化,然后add重新赋值等于Profiled(add)这个类实例化 def add(x, y): return x + y add(2, 3) add(2, 3) add(2, 3) add(2, 3) add(2, 3) print(add.ncalls) # 5 # 类里面使用 class Spam: @Profiled def bar(self, x): print(self, x) s = Spam() s.bar(1) s.bar(2) print(Spam.bar.ncalls) # 输出2
__call__():该方法的功能类似于在类中重载 () 运算符,使得类实例对象可以像调用普通函数那样 __get__:类中定义了__get__,__set__,__delete__中任意一个方法就是描述器,
拥有这个方法的类,应该(也可以说是必须)产生一个实例,并且这个实例是另外一个类的类属性 # 也就是说拥有这个方法的类,那么它的实例应该属于另外一个类/对象的一个属性 class TestDes: def __get__(self, instance, owner): print(instance, owner) return 'TestDes:__get__' class TestMain: des = TestDes() if __name__ == '__main__': t = TestMain() print(t.des) print(TestMain.des) # TestDes定义了__get__方法,在TestMain中,定义了一个类属性des,
是TestDes的一个实例,我们访问t.des或者TestMain.des的时候访问的就是访问了TestDes的__get__方法
Python types.MethodType动态更改类方法:https://www.cnblogs.com/marsggbo/p/12625697.html # types.MethodType的用法 import types class Person(object): def __init__(self, name=None, age=None): self.name = name self.age = age def eat(self): print("eat food") class A: age = 18 def __call__(self, obj, speed): print("%s在移动, 速度是 %d km/h" % (obj.name, speed)) P = Person("老王", 24) a = A() x = types.MethodType(a, P) # 类似给run方法绑定了P这个参数给self,还能使用self的方法和属性 x(180) print(x.age)
x = types.MethodType(a, P):类似返回了一个a对象赋值给x(a只要是可以调用的对象就可以,可以是函数对象,可以是实现了__call__方法的类的实例)
x(180):相当于types.MethodType(a, P)()本质上是调用a这个对象但是指定了参数p,a(p, 180)
x.age:types.MethodType(a, P)类似返回了一个a对象,等同调用a.age返回18
types.MethodType也是类似指定一个参数,把后面的参数p绑定给前面可以调用的对象a,和partial函数很类似
但是比partial强大的是types.MethodType函数返回的是一个对象a,可以求对象a里面的属性和方法
但是partial(a, p)返回的不是一个对象,仅仅只是一个方法,不可以对返回的方法函数求属性,如下
from functools import partial
class Person(object):
def __init__(self, name=None, age=None):
self.name = name
self.age = age
def eat(self):
print("eat food")
class A:
age = 18
def __call__(self, obj, speed):
print("%s在移动, 速度是 %d km/h" % (obj.name, speed))
P = Person("老王", 24)
a = A()
x = partial(a, P)
x(1000)
print(x.age) # 报错:AttributeError: 'functools.partial' object has no attribute 'age'
# 使用类装饰器装饰一个类里面函数的详解
import types from functools import wraps class Profiled: def __init__(self, func): wraps(func)(self) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 return self.__wrapped__(*args, **kwargs) # 指向原始的 Spam类里面的bar函数,bar函数需要self+x两个参数才能调用 def __get__(self, instance, cls): # instance是c这个实例,非实例调用返回None if instance is None: return self else: # self为Profiled(bar)实例=bar, # instance为Spam()实例=s # types.MethodType方法:把self绑定到instance这个实例上 # 也就是实例bar绑定到到实例s return types.MethodType(self, instance) # return self 和 return partial(self, instance)都不行, # 只能返回types.MethodType(self, instance)既绑定了instance参数也有ncalls属性 # return self不行没有绑定instance这个参数, # 如果return self那么外面调用s.bar调用__get__方法走else得到return self当前self = bar = Profiled(bar) # s.bar(1)——>Profiled(bar)(1)——> 调用__get__方法传递参数1 # Profiled.__get__(Profiled(bar), 1),然后return调用self.__wrapped__(*args, **kwargs) # 本质上就是调用Spam里面的bar函数,等同bar(1)但是调用bar函数需要bar(self, x)两个参数, # 所以需要使用types.MethodType(self, instance)绑定instance传参给self,然后1传参给x就可以调用了 # return partial(self, instance)这个虽然绑定参数了,但是返回的是一个类似self的函数,没有ncalls属性 # 只能使用types.MethodType(self, instance) class Spam: # @Profiled等同bar = Profiled(bar) # Profiled类是一个描述器,bar = Profiled(bar) @Profiled def bar(self, x): print(self, x) s = Spam() # s.bar(1)——>先运行: # s.bar调用Profiled类里的__get__方法, # 因为实例s调用走else: return types.MethodType(self, instance) # types.MethodType(self, instance)(1)本质上是self(1),self是Profiled(bar) # ——>rofiled(bar)(1)——>rofiled(bar).__call__(1),计数+次后1, # 调用self.__wrapped__(*args, **kwargs)也就是原来的bar函数 s.bar(1) s.bar(2) # s.bar:同上——>得到return types.MethodType(self, instance) # self为Profiled(bar)实例=bar, instance为Spam()实例=s 把self绑定到instance这个实例上 # rofiled(bar)实例,rofiled(bar).ncalls print(s.bar.ncalls)
# functools.wraps() 函数将被包装函数的元信息复制到可调用实例中去
# 忽视的 __get__() 方法运行报错:TypeError: bar() missing 1 required positional argument: 'x'
# 如下,没有__get__函数的话
from functools import wraps
class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)
class Spam:
@Profiled
def bar(self, x):
print(self, x)
s = Spam()
s.bar('ywt')
# s.bar = Profiled(bar), s.bar('ywt') = Profiled(bar)('ywt')
# ——> Profiled.__call__(Profiled(bar), 'ywt')
# ——> self.__wrapped__('ywt')
# ——> bar('ywt') 少了个参数,bar函数需要传两个参数,报错
# 出错原因是当方法函数在一个类中被查找时,它们的 __get__() 方法依据描述器协议被调用,
在这里,__get__() 的目的是创建一个绑定方法对象 (最终会给这个方法传递self参数) class Spam: pass def grok(self, x): print(self, x) s = Spam() g = grok.__get__(s, Spam) print(g) # <bound method grok of <__main__.Spam object at 0x00000247E1F5DFA0>> g(100) # <__main__.Spam object at 0x0000019D3DEFC9A0> 100:等同运行grok函数 # __get__() 方法是为了确保绑定方法对象能被正确的创建。
type.MethodType() 手动创建一个绑定方法来使用。只有当实例被使用的时候绑定方法才会被创建。
如果这个方法是在类上面来访问, 那么 __get__() 中的instance参数会被设置成None并直接返回 Profiled 实例本身。
这样的话我们就可以提取它的 ncalls 属性了
# 也可以使用闭包和 nonlocal 变量实现的装饰器 import types from functools import wraps def profiled(func): ncalls = 0 @wraps(func) def wrapper(*args, **kwargs): nonlocal ncalls ncalls += 1 return func(*args, **kwargs) wrapper.ncalls = lambda: ncalls return wrapper @profiled def add(x, y): return x + y # 这里对于 ncalls 的访问现在是通过一个被绑定为属性的函数来实现 add(2, 3) add(4, 5) add.ncalls() # 2
from functools import wraps class Profiled: def __init__(self, func): wraps(func)(self) self.ncalls = 0 def __call__(self, *args, **kwargs): self.ncalls += 1 return self.__wrapped__(*args, **kwargs) class Spam: @Profiled def bar(self, x): print(self, x) s = Spam() s.bar('ywt') # s.bar = Profiled(bar), s.bar('ywt') = Profiled(bar)('ywt') # ——> Profiled.__call__(Profiled(bar), 'ywt') # ——> self.__wrapped__('ywt'),self.__wrapped__指向bar函数所以
# ——> bar('ywt') 少了个参数,bar函数需要传两个参数
10:为类和静态方法提供装饰器 给类或静态方法提供装饰器
# 给类或静态方法提供装饰器要确保装饰器在 @classmethod 或 @staticmethod 之前 import time from functools import wraps def timethis(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() r = func(*args, **kwargs) end = time.time() print(end - start) return r return wrapper # 类说明装饰器对不同类型方法的应用 class Spam: @timethis # 装饰器使用在类实例方法 def instance_method(self, n): print(self, n) while n > 0: n -= 1
@classmethod @timethis # 装饰器使用在类方法 def class_method(cls, n): print(cls, n) while n > 0: n -= 1
@staticmethod @timethis # 装饰器使用在静态方法 def static_method(n): print(n) while n > 0: n -= 1 # 装饰后的类和静态方法可正常工作,只不过增加了额外的计时功能: s = Spam() s.instance_method(1000000) Spam.class_method(1000000) Spam.static_method(1000000) 输出: <__main__.Spam object at 0x00000279F662DE50> 1000000 0.04483962059020996 <class '__main__.Spam'> 1000000 0.053858280181884766 1000000 0.06083512306213379
# 如果把装饰器的顺序写错了就会出错 class Spam: @timethis @staticmethod def static_method(n): print(n) while n > 0: n -= 1 # 那么调用这个静态方法时就会报错: Spam.static_method(1000000) # TypeError: 'staticmethod' object is not callable # 问题在于 @classmethod 和 @staticmethod 实际上并不会创建可直接调用的对象,
而是创建特殊的描述器对象。因此在其他装饰器中将它们当做函数来使用时就会出错。
确保这种装饰器出现在装饰器链中的第一个位置可以修复这个问题 # 当我们在抽象基类中定义类方法和静态方法时:abc模块
例如,如果你想定义一个抽象类方法,可以使用类似下面的代码 from abc import ABCMeta, abstractmethod class A(metaclass=ABCMeta): @classmethod @abstractmethod # abstractmethod表示子类必须要实现这个方法 def method(cls): pass # 段代码中,@classmethod 跟 @abstractmethod 两者的顺序是有讲究的,如果你调换它们的顺序就会出错
11:装饰器为被包装函数增加参数 在装饰器中给被包装函数增加额外的参数,不影响这个函数现有的调用规则
# 可以使用关键字参数来给被包装函数增加额外参数 from functools import wraps def optional_debug(func): @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) return wrapper @optional_debug def spam(a, b, c): print(a, b, c) spam(1, 2, 3) 输出:1 2 3 spam(1, 2, 3, debug=True) 输出: Calling spam 1 2 3
装饰器装饰后调用spam本质上就是调用wrapper,wrapper能接收debug参数,把位置参数1,2,3封装给args成为一个元组
然后func(*args)里面解包成1,2,3
# 装饰器来给被包装函数增加参数的做法并不常见。它可以避免一些重复代码 from functools import wraps import inspect def a(x, debug=False): if debug: print('Calling a') def b(x, y, z, debug=False): if debug: print('Calling b') def c(x, y, debug=False): if debug: print('Calling c') # 如下重构a,b,f函数 # inspect模块用于收集python对象的信息,可以获取类或函数的参数的信息,源码,解析堆栈,对对象进行类型检查等等(详情查看官方文档) def optional_debug(func): # if 'debug' in inspect.getargspec(func).args: # 上面这种用法过时了,会警告,使用signature函数返回对象后再使用parameters返回一个有序字典来解析 如下 if 'debug' in inspect.signature(func).parameters.keys(): raise TypeError('debug argument already defined') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) return wrapper @optional_debug def a(x): pass @optional_debug def b(x, y, z): pass @optional_debug def c(x, y): pass
# 这种实现方案之所以行得通,在于强制关键字参数很容易被添加到接受 *args 和 **kwargs 参数的函数中。 # 通过使用强制关键字参数,它被作为一个特殊情况被挑选出来, 并且接下来仅仅使用剩余的位置和关键字参数去调用这个函数时, # 这个特殊参数会被排除在外。 也就是说,它并不会被纳入到 **kwargs 中去
# 还有一个难点就是如何去处理被添加的参数与被包装函数参数直接的名字冲突。 # 例如,如果装饰器 @optional_debug 作用在一个已经拥有一个 debug 参数的函数上时会有问题。 # 我们增加一步名字检查 # 被包装函数的函数签名其实是错误的 from functools import wraps import inspect def optional_debug(func): if 'debug' in inspect.signature(func).parameters.keys(): raise TypeError('debug argument already defined') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) return wrapper @optional_debug def add(x, y): return x + y print(inspect.signature(add)) # (x, y) # 其实现在add函数可以传递两个必填参数x,y,还有一个选填参数debug # inspect.signature(add)打印add这个被包装的函数的参数签名是错误的
# 装饰器为被包装函数增加参数,修改被装饰函数的函数参数签名错误问题 # 修改后包装后的函数签名就能正确的显示 debug 参数的存在了 from functools import wraps import inspect def optional_debug(func): if 'debug' in inspect.signature(func).parameters.keys(): raise TypeError('debug argument already defined') @wraps(func) def wrapper(*args, debug=False, **kwargs): if debug: print('Calling', func.__name__) return func(*args, **kwargs) sig = inspect.signature(func) # sig = (x, y) # sig.parameters = OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">)]) parms = list(sig.parameters.values()) # [<Parameter "x">, <Parameter "y">] parms.append(inspect.Parameter('debug', inspect.Parameter.KEYWORD_ONLY, default=False)) # parms = [<Parameter "x">, <Parameter "y">, <Parameter "debug=False">] wrapper.__signature__ = sig.replace(parameters=parms) # sig.replace修改wrapper函数的signature签名信息 return wrapper @optional_debug def add(x, y): return x + y print(inspect.signature(add)) # (x, y, *, debug=False) print(add(2, 3)) # 5
在装饰器内把debug参数的签名信息写到wrapper函数里面去
12:使用装饰器扩充类的功能 反省或者重写类定义的某部分来修改它的行为,不使用继承或元类的方式
# 类装饰器 # 重写了特殊方法 __getattribute__ 的类装饰器,加打印日志 def log_getattribute(cls): # 获取原始实现的__getattribute__方法 orig_getattribute = cls.__getattribute__ # 重构一个新的getattribute方法 def new_getattribute(self, name): print('getting:', name) return orig_getattribute(self, name) # 把重构的新的getattribute方法附加到类cls并返回cls cls.__getattribute__ = new_getattribute return cls @log_getattribute class A: def __init__(self, x): self.x = x def spam(self): pass a = A(42) a.x # getting: x a.spam() # getting: spam
# __getattribute__与__getattr__ # __getattribute__:可以改变访问属性的逻辑,类里是实现__getattribute__后,
obj.age就是调用__getattribute__方法,obj实例的类里没有实现__getattribute__方法就调用父类的__getattribute__方法 class A(object): def __init__(self): self.name = "Bob" self.age = 18 self.gender = "male" def __getattribute__(self, attr): # 拦截age属性 if attr == "age": return "问年龄是不礼貌的行为" # 非age属性执行默认操作 else: return object.__getattribute__(self, attr) if __name__ == "__main__": a = A() print(a.age) print(a.name) print(a.gender) # 输出: #问年龄是不礼貌的行为 #Bob #male # 在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。 # 但是,如果某个属性在__getattribute__方法中未能找到,此时会调用__getattr__方法 # 访问对象a中不存在的属性,会得到异常:AttributeError: 'A' object has no attribute 'NAME' # 想无论是a.NAME,a.Name,还是a.NaME等等,即n a m e(包含大小写)四个元素组成的属性,访问结果都同a.name一样,可以做如下处理 class A(object): def __init__(self): self.name = "Bob" self.age = 18 self.gender = "male" def __getattr__(self, attr): return eval("self." + attr.lower()) # 即:再次去执行__getattribute__方法 if __name__ == "__main__": a = A() print("a.name -> {}".format(a.name)) # a.name -> Bob print("a.NAME -> {}".format(a.NAME)) # a.name -> Bob print("a.Name -> {}".format(a.Name)) # a.name -> Bob print("a.NaME -> {}".format(a.NaME)) # a.name -> Bob 总结: 1.__getattribute__方法优先级比__getattr__高 2.只有在__getattribute__方法中找不到对应的属性时,才会调用__getattr__
求属性默认都是找__getattribute__方法,如果属性找不到跳转到__getattr__方法
3.如果是对不存在的属性做处理,尽量把逻辑写在__getattr__方法中
4.如果非得重写__getattribute__方法,需要注意两点:第一是避免.操作带来的死循环;
第二是不要遗忘父类的__getattribute__方法在子类中起的作用
# 重构__getattribute__方法很容易报错,谨慎使用 class Tree(object): def __init__(self, name): self.name = name self.cate = "plant" def __getattribute__(self, obj): if obj.endswith("e"): return object.__getattribute__(self, obj) else: return self.call_wind() def call_wind(self): return "树大招风" aa = Tree("大树") print(aa.name) # 因为name是以e结尾,所以返回的还是name,所以打印出"大树" print(aa.wind) # 报错:RecursionError: maximum recursion depth exceeded while calling a Python object # 执行aa.wind时,先调用__getattribute__方法,经过判断后,它返回的是self.call_wind(),即self.call_wind的执行结果, # self.call_wind==aa.call_wind,但当去调用aa这个对象的call_wind属性时,前提是又要去调用__getattribute__方法, # 反反复复,没完没了,形成了递归调用且没有退出机制,最终程序就挂了
#__getattribute__
的用法
class Tree(object):
def __init__(self,name):
self.name = name
self.cate = "plant"
def __getattribute__(self,obj):
print("哈哈")
return object.__getattribute__(self,obj)
aa = Tree("大树")
print(aa.name)
打印:
哈哈
大树
# __getattribute__是属性访问拦截器,就是当这个类的属性被访问时,会自动调用类的__getattribute__方法。
上面代码中,调用实例对象aa的name属性时,不会直接打印,而是把name的值作为实参传进__getattribute__方法中
经过一系列操作后,再把name的值返回。Python中只要定义了继承object的类,就默认存在属性拦截器,只不过是拦截后没有进行任何操作,而是直接返回。
所以我们可以自己改写__getattribute__方法来实现相关功能,比如查看权限、打印log日志等 class Tree(object): def __init__(self,name): self.name = name self.cate = "plant" def __getattribute__(self,*args,**kwargs): if args[0] == "大树" print("log 大树") return "我爱大树" else: return object.__getattribute__(self,*args,**kwargs) aa = Tree("大树") print(aa.name) print(aa.cate) 打印: log 大树 我爱大树 plant
# 类装饰器通常可以作为其他高级技术比如混入或元类的一种非常简洁的替代方案 # 使用继承方式扩充类的功能(调用属性加打印日志) class LoggedGetattribute: def __getattribute__(self, name): print('getting:', name) return super().__getattribute__(name) class A(LoggedGetattribute): def __init__(self,x): self.x = x def spam(self): pass # 继承方案也可以实现, # 类装饰器方案就显得更加直观,它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数 # 一个类上面使用多个类装饰器,那么就需要注意下顺序问题。
例如,一个装饰器A会将其装饰的方法完整替换成另一种实现,
而另一个装饰器B只是简单的在其装饰的方法中添加点额外逻辑。 那么这时候装饰器A就需要放在装饰器B的前面(也就是A装饰器在最外层)
13:使用元类控制实例的创建 通过改变实例创建方式来实现单例、缓存或其他类似的特性
# 定义了一个类,就能像函数一样的调用它来创建实例 class Spam: def __init__(self, name): self.name = name a = Spam('Guido') b = Spam('Diana')
# 想自定义这个步骤,你可以定义一个元类并自己实现 __call__() 方法 # 假设不想任何人创建这个类的实例 如下 class NoInstances(type): def __call__(self, *args, **kwargs): raise TypeError("Can't instantiate directly") class Spam(metaclass=NoInstances): @staticmethod def grok(x): print('Spam.grok') # 这样写用户只能调用这个类的静态方法,而不能使用通常的方法来创建它的实例。 Spam.grok(42) # Spam.grok s = Spam() # 报错:TypeError: Can't instantiate directly
# 元类:https://www.jianshu.com/p/c1ca0b9c777d type(类名, 由父类名称组成的元组(针对继承的情况,可以为空),包含属性的字典(名称和值)) 创建一个类 第1个实参:类的名称 第2个参数:填写继承父类的名称 第3个参数: 用来设置类属性的 Test2 = type("Test2",(),{}) # type定义了一个Test2类 "Test2"作为类名,并且也可以把它当做一个变量来作为类的引用。类和变量是不同的,这里没有任何理由把事情弄的复杂 Test2() # 创建一个Test2类实例对象
# type创建带有属性的类 # 创建一个胖子老板类,增加属性hobby,添加的属性是类属性,不是实例属性 FatBoss = type('FatBoss', (), {'hobby': "胖子老板卖槟榔"}) print(FatBoss.hobby) # 胖子老板卖槟榔 print(FatBoss.__dict__) # {'hobby': '胖子老板卖槟榔', '__module__': '__main__', # '__dict__': <attribute '__dict__' of 'FatBoss' objects>, # '__weakref__': <attribute '__weakref__' of 'FatBoss' objects>, '__doc__': None} # type创建带有方法的类 # 添加实例方法 def sell(self): print(self.hobby) FatBoss = type('FatBoss', (), {'hobby': "胖子老板卖槟榔"}) print(hasattr(FatBoss, 'hobby')) # True FatBossGril = type('FatBossGril', (FatBoss,), {'sell': sell}) print(hasattr(FatBossGril, 'sell')) # True print(hasattr(FatBossGril, 'hobby')) # True f = FatBossGril() f.sell() # 胖子老板卖槟榔
# 添加静态方法 @staticmethod def static_method(): print("static method...") FatBoss = type('FatBoss', (), {'hobby': "胖子老板卖槟榔"}) FatBossGril = type('FatBossGril', (FatBoss,), {'static_method': static_method}) f = FatBossGril() f.static_method() # static method... FatBossGril.static_method() # static method... # 添加类方法 @classmethod def class_method(cls): print(cls.hobby) FatBoss = type('FatBoss', (), {'hobby': "胖子老板卖槟榔"}) FatBossGril = type('FatBossGril', (FatBoss,), {'class_method': class_method}) f = FatBossGril() f.class_method() # 胖子老板卖槟榔 FatBossGril.class_method() # 胖子老板卖槟榔
# 元类:元类就是用来创建类的“东西”, # 元类就是用来创建这些类(对象)的,元类就是类的类 MyClass = MetaClass() # 使用元类MetaClass创建出一个对象,这个创造的对象称为“类” my_object = MyClass() # type创造一个类 MyClass = type('MyClass', (), {}) # 函数type实际上是一个元类,type就是Python在背后用来创建所有类的元类 # str是用来创建字符串对象的类,int是用来创建整数对象的类,type就是创建类对象的类 # Python中所有的东西,注意,我是指所有的东西——都是对象。这包括整数、字符串、函数以及类。
它们全部都是对象,而且它们都是从一个类创建而来,这个类就是type # 检查class属性 age = 23 print(age.__class__) # <class 'int'> name = '肥仔白' print(name.__class__) # <class 'str'> def sell(): pass print(sell.__class__) # <class 'function'> class A: pass print(A.__class__) # <class 'type'> a = A() print(a.__class__) # <class '__main__.A'> # 从上得知,整型、字符串等等 对象的的类属性
# 类(__class__)的类属性(__class__)是type age = 23 print(age.__class__.__class__) # <class 'type'> name = '肥仔白' print(name.__class__.__class__) # <class 'type'> def sell(): pass print(sell.__class__.__class__) # <class 'type'> class A: pass print(A.__class__.__class__) # <class 'type'> a = A() print(a.__class__.__class__) # <class 'type'> # 从上得知:不管是什么类型的类,最终的创建元类都是type # 元类就是创建类这种对象的东西。type就是Python的内建元类,也可以创建自己的元类
# 元类:__metaclass__属性 # 使用type这个元类来创建类,用__metaclass__属性创建自己的元类 class FatBoss(object): __metaclass__ = something… ...省略... # 这样写代码,Python就会用元类来创建类FatBoss,先写下class Foo(object),
类Foo还没有在内存中创建,Python会在类的定义中寻找__metaclass__属性,如果找到了,
Python就会用它来创建类Foo,如果没有找到,就会用内建的type来创建这个类 class FatBossGril(FatBoss): pass # 上面的class定义 代码做如下的操作: FatBossGril中有__metaclass__这个属性吗?如果有,那么Python会通过__metaclass__创建一个名字为FatBossGril的类(对象) 如果Python没有找到__metaclass__,它会继续在FatBoss(父类)中寻找__metaclass__属性,并尝试做和前面同样的操作。 如果Python在任何父类中都找不到__metaclass__,它就会在模块层次中去寻找__metaclass__,并尝试做同样的操作。 如果还是找不到__metaclass__,Python就会用内置的type来创建这个类对象 # 在__metaclass__中放置些什么代码,可以创建一个类的东西type
或者任何使用到type或者子类化type的类都可以,反正最终还是需要type的
# 自定义元类:函数方式 # 元类的主要目的就是为了当创建类时能够自动地改变类 # 在模块级别设定__metaclass__方法让所有的类的属性都应该是大写形式 # 采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式 def upper_attr(class_name, class_parents, class_attr): # class_name 会保存类的名字 Foo # class_parents 会保存类的父类 object # class_attr 会以字典的方式保存所有的类属性 # 遍历属性字典,把不是__开头的属性名字变为大写 new_attr = {} print("=" * 30) for name, value in class_attr.items(): print("name=%s and value=%s" % (name, value)) # 打印所有类属性出来 if not name.startswith("__"): new_attr[name.upper()] = value print("name.upper()=", name.upper()) print("value=", value) # 调用type来创建一个类 return type(class_name, class_parents, new_attr) class Foo(object, metaclass=upper_attr): # python3 与 2的写法唯一区别 bar = 'bip' print("=" * 30) print(hasattr(Foo, 'bar')) # False print(hasattr(Foo, 'BAR')) # True f = Foo() print(f.BAR) # bip # 使用元类的方式,将Foo类中的属性bar修改为BAR。在这是使用的def 方法来作为类似元类的做法
# 自定义元类:class类方式里面的__new__方法 class UpperAttrMetaClass(type): # __new__ 是在__init__之前被调用的特殊方法 # __new__是用来创建对象并返回之的方法 # 而__init__只是用来将传入的参数初始化给对象 # 你很少用到__new__,除非你希望能够控制对象的创建 # 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__ # 如果你希望的话,你也可以在__init__中做些事情 # 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用 def __new__(cls, class_name, class_parents, class_attr): # 遍历属性字典,把不是__开头的属性名字变为大写 new_attr = {} print("=" * 30) for name, value in class_attr.items(): print("name=%s and value=%s" % (name, value)) # 打印所有类属性出来 if not name.startswith("__"): new_attr[name.upper()] = value print("name.upper()=", name.upper()) print("value=", value) # 调用type来创建一个类 return type(class_name, class_parents, new_attr) class Foo(object, metaclass=UpperAttrMetaClass): bar = 'bip' print("=" * 30) print(hasattr(Foo, 'bar')) # False print(hasattr(Foo, 'BAR')) # True f = Foo() print(f.BAR) # bip
元类本身而言,它们其实是很简单的:
- 拦截类的创建
- 修改类
- 返回修改之后的类
# 自定义元类:class类方式里面的__init__方法 class StructureMeta(type): """ 自动创建StructField描述符的元类 """ # 这个类继承了type元类,那么他就是一个元类,并且在__new__方法里自动调用type方法创建一个类实例 # 并且返回了三个参数clsname, bases, clsdict,init方法里面必填这三个参数,不然报错 # metaclass=StructureMeta,有类指定了这个类是元类的话一定会传递四个参数给元类,不然报错 # init里面的self指代下面调用元类的类 def __init__(self, clsname, bases, clsdict): fields = getattr(self, '_fields_', []) # 得到[('<i', 'file_code'), ('d', 'min_x'), ('d', 'min_y'), # ('d', 'max_x'), ('d', 'max_y'), ('i', 'num_polys')] byte_order = '' offset = 0 for format, fieldname in fields: if format.startswith(('<', '>', '!', '@')): byte_order = format[0] format = format[1:] format = byte_order + format setattr(self, fieldname, StructField(format, offset)) # 设置self类属性 offset += struct.calcsize(format) setattr(self, 'struct_size', offset)
class StructureMeta(type): def __init__(self, clsname, bases, clsdict): print(clsname) # 打印:A clsname是下面A类传递进来的类名称 print(bases) # 打印:(<class '__main__.B'>,) bases是下面A类传递进来的A类的父类,如果除了默认的object外没有继承其他父类那么为空 print(clsdict) # 打印:{'__module__': '__main__', '__qualname__': 'A'} clsdict下面A类传递进来的A类的类内部属性和方法 setattr(self, "name", 'ywt') def __call__(self, *args, **kwargs): raise TypeError("Can't instantiate directly") class B: pass class A(B, metaclass=StructureMeta): pass print(A.__dict__) # {'__module__': '__main__', '__dict__': <attribute '__dict__' of 'A' objects>, # '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None, 'name': 'ywt'} print(A.name) # ywt # a = A() # TypeError: Can't instantiate directly
A类使用StructureMeta这个类作为元类,本质上A这个类对象就是通过StructureMeta元类生成
A=StructureMeta(),A就是通过StructureMeta这个元类实例化生成的,StructureMeta()实例化生成一个
对象A,A()对象实例化的时候会调用StructureMeta()()会调用StructureMeta类的__call__函数,所以raise出错误来
# 使用元类实现单例模式(只能创建唯一实例的类) class Singleton(type): def __init__(self, *args, **kwargs): self.__instance = None # super().__init__(*args, **kwargs) type.__init__(self, *args, **kwargs) # 调用type类的__init__本质上就是type(xxx)类实例化 def __call__(self, *args, **kwargs): if self.__instance is None: # self.__instance = super().__call__(*args, **kwargs) self.__instance = type.__call__(self, *args, **kwargs) return self.__instance else: return self.__instance class Spam(metaclass=Singleton): def __init__(self): print('Creating Spam') # print(Singleton.__mro__) # (<class '__main__.Singleton'>, <class 'type'>, <class 'object'>) a = Spam() b = Spam() print(a is b) # True
class Singleton(type): __instance = None def __call__(self, *args, **kwargs): if self.__instance is None: # self.__instance = super().__call__(*args, **kwargs) self.__instance = type.__call__(self, *args, **kwargs) return self.__instance else: return self.__instance class Spam(metaclass=Singleton): def __init__(self): print('Creating Spam') a = Spam() b = Spam() print(a is b) # True
# 让类Spam实现单例模式这样写也可以,只要构建Spam类的元类Singleton的call方法
因为本质上:
Spam = Singleton()
Spam() = Singleton()() 等同调用Singleton类的__call__方法
第一次Spam()实例化调用__call__方法时候,__instance=None,
所以走if:调用父类type的__call__方法,生成一个实例赋值给 self.__instance
然后返回出去
后面Spam()实例化调用__call__方法时候,__instance这时候不为空了,直接返回__instance=None
本质上python内定义的所有的类都是由type元类生成的:如下
class A:
pass
解释器定义这也一个类的时候内部实现原理是:
A = type()
A() = type()()
type()()本质上就越是调用type类的__call__方法,所有A这个类的实例化A()本质上都是type类的__call__方法构建的
# 使用元类实现缓存实例 import weakref # 弱引用库 class Cached(type): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__cache = weakref.WeakValueDictionary() # 弱引用字典 def __call__(self, *args): if args in self.__cache: return self.__cache[args] else: obj = super().__call__(*args) self.__cache[args] = obj return obj class Spam(metaclass=Cached): def __init__(self, name): print('Creating Spam({!r})'.format(name)) self.name = name a = Spam('Guido') # Creating Spam('Guido') b = Spam('Diana') # Creating Spam('Diana') c = Spam('Guido') # 没有打印 print(a is b) # False print(a is c) # True
# Spam = Cached(),Spam类就指向Cached()这个类的实例化对象,假设这个实例为x
后面Spam() = x()本质上就是调用Cached这个元类的__calll__方法,
每次Spam()实例化得时候查询x实例内__cache这个字典里有没有arg这个元素,
arg就是Spam('Guido')实例化得时候传递参数封装成元组('Guido',)
如果没有arg元素就调用else调用super().__call__(*args)生成实例obj返回
如果有arg就返回字典里面对应的值
# 元类实现多种实例创建模式通常要比不使用元类的方式优雅得多 # 不使用元类,可能需要将类隐藏在某些工厂函数后面 # 实现一个单例模式不使用元类 class _Spam: def __init__(self): print('Creating Spam') _spam_instance = None def Spam(): global _spam_instance if _spam_instance is not None: return _spam_instance else: _spam_instance = _Spam() return _spam_instance
Spam是工厂函数,通过工厂函数来实例化 _Spam类
14:捕获类的属性定义顺序 自动记录一个类中属性和方法定义的顺序, 然后可以利用它来做操作(比如序列化、映射到数据库等等)
# Python元类__prepare__方法 class member_table(dict): def __init__(self): self.member_names = [] def __setitem__(self, key, value): if key not in self: self.member_names.append(key) dict.__setitem__(self, key, value) class OrderedClass(type): @classmethod def __prepare__(cls, name, bases): classdict = member_table() # 重构后的一个字典类member_table,有类的属性也能查看member_names属性 print("prepare return dict id is:", id(classdict)) # 输出:prepare return dict id is: 1485782773248 return classdict def __new__(cls, name, bases, classdict): print("new get dict id is:", id(classdict)) # 输出:new get dict id is: 1485782773248 result = type.__new__(cls, name, bases, dict(classdict)) # 这里使用dict(classdict),把classdict替换成普通的dict,不然会一直记录类里面添加的键值对, # 使用了普通字典就不会记录了,这里的这个dict(classdict)字典就是后面类的存储属性的空间 result.member_names = classdict.member_names # 现在classdict.member_names = ['__module__', '__qualname__', 'method1', 'method2'] print("the class's __dict__ id is:", id(result.__dict__)) return result def __init__(cls, name, bases, classdict): print("init get dict id is ", id(classdict)) super().__init__(name, bases, classdict) class MyClass(metaclass=OrderedClass): def method1(self): pass def method2(self): pass print("MyClass locals() id is ", id(locals())) # 输出:MyClass locals() id is 1485782773248 # 执行顺序: # 元类OrderedClass调用prepare(创建命名空间)-> 依次执行MyClass类内部定义语句 -> # 元类OrderedClass调用new(创建类)-> 元类OrderedClass调用init(初始化类) # 元类定义了prepare以后,会最先执行prepare方法,返回一个空的定制的字典, # 然后再执行类的语句,类中定义的各种属性被收集入定制的字典, # 最后传给new和init方法 print(MyClass.member_names) # ['__module__', '__qualname__', 'method1', 'method2'] MyClass.attr1 = 'attr1' print(MyClass.__dict__) # {'__module__': '__main__', 'method1': <function MyClass.method1 at 0x00000217D6CEC700>, # 'method2': <function MyClass.method2 at 0x00000217D6CEC790>, # '__dict__': <attribute '__dict__' of 'MyClass' objects>, # '__weakref__': <attribute '__weakref__' of 'MyClass' objects>, # '__doc__': None, 'member_names': ['__module__', '__qualname__', 'method1', 'method2'], 'attr1': 'attr1'} print(id(MyClass.__dict__)) # 2780863860064 print(MyClass.member_names) # ['__module__', '__qualname__', 'method1', 'method2'] # 在new方法中,dict被替换成一个普通的dict。所以MyClass.member_names不会记录class创建以后新增的属性。 # 同时__dict__属性是类命名空间的一个代理,每次查看其id都不同 # 3.6版本以前,prepare方法主要用来返回一个orderdict对象, # 以保存类中属性的添加顺序。而3.6版本以后,默认已经是保持顺序的了
# 利用元类可以很容易的捕获类的定义信息
from collections import OrderedDict # 各种类型的一组描述符class Typed: _expected_type = type(None) def__init__(self, name=None): self._name = name def__set__(self, instance, value): ifnot isinstance(value, self._expected_type): raise TypeError('Expected ' + str(self._expected_type)) instance.__dict__[self._name] = value class Integer(Typed): _expected_type = int class Float(Typed): _expected_type = float class String(Typed): _expected_type = str # 使用OrderedDict作为类主体的元类class OrderedMeta(type): @classmethod def__prepare__(cls, clsname, bases): return OrderedDict() def__new__(cls, clsname, bases, clsdict): d = dict(clsdict) order = [] # 遍历有序字典clsdict,如果有序字典里面的值使用Typed的实例,那么order列表把元素添加进去for name, value in clsdict.items(): if isinstance(value, Typed): value._name = name
# 这里再每个value对象添加了 "_name" = name这个键值对可以给描述器instance.__dict__[self._name] = value后面使用 order.append(name) d['_order'] = order # 往后面类的命名空间里添加_order = order这个键值对属性 # order = ['name', 'shares', 'price']
return type.__new__(cls, clsname, bases, d) # 在这个元类中,执行类主体时描述器的定义顺序会被一个 OrderedDict 捕获到, # 生成的有序名称从字典中提取出来并放入类属性 _order 中。这样的话类中的方法可以通过多种方式来使用它。 # 例如,下面是一个简单的类,使用这个排序字典来实现将一个类实例的数据序列化为一行CSV数据 class Structure(metaclass=OrderedMeta): def as_csv(self): # self._order = ['name', 'shares', 'price'],遍历这个列表找值, # self实例化后内部存储了字典:s.__dict__ = {'name': 'GOOG', 'shares': 100, 'price': 490.1} # 所以返回的是键对应的值:GOOG,100,490.1return','.join(str(getattr(self, name)) for name in self._order) class Stock(Structure): name = String() shares = Integer() price = Float() def__init__(self, name, shares, price): self.name = name self.shares = shares self.price = price s = Stock('GOOG', 100, 490.1) print(s.name) # GOOGprint(s.as_csv()) # GOOG,100,490.1 # t = Stock('AAPL', 'a lot', 610.23) # 报错:TypeError: Expected <class 'int'> # 类Structure使用OrderedMeta作为元类,类Stock继承Structure,所以Stock和Structure类的生成都会调用元类OrderedMeta
# OrderedMeta元类中定义的 __prepare__() 方法。 这个方法会在开始定义类和它的父类的时候被执行。
它必须返回一个映射对象以便在类定义体中被使用到。 我们这里通过返回了一个OrderedDict(有序字典)而不是一个普通的字典,可以很容易的捕获定义的顺序 # 可以构造自己的类字典对象扩展这个功能。比如,下面的这个修改方案可以防止重复的定义 from collections import OrderedDict class NoDupOrderedDict(OrderedDict): def __init__(self, clsname): self.clsname = clsname super().__init__() def __setitem__(self, name, value): if name in self: raise TypeError('{} already defined in {}'.format(name, self.clsname)) super().__setitem__(name, value) class OrderedMeta(type): def __new__(cls, clsname, bases, clsdict): d = dict(clsdict) d['_order'] = [name for name in clsdict if name[0] != '_'] return type.__new__(cls, clsname, bases, d) @classmethod def __prepare__(cls, clsname, bases): return NoDupOrderedDict(clsname) class A(metaclass=OrderedMeta): def spam(self): pass def spam(self): pass
# 报错:TypeError: spam already defined in A
# 使用OrderedMeta这个类作为元类的所有的类定义同名函数或者同名字属性都会报错
# __new__() 方法中对于元类中被修改字典的处理。 尽管类使用了另外一个字典来定义,
在构造最终的 class 对象的时候, 我们仍然需要将这个字典转换为一个正确的 dict 实例。
通过语句 d = dict(clsdict) 来完成这个效果 # 对于很多应用程序而已,能够捕获类定义的顺序是一个看似不起眼却又非常重要的特性。
例如,在对象关系映射中,我们通常会看到下面这种方式定义的类 class Stock(Model): name = String() shares = Integer() price = Float() # 在框架底层,我们必须捕获定义的顺序来将对象映射到元组或数据库表中的行(就类似于上面例子中的 as_csv() 的功能)。
这节演示的技术非常简单,并且通常会比其他类似方法(通常都要在描述器类中维护一个隐藏的计数器)要简单的多
15:定义有可选参数的元类 定义一个元类,允许类定义时提供可选参数,这样可以控制或配置类型的创建过程
# 定义类的时候,Python允许我们使用 ``metaclass``关键字参数来指定特定的元类 # 使用抽象基类 from abc import ABCMeta, abstractmethod class IStream(metaclass=ABCMeta): @abstractmethod def read(self, maxsize=None): pass @abstractmethod def write(self, data): pass # 为了使元类支持这些关键字参数,你必须确保在 __prepare__() , __new__() 和 __init__() 方法中 都使用强制关键字参数 class MyMeta(type): # 要求的 @classmethod def __prepare__(cls, name, bases, *, debug=False, synchronize=False): # 定制处理 pass return super().__prepare__(name, bases) # 要求的 def __new__(cls, name, bases, ns, *, debug=False, synchronize=False): # 定制处理 pass return super().__new__(cls, name, bases, ns) # 要求的 def __init__(self, name, bases, ns, *, debug=False, synchronize=False): # 定制处理 pass super().__init__(name, bases, ns) # 定义元类中我们还可以提供其他的关键字参数 class Spam(metaclass=MyMeta, debug=True, synchronize=True): pass
# 给一个元类添加可选关键字参数需要你完全弄懂类创建的所有步骤, 因为这些参数会被传递给每一个相关的方法。
__prepare__() 方法在所有类定义开始执行前首先被调用,用来创建类命名空间。
通常来讲,这个方法只是简单的返回一个字典或其他映射对象。
__new__() 方法被用来实例化最终的类对象。
它在类的主体被执行完后开始执行。
__init__() 方法最后被调用,用来执行其他的一些初始化工作
# 当我们构造元类的时候,通常只需要定义一个 __new__() 或 __init__() 方法,但不是两个都定义。
但是,如果需要接受其他的关键字参数的话,这两个方法就要同时提供,并且都要提供对应的参数签名。
默认的 __prepare__() 方法接受任意的关键字参数,但是会忽略它们,
所以只有当这些额外的参数可能会影响到类命名空间的创建时你才需要去定义 __prepare__() 方法
# 过使用强制关键字参数,在类的创建过程中我们必须通过关键字来指定这些参数。
# 使用关键字参数配置一个元类还可以视作对类变量的一种替代方式 class Spam(metaclass=MyMeta): debug = True synchronize = True pass # 将这些属性定义为参数的好处在于它们不会污染类的名称空间, 这些属性仅仅只从属于类的创建阶段,而不是类中的语句执行阶段。
另外,它们在 __prepare__() 方法中是可以被访问的,因为这个方法会在所有类主体执行前被执行。
但是类变量只能在元类的 __new__() 和 __init__() 方法中可见
16:*args和**kwargs的强制参数签名 一个函数或方法,使用*args和**kwargs作为参数,这样使得它比较通用, 有时想检查传递进来的参数是不是某个你想要的类型。
# 对任何涉及到操作函数调用签名的问题,使用 inspect 模块中的签名特性。
主要使用两个类:Signature 和 Parameter from inspect import Signature, Parameter params = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD), Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42), Parameter('z', Parameter.KEYWORD_ONLY, default=None)] sig = Signature(params) # print(sig, type(sig)) # (x, y=42, *, z=None) <class 'inspect.Signature'> # 有了一个签名对象,你就可以使用它的 bind() 方法很容易的将它绑定到 *args 和 **kwargs 上去 def func(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): print(name, value, end=',') print('') func(1, 2, z=3) # 输出:x 1,y 2,z 3, func(1) # 输出:x 1, func(1, z=3) # 输出:x 1,z 3, func(y=2, x=1) # 输出:x 1,y 2, # func(1, 2, 3, 4) # 报错:TypeError: too many positional arguments # func(y=2) # 报错:TypeError: missing a required argument: 'x' # func(1, y=2, x=3) # 报错:TypeError: multiple values for argument 'x' # 通过将签名和传递的参数绑定起来,可以强制函数调用遵循特定的规则,比如必填、默认、重复等等
这里函数内部bound_values = sig.bind(*args, **kwargs)把前面对象绑定到*args 和 **kwargs 上去
# 强制函数签名 # 基类中先定义了一个非常通用的 __init__() 方法, 然后我们强制所有的子类必须提供一个特定的参数签名 from inspect import Signature, Parameter import inspect def make_sig(*names): parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names] return Signature(parms) # (name, shares, price) class Structure: __signature__ = make_sig() def __init__(self, *args, **kwargs): # self.__signature__ = (name, shares, price) # bound_values = <BoundArguments (name=1, shares=2, price=3)> bound_values = self.__signature__.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): setattr(self, name, value) class Stock(Structure): __signature__ = make_sig('name', 'shares', 'price') # (name, shares, price) class Point(Structure): __signature__ = make_sig('x', 'y') # (x, y) print(inspect.signature(Stock)) # (name, shares, price) s1 = Stock('ACME', 100, 490.1) # s2 = Stock('ACME', 100) # 报错:TypeError: missing a required argument: 'price'、 # s3 = Stock('ACME', 100, 490.1, shares=50) # 报错:TypeError: multiple values for argument 'shares'
# Signature的用法 from inspect import Signature, Parameter, signature params = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD), Parameter('y', Parameter.POSITIONAL_OR_KEYWORD), Parameter('z', Parameter.POSITIONAL_OR_KEYWORD)] sig = Signature(params) sig.bind(1, 2, 3) # sig.bind(1, 2) # 报错:TypeError: missing a required argument: 'z' # sig.bind绑定参数必须要sig里面绑定的属性和传递进来的参数想符合规则才不报错
# 构建通用函数库、编写装饰器或实现代理的时候,对于 *args 和 **kwargs 的使用是很普遍的。
但是,这样的函数有一个缺点就是当你想要实现自己的参数检验时,代码就会笨拙混乱。
# 自定义元类来创建签名对象 from inspect import Signature, Parameter def make_sig(*names): parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in names] return Signature(parms) class StructureMeta(type): def __new__(cls, clsname, bases, clsdict):
# clsdict.get('_fields', []) # 从clsdict字典里获取_fields这个键对应的值,如果获取不到返回[]空字典
# 在clsdict这个类的内存空间里添加:__signature__ = (),(name, shares, price), (x, y)
# 那么元类生成类对象的时候生成的类里的内存空间里有__signature__ = (),(name, shares, price), (x, y)这样的键值对
clsdict['__signature__'] = make_sig(*clsdict.get('_fields',[])) return super().__new__(cls, clsname, bases, clsdict) class Structure(metaclass=StructureMeta): _fields = [] def __init__(self, *args, **kwargs): bound_values = self.__signature__.bind(*args, **kwargs) for name, value in bound_values.arguments.items(): setattr(self, name, value) class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): _fields = ['x', 'y'] # 自定义签名的时候,将签名存储在特定的属性 __signature__ 中通常是很有用的。
这样的话,在使用 inspect 模块执行内省的代码就能发现签名并将它作为调用约定 import inspect print(inspect.signature(Stock)) # (name, shares, price) print(inspect.signature(Point)) # (x, y)
17:在类上强制使用编程规约 程序包含一个很大的类继承体系,需要强制执行某些编程规约(或者代码诊断)来帮助程序员保持清醒
# 监控类的定义,通常可以通过定义一个元类。 # 本元类通常是继承自 type 并重定义它的 __new__() 方法 或者是 __init__() 方法 # 定义基类的第一种方法:定义__new__ class MyMeta(type): def __new__(self, clsname, bases, clsdict): # clsname是正在定义的类的名称 # bases是基类的元组 # clsdict是类属性字典 return super().__new__(cls, clsname, bases, clsdict) # 定义基类的第二种方法:定义 __init__() class MyMeta(type): def __init__(self, clsname, bases, clsdict): super().__init__(clsname, bases, clsdict) # clsname是正在定义的类的名称 # bases是基类的元组 # clsdict是类属性字典 # 使用元类,需要将元类放到一个顶级父类定义中,然后其他的类继承这个顶级父类 class Root(metaclass=MyMeta): pass class A(Root): pass class B(Root): pass
# 元类的一个关键特点是它允许你在定义的时候检查类的内容。在重新定义 __init__() 方法中, 你可以很轻松的检查类字典、父类等等。并且,一旦某个元类被指定给了某个类,那么就会被继承到所有子类中去。 因此,一个框架的构建者就能在大型的继承体系中通过给一个顶级父类指定一个元类去捕获所有下面子类的定义 # 定义了一个元类,拒绝任何有混合大小写名字作为方法的类定义 # __new__方法 class NoMixedCaseMeta(type): def __new__(cls, clsname, bases, clsdict): for name in clsdict: if name.lower() != name: raise TypeError('Bad attribute name: ' + name) return super().__new__(cls, clsname, bases, clsdict) class Root(metaclass=NoMixedCaseMeta): pass class A(Root): def foo_bar(self): # Ok pass class B(Root): def fooBar(self): # TypeError pass # __init__方法 class NoMixedCaseMeta(type): def __init__(self, clsname, bases, clsdict): for i, j in clsdict.items(): if not i.startswith('__'): if i.lower() != i: raise TypeError('Bad attribute name: ' + i) super().__init__(clsname, bases, clsdict) class Root(metaclass=NoMixedCaseMeta): pass class A(Root): def foo_bar(self): # Ok pass class B(Root): def fooBar(self): # TypeError pass
# Python super() 函数 super() 函数是用于调用父类(超类)的一个方法。 super(type[, object-or-type]) type -- 类。 object-or-type -- 类,一般是 self class A: @classmethod def pin(cls): print('A') class B(A): @classmethod def pin(cls): print('B') print(super(B, B)) # <super: <class 'B'>, <B object>> super(B, B).pin() # A # 返回当前B类的父类,就是返回A类这个对象
# 使用元类检测重载方法,确保它的调用参数跟父类中原始方法有着相同的参数签名 # 定义元类,检测重载方法,确保它的调用参数跟父类中原始方法有着相同的参数签名 from inspect import signature import logging class MatchSignaturesMeta(type): def __init__(self, clsname, bases, clsdict): super().__init__(clsname, bases, clsdict) sup = super(self, self) # super(self, self)得到的是当前self的父类(超类) print(clsdict) for name, value in clsdict.items(): # 如果不是以_开头和不可以调用的对象的话直接continue跳过本次循环继续下一次 if name.startswith('_') or not callable(value): continue # 获取以前的定义(如果有)并比较签名,sup是当前类的父类,从父类获取本类里面定义的函数比如'foo',获取不到从父类找,找到object还是找不到返回None prev_dfn = getattr(sup, name, None) if prev_dfn: # 如果获取到了 prev_sig = signature(prev_dfn) # signature(父类的函数)的参数标签 val_sig = signature(value) # signature(子类的函数)的参数标签 if prev_sig != val_sig: # 如果父类和子类有同名方法而且方法的参数标签还不相同就打印logging.warning日志 logging.warning('Signature mismatch in %s. %s != %s', value.__qualname__, prev_sig, val_sig) class Root(metaclass=MatchSignaturesMeta): pass class A(Root): def foo(self, x, y): pass def spam(self, x, *, z): pass # 使用重新定义的方法初始化,但签名略有不同 class B(A): def foo(self, a, b): pass def spam(self, x, z): pass
输出:
WARNING:root:Signature mismatch in B.spam. (self, x, *, z) != (self, x, z)
WARNING:root:Signature mismatch in B.foo. (self, x, y) != (self, a, b)
#如果父类和字类有同名方法而且方法的参数标签还不相同就打印logging.warning日志
# 这种警告信息对于捕获一些微妙的程序bug是很有用的。
# 例如,如果某个代码依赖于传递给方法的关键字参数, 那么当子类改变参数名字的时候就会调用出错
# 大型面向对象的程序中,通常将类的定义放在元类中控制是很有用的。
元类可以监控类的定义,警告编程人员某些没有注意到的可能出现的问题。
# 元类中选择重新定义 __new__() 方法还是 __init__() 方法取决于你想怎样使用结果类。
__new__() 方法在类创建之前被调用,通常用于通过某种方式(比如通过改变类字典的内容)修改类的定义。
__init__() 方法是在类被创建之后被调用,当你需要完整构建类对象的时候会很有用。
在最后一个例子中,这是必要的,因为它使用了 super() 函数来搜索之前的定义。
它只能在类的实例被创建之后,并且相应的方法解析顺序也已经被设置好了
# Python的函数签名对象的使用。 实际上,元类将每个可调用定义放在一个类中,搜索前一个定义(如果有的话),
然后通过使用 inspect.signature() 来简单的比较它们的调用签名
# super(self, self) 并不是排版错误。 当使用元类的时候,我们要时刻记住一点就是 self 实际上是一个类对象。
因此,这条语句其实就是用来寻找位于继承体系中构建 self 父类的定义
18:以编程方式定义类 类的定义源代码以字符串的形式发布出去。 并且使用函数比如 exec()
来执行它,这里使用一种更加优雅的方式
# 使用函数 types.new_class() 来初始化新的类对象。
你需要做的只是提供类的名字、父类元组、关键字参数,以及一个用成员变量填充类字典的回调函数 import types # 从零件手动生成类的示例 def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price cls_dict = { '__init__': __init__, 'cost': cost, } Stock = types.new_class('Stock', (), {}, lambda ns: ns.update(cls_dict)) Stock.__module__ = __name__ # 这种方式会构建一个普通的类对象,按照期望工作: s = Stock('ACME', 50, 91.1) print(s) # <__main__.Stock object at 0x000001E2E2645CA0> print(s.cost()) # 4555.0 # 调用完 types.new_class() 对 Stock.__module__ 的赋值。 每次当一个类被定义后,
它的 __module__ 属性包含定义它的模块名。 这个名字用于生成 __repr__() 方法的输出。
它同样也被用于很多库,比如 pickle 。 因此,为了让你创建的类是“正确”的,你需要确保这个属性也设置正确了
# 创建的类需要一个不同的元类,可以通过 types.new_class() 第三个参数传递给它 import abc import types def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price def cost(self): return self.shares * self.price cls_dict = { '__init__': __init__, 'cost': cost, } Stock = types.new_class('Stock', (), {'metaclass': abc.ABCMeta}, lambda ns: ns.update(cls_dict)) Stock.__module__ = __name__ print(Stock) # <class '__main__.Stock'> print(type(Stock)) # <class 'abc.ABCMeta'> s = Stock('ACME', 50, 91.1) print(s.cost())
# 第三个参数字典指定不同的元类
# types.new_class第三个参数还可以包含其他的关键字参数 class Spam(Base, debug=True, typecheck=False): pass Spam = types.new_class('Spam', (Base,), {'debug': True, 'typecheck': False}, lambda ns: ns.update(cls_dict))types.
new_class
(name, bases=(), kwds=None, exec_body=None)
使用适当的元类动态地创建一个类对象。
前三个参数是组成类定义头的部件:类名称,基类 (有序排列),关键字参数 (例如 metaclass
)。
exec_body 参数是一个回调函数,用于填充新创建类的命名空间。
exec_body应当接受类命名空间作为其唯一的参数并使用类内容直接更新命名空间。 如果未提供回调函数,则它就等效于传入 lambda ns: None
# new_class() 第四个参数最神秘,它是一个用来接受类命名空间的映射对象的函数。
通常这是一个普通的字典,但是它实际上是 __prepare__() 方法返回的任意对象,
这个函数需要使用上面演示的 update() 方法给命名空间增加内容
# Python中9种生成新对象的方法 class Point: def __init__(self, x, y): self.x = x self.y = y # 9种方法来生成新的对象 point1 = Point(1, 2) point2 = eval("{}({}, {})".format("Point", 1, 2)) point3 = globals()["Point"](1, 2) point4 = locals()["Point"](1, 2) point5 = getattr(sys.modules[__name__], "Point")(1, 2) point6 = copy.deepcopy(point1) point7 = point1.__class__(1, 2) point8 = type('Point', (Point, ), {})(1, 2) # 重新定义Point类,并且继承了以前的Point类
point9 = types.new_class('Point', (Point, ), {})(1, 2)
# 构造新的类对象是很有用的。 # 调用 collections.namedtuple() 函数 import collections Stock = collections.namedtuple('Stock', ['name', 'shares', 'price']) # print(Stock) # <class '__main__.Stock'> s = Stock('111', '222', '333') print(s) # Stock(name='111', shares='222', price='333') 元组是不可变的对象,不可增删改,只能查看 # 上面使用的类似创建一个新类,Stock, # namedtuple() 使用 exec()来实现的
# 创建一个类 import operator import types import sys def named_tuple(classname, fieldnames): # 填充字段属性访问器的字典 cls_dict = {name: property(operator.itemgetter(n)) for n, name in enumerate(fieldnames)} # 创建一个__new__ 函数并添加到类dict中 def __new__(cls, *args): if len(args) != len(fieldnames): raise TypeError('Expected {} arguments'.format(len(fieldnames))) return tuple.__new__(cls, args) cls_dict['__new__'] = __new__ # 创建类 cls = types.new_class(classname, (tuple,), {}, lambda ns: ns.update(cls_dict)) # 将模块设置为调用者的模块 cls.__module__ = sys._getframe(1).f_globals['__name__'] return cls # 最后部分使用了”框架魔法”,通过调用 sys._getframe() 来获取调用者的模块名,堆栈 Point = named_tuple('Point', ['x', 'y']) print(Point) # <class '__main__.Point'> p = Point(4, 5) print(len(p)) # 2 print(p.x) # 4 print(p.y) # 5 # p.x = 2 # AttributeError: can't set attribute
# 通过直接实例化一个元类来直接创建一个类: Stock = type('Stock', (), cls_dict) # type是一个类,这里表示type类的实例化 # 种方法的问题在于它忽略了一些关键步骤,比如对于元类中 __prepare__() 方法的调用。
通过使用 types.new_class() ,你可以保证所有的必要初始化步骤都能得到执行。
比如,types.new_class() 第四个参数的回调函数接受 __prepare__() 方法返回的映射对象 # 如果只是想执行准备步骤,可以使用 types.prepare_class() import types metaclass, kwargs, ns = types.prepare_class('Stock', (), {'metaclass': type}) # 它会查找合适的元类并调用它的 __prepare__() 方法。 然后这个元类保存它的关键字参数,准备命名空间后被返回
# property() 函数的作用是在新式类中返回属性值。 class property([fget[, fset[, fdel[, doc]]]]) fget -- 获取属性值的函数 fset -- 设置属性值的函数 fdel -- 删除属性值函数 doc -- 属性描述信息 f返回值:返回新式类属性。 class C(object): def __init__(self): self._x = None def getx(self): return self._x def setx(self, value): self._x = value def delx(self): del self._x x = property(getx, setx, delx, "I'm the 'x' property.") # 如果 c 是 C 的实例化, c.x 将触发 getter,c.x = value 将触发 setter , del c.x 触发 deleter。 如果给定 doc 参数,其将成为这个属性值的 docstring,否则 property 函数就会复制 fget 函数的 docstring(如果有的话)。 # 将 property 函数用作装饰器可以很方便的创建只读属性: class Parrot(object): def __init__(self): self._voltage = 100000 @property def voltage(self): """Get the current voltage.""" return self._voltage 将 voltage() 方法转化成同名只读属性的 getter 方法。 # property 的 getter,setter 和 deleter 方法同样可以用作装饰器: class C(object): def __init__(self): self._x = None @property def x(self): """I'm the 'x' property.""" return self._x @x.setter def x(self, value): self._x = value @x.deleter def x(self): del self._x
# python3中 operator模块用法介绍:https://blog.csdn.net/u010339879/article/details/98304292 # operator模块attrgetter类
# attrgetter 可以 获取 对象的属性, 然后进行 排序 操作 .
from operator import attrgetter class Student: pass def __init__(self, name, age, score): self.name = name self.age = age self.score = score def __str__(self): return '%s(name=%r,age=%r,score=%r)' % (self.__class__.__name__, self.name, self.age, self.score) __repr__ = __str__ std1 = Student("A", 11, 23) std2 = Student("B", 13, 10) std3 = Student("C", 16, 15) std4 = Student("D", 34, 4) st = [std1, std2, std3, std4] print(attrgetter('name')(std1)) # A st.sort(key=lambda x: x.score) print(st) print(sorted(st, key=attrgetter('age')))
# operator模块itemgetter类 # attrgetter 返回 一个 可调用对象 ,
使用操作数的__getitem__()方法从其操作数获取项的可调用对象。如果指定了多个项,则返回查找值的元组 from operator import itemgetter data = [ ("frank", 10), ("frank1", 15), ("frank2", 19), ("frank3", 12), ("frank4", 17), ("frank5", 11), ("frank6", 18), ] data.sort(key=itemgetter(1), reverse=True) print(data) # [('frank2', 19), ('frank6', 18), ('frank4', 17), ('frank1', 15), ('frank3', 12), ('frank5', 11), ('frank', 10)] for i in data: print(itemgetter(1)(i), end=',') # 19,18,17,15,12,11,10,
itemgetter(1)('ABCDEFG') # 'B'
itemgetter(1, 3, 5)('ABCDEFG') # ('B', 'D', 'F')
itemgetter(slice(2, None))('ABCDEFG') # 'CDEFG'
inventory = [('apple', 3), ('banana', 2), ('pear', 5), ('orange', 1)]
getcount = itemgetter(1)
list(map(getcount, inventory)) # [3, 2, 5, 1]
sorted(inventory, key=getcount) # [('orange', 1), ('banana', 2), ('apple', 3), ('pear', 5)]
# operator模块methodcaller类 # operator.methodcaller() 创建一个可调用对象,并同时提供所有必要参数,
然后调用的时候只需要将实例对象传递给它即可, import math from operator import methodcaller class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return 'Point({0},{1})'.format(self.x, self.y) def distance(self, x, y): return math.hypot(self.x - x, self.y - y) d = methodcaller('distance', 0, 0)(Point(3, 4)) print(d) # 5.0 # 等同如下 d = Point(3, 4).distance(0, 0) print(d) # 5.0
# Python的property函数:https://blog.csdn.net/dxk_093812/article/details/83212231 # property:改变访问对象属性得方式,更加便捷访问属性 class Test: def __init__(self): self._x = None def get_x(self): print("I'm the getter method") return self._x def set_x(self, value): print("I'm the setter method") self._x = value def del_x(self): print("I'm the deleter method") del self._x # 设置doc参数 x = property(get_x, set_x, del_x, "I'm the 'x' property.") t = Test() t.x = '100' # 等同于t.set_x("100") print(t.x) # 等同于print(t.get_x()) t.x = '90' print(t.x)
property(fget=None, fset=None, fdel=None, doc=None) fget是获取属性值的函数, fset是设置属性值的函数, fdel是删除属性的函数, doc是一个文档字符串。property函数的参数都是可选的,若没有参数,产生的属性既不可读也不可写。若只有一个取值方法,产生的属性是只读的 property并不是一个真正的函数,它是拥有许多特殊方法的类:__get__、__set__、__del__。
这三个方法合在一起定义了描述符规则。实现了任何一个方法的对象就可称为描述符(descriptor)。
当程序读取一个特性时,若该特性被绑定到实现了__get__方法的对象上时,那么就会调用__get__方法 如上所示,我们还是实现getter、setter方法,只需要加入property的语句就能实现特性的简单访问。 # property除了上述这种用法外,还可以使用@property实现相同的功能 class Test: def __init__(self): self._x = None @property def x(self): print("I'm the getter method") return self._x @x.setter def x(self, value): print("I'm the setter method") self._x = value @x.deleter def x(self): print("I'm the deleter method") del self._x t = Test() t.x = '100' print(t.x) t.x = '90' print(t.x) # 运行结果和上述property()函数一样。 把get方法变为属性只需加上@property装饰器,
然后@property装饰器又创建另外的装饰器@x.setter(用于把setter方法变成属性赋值)
和@x.deleter。我们也可以自只定义getter方法而不定义setter方法,让属性变为只读。
# 创建一个类过程详解 import operator import types import sys def named_tuple(classname, fieldnames): # 填充字段属性访问器的字典 cls_dict = {name: property(operator.itemgetter(n)) for n, name in enumerate(fieldnames)} # {'x': <property object at 0x000001E9DC5E04A0>, 'y': <property object at 0x000001E9DC125720>} # 这里相当于定义了一个字典:前面是x函数名称,后面是函数体operator.itemgetter(0)函数,这个函数需要后面加一个可迭代得对象才能运行 # 比如operator.itemgetter(0)((1, 2)), # property(operator.itemgetter(n)相当于使用@property装饰了这个函数,使得只要外面对象调用p.x就会直接调用operator.itemgetter(0)这个函数 # types.new_class(classname, (tuple,), {}, lambda ns: ns.update(cls_dict))这里生成类得使用使用了cls_dict这个字典 # 那么生成得cls类内部会有'x': <property object at 0x000001E9DC5E04A0>这个键值对,等于有def x这个函数 # x函数本质上是被装饰器@property装饰得operator.itemgetter(0)这个函数并且绑定了第一个参数是self # 等同operator.itemgetter(0)(self)等同返回self这个对象的第一个元素, # 因为cls对象继承了tuple所以本质上return cls本质上返回的类也是元组类,实例化之后也是元组对象, # 可以operator.itemgetter(0)(self)这也对本身取值 # type和types.new_class实例化的时候参考下面 # def sell(zzz, a): # print(zzz.hobby, a) # FatBoss = type('FatBoss', (), {'hobby': "胖子老板卖槟榔", "yell": sell}) # f = FatBoss() # f.yell(10) # 胖子老板卖槟榔 这也yell是实例方法 # 创建一个__new__ 函数并添加到类dict中 def __new__(cls, *args): if len(args) != len(fieldnames): raise TypeError('Expected {} arguments'.format(len(fieldnames))) return tuple.__new__(cls, args) cls_dict['__new__'] = __new__ # 创建类 cls = types.new_class(classname, (tuple,), {}, lambda ns: ns.update(cls_dict)) # 将模块设置为调用者的模块 cls.__module__ = sys._getframe(1).f_globals['__name__'] return cls # 最后部分使用了”框架魔法”,通过调用 sys._getframe() 来获取调用者的模块名,堆栈 Point = named_tuple('Point', ['x', 'y']) # print(Point.__dict__) # {'x': operator.itemgetter(0), 'y': operator.itemgetter(1), # '__new__': <staticmethod object at 0x0000014E360AFBB0>, # '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Point' objects>, '__doc__': None} print(Point) # <class '__main__.Point'> p = Point(4, 5) print(len(p)) # 2 print(p.x) # 4 print(p.y) # 5 # p.x = 2 # AttributeError: can't set attribute
本质上是 cls_dict = {name: property(operator.itemgetter(n)) for n, name in enumerate(fieldnames)} 这个创建键值字典
对然types.new_class(classname, (tuple,), {}, lambda ns: ns.update(cls_dict)) 把cls_dict这个字典里面的键值对写到类里面
写方法或者属性都可以,写的时候指定函数名称 x,指定函数体operator.itemgetter(0)(self),并且对这个函数使用@property装饰器
所以外面self实例调用x,p.x会直接调用operator.itemgetter(0)(self)这个函数,p.x = xxx报错,因为被@property装饰器装饰的函数只能查看属性
函数当作方法来使用
# f_globals
import sys
def func():
print(sys._getframe(1).f_globals['__name__']) # 查看堆栈
func() # __main__
print(sys._getframe(0).f_globals['__name__']) # __main__
# 一个很重要的方面是它对于元类的正确使用。 可能像通过直接实例化一个元类来直接创建一个类 Stock = type('Stock', (), cls_dict) 上面的实例化是: Point = named_tuple('Point', ['x', 'y']) # 上面方法的问题在于它忽略了一些关键步骤,比如对于元类中 __prepare__() 方法的调用。 # 通过使用 types.new_class() ,你可以保证所有的必要初始化步骤都能得到执行。
比如,types.new_class() 第四个参数的回调函数接受 __prepare__() 方法返回的映射对象,可以操作映射对象 # 仅仅只是想执行准备步骤,可以使用 types.prepare_class() import types metaclass, kwargs, ns = types.prepare_class('Stock', (), {'metaclass': type}) # 它会查找合适的元类并调用它的 __prepare__() 方法。 然后这个元类保存它的关键字参数,准备命名空间后被返回。
19:在定义的时候初始化类的成员 类被定义的时候就初始化一部分类的成员,而不是要等到实例被创建后
# 类定义时就执行初始化或设置操作是元类的一个典型应用场景 # 一个元类会在定义时被触发, 这时候你可以执行一些额外的操作 # 创建类似于 collections 模块中的命名元组的类 import operator class StructTupleMeta(type): def __init__(cls, *args, **kwargs): super().__init__(*args, **kwargs) for n, name in enumerate(cls._fields): setattr(cls, name, property(operator.itemgetter(n))) class StructTuple(tuple, metaclass=StructTupleMeta): _fields = [] def __new__(cls, *args): # 创建实例self的,外部s = Stock('ACME', 50, 91.1)实例化的时候才会调用这个__new__方法 if len(args) != len(cls._fields): raise ValueError('{} arguments required'.format(len(cls._fields))) return super().__new__(cls, args) # 这段代码可以用来定义简单的基于元组的数据结构, class Stock(StructTuple): _fields = ['name', 'shares', 'price'] class Point(StructTuple): _fields = ['x', 'y'] # s = Stock('ACME', 50, 91.1) # print(s) # ('ACME', 50, 91.1) # print(s[0]) # ACME # print(s.name) # ACME # print(s.shares * s.price) # 4555.0 # s.shares = 23 # AttributeError: can't set attribute
# 类 StructTupleMeta 获取到类属性 _fields 中的属性名字列表,
然后将它们转换成相应的可访问特定元组槽的方法。函数 operator.itemgetter() 创建一个访问器函数,
然后 property() 函数将其转换成一个属性 # StructTupleMeta 中的 __init__() 方法只在每个类被定义时被调用一次。
cls 参数就是那个被定义的类。实际上,上述代码使用了 _fields 类变量来保存新的被定义的类, 然后给它再添加一点新的东西 # StructTuple 类作为一个普通的基类,供其他使用者来继承。 这个类中的 __new__() 方法用来构造新的实例。
这里使用 __new__() 并不是很常见,主要是因为我们要修改元组的调用签名, 使得我们可以像普通的实例调用那样创建实例 s = Stock('ACME', 50, 91.1) # OK s = Stock(('ACME', 50, 91.1)) # Error # 跟 __init__() 不同的是,__new__() 方法在实例被创建之前被触发。
由于元组是不可修改的,所以一旦它们被创建了就不可能对它做任何改变。
而 __init__() 会在实例创建的最后被触发, 这样的话我们就可以做我们想做的了。
这也是为什么 __new__() 方法已经被定义了
20:利用函数注解实现方法重载 参数注解
# Python允许参数注解 class Spam: def bar(self, x: int, y: int): print('Bar 1:', x, y) def bar(self, s: str, n: int = 0): print('Bar 2:', s, n) s = Spam() s.bar(2, 3) # Prints Bar 1: 2 3 s.bar('hello') # Prints Bar 2: hello 0
# 元类+描述器 import inspect import types import time class MultiMethod: """表示单个多重方法""" def __init__(self, name): self._methods = {} self.__name__ = name def register(self, meth): """将新方法注册为多重方法""" sig = inspect.signature(meth) # 从方法的注释生成类型签名 types = [] for name, parm in sig.parameters.items(): if name == 'self': continue if parm.annotation is inspect.Parameter.empty: raise TypeError( 'Argument {} must be annotated with a type'.format(name) ) if not isinstance(parm.annotation, type): raise TypeError( 'Argument {} annotation must be a type'.format(name) ) if parm.default is not inspect.Parameter.empty: self._methods[tuple(types)] = meth types.append(parm.annotation) self._methods[tuple(types)] = meth def __call__(self, *args): """基于参数的类型签名调用方法""" types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: raise TypeError('No matching method for types {}'.format(types)) def __get__(self, instance, cls): """使调用在类中工作所需的描述符方法""" if instance is not None: return types.MethodType(self, instance) else: return self class MultiDict(dict): """用于在元类中构建多方法的特殊字典""" def __setitem__(self, key, value): if key in self: # If key already exists, it must be a multimethod or callable current_value = self[key] if isinstance(current_value, MultiMethod): current_value.register(value) else: mvalue = MultiMethod(key) mvalue.register(current_value) mvalue.register(value) super().__setitem__(key, mvalue) else: super().__setitem__(key, value) class MultipleMeta(type): """允许多个方法分派的元类""" def __new__(cls, clsname, bases, clsdict): return type.__new__(cls, clsname, bases, dict(clsdict)) @classmethod def __prepare__(cls, clsname, bases): return MultiDict() # 使用 class Spam(metaclass=MultipleMeta): def bar(self, x: int, y: int): print('Bar 1:', x, y) def bar(self, s: str, n: int = 0): print('Bar 2:', s, n) class Date(metaclass=MultipleMeta): def __init__(self, year: int, month: int, day: int): self.year = year self.month = month self.day = day def __init__(self): t = time.localtime() self.__init__(t.tm_year, t.tm_mon, t.tm_mday) s = Spam() s.bar(2, 3) # Bar 1: 2 3 s.bar('hello') # Bar 2: hello 0 s.bar('hello', 5) # Bar 2: hello 5 # s.bar(2, 'hello') # 报错:TypeError: No matching method for types (<class 'int'>, <class 'str'>) # 重载__init__ d = Date(2012, 12, 21) e = Date() print(e.year) # 2021 print(e.month) # 9 print(e.day) # 15
# 实现方法重载详解 import inspect import types import time class MultiMethod: """表示单个多重方法""" def __init__(self, name): self._methods = {} self.__name__ = name def register(self, meth): print('register') """将新方法注册为多重方法""" sig = inspect.signature(meth) # (self, x: int, y: int) # 从方法的注释生成类型签名 types = [] for name, parm in sig.parameters.items(): # name = self, x, y parm = self, x: int, y: int if name == 'self': # self跳过的,那么只会运行name = x y 这种 continue # parm.annotation = <class 'int'>,表示类型parm参数的类型 # inspect.Parameter.empty = <class 'inspect._empty'> 为空类型 # 如果传递进来的参数没有后面带标签 报错哦 if parm.annotation is inspect.Parameter.empty: raise TypeError( 'Argument {} must be annotated with a type'.format(name) ) # 如果参数的标签类型不是一个str,int这种类的话那么isinstance返回False就raise报错 if not isinstance(parm.annotation, type): raise TypeError( 'Argument {} annotation must be a type'.format(name) ) # parm.default:<class 'inspect._empty'>或者0,表示签名的默认值,要么为空或者0 # 如果参数签名默认值不为空那么走if里面语句 # 往_methods字典里添加 (<class 'str'>,): <function Spam.bar at 0x000001B7C490AE50> if parm.default is not inspect.Parameter.empty: self._methods[tuple(types)] = meth types.append(parm.annotation) self._methods[tuple(types)] = meth print(self._methods) # types = [<class 'int'>, <class 'int'>] meth = <function Spam.bar at 0x0000020C945DADC0> # self._methods:被调用了两次register所以打印两次 # {(<class 'int'>, <class 'int'>): <function Spam.bar at 0x0000022F4CA6ADC0>} # {(<class 'int'>, <class 'int'>): <function Spam.bar at 0x0000022F4CA6ADC0>, # (<class 'str'>,): <function Spam.bar at 0x0000022F4CA6AE50>, # (<class 'str'>, <class 'int'>): <function Spam.bar at 0x0000022F4CA6AE50>} def __call__(self, *args): """基于参数的类型签名调用方法""" types = tuple(type(arg) for arg in args[1:]) # print(types) # (<class 'int'>, <class 'int'>) meth = self._methods.get(types, None) # 从MultiMethod()的实例找到types参数对应的函数 if meth: # 如果有函数,把函数返回出去,不然报错 return meth(*args) else: raise TypeError('No matching method for types {}'.format(types)) def __get__(self, instance, cls): print('get') """使调用在类中工作所需的描述符方法""" if instance is not None: return types.MethodType(self, instance) else: return self class MultiDict(dict): """用于在元类中构建多方法的特殊字典""" def __setitem__(self, key, value): # key = [__module__, __qualname__, bar, bar] # 如果没有重名方法,那么就不走if里面语句,如果已经存了一个bar又来一个bar走if语句 if key in self: current_value = self[key] # 第一个bar指向的函数:<function Spam.bar at 0x000002001288ADC0> if isinstance(current_value, MultiMethod): # 如果这个函数是MultiMethod的实例化对象那么可以调用register方法 current_value.register(value) else: mvalue = MultiMethod(key) mvalue.register(current_value) mvalue.register(value) # current_value是第一个bar函数,value是第二个bar函数 super().__setitem__(key, mvalue) # 调用父类的往字典里写键值对 else: super().__setitem__(key, value) class MultipleMeta(type): """允许多个方法分派的元类""" @classmethod def __prepare__(cls, clsname, bases): return MultiDict() # 返回一个命名空间,一个特殊字典 def __new__(cls, clsname, bases, clsdict): # clsdict = {'__module__': '__main__', # '__qualname__': 'Spam', # 'bar': <__main__.MultiMethod object at 0x00000176555736A0>} # 这里clsdict这个字典存储bar对应的是一个MultiMethod()实例, # 这个类里self._methods = {(<class 'int'>, <class 'int'>): <function Spam.bar at 0x0000022F4CA6ADC0>, # (<class 'str'>,): <function Spam.bar at 0x0000022F4CA6AE50>, # (<class 'str'>, <class 'int'>): <function Spam.bar at 0x0000022F4CA6AE50>} # 里面有三个方法, # 'bar':MultiMethod(),MultiMethod是一个描述器 # 所以 外面 s.bar调用MultiMethod里面的 __get__方法,走if语句 # return types.MethodType(self, instance) 返回一个绑定方法,可以调用的方法,self这个方法绑定第一个参数为instance # self = MultiMethod()实例 instance = Spam()实例,类似给self方法绑定Spam()实例这个参数 # MultiMethod()(Spam(), 2, 3)等同__call__(MultiMethod(),(Spam(), 2, 3) # 分析传递进来的 2, 3数据类型是(<class 'int'>, <class 'int'>) # return meth(*args)——> meth(<__main__.Spam object at 0x000001BE748234F0>, 2, 3) # 第一个参数是Spam()实例,第二个参数是2,第三个参数是3,所以可以调用s.bar(2, 3) return type.__new__(cls, clsname, bases, dict(clsdict)) # 使用 # class Spam(metaclass=MultipleMeta): # def bar(self, x: int, y: int): # print('Bar 1:', x, y) # # def bar(self, s: str, n: int = 0): # print('Bar 2:', s, n) class Date(metaclass=MultipleMeta): def __init__(self, year: int, month: int, day: int): self.year = year self.month = month self.day = day def __init__(self): t = time.localtime() self.__init__(t.tm_year, t.tm_mon, t.tm_mday) # s = Spam() # s.bar(2, 3) # Bar 1: 2 3 # s.bar('hello') # Bar 2: hello 0 # s.bar('hello', 5) # Bar 2: hello 5 # s.bar(2, 'hello') # 报错:TypeError: No matching method for types (<class 'int'>, <class 'str'>) # 根据传入的参数选择对应的参数标签类型来调用函数 print('---------------------------') # # 重载__init__ # d = Date(2012, 12, 21) # 这里调用一次__init__调用一个get e = Date() # 不传递参数的话那么这里调用了两次get,因为下面的__init__里面调用了self.__init__(t.tm_year, t.tm_mon, t.tm_mday) # print(e.year) # 2021 # print(e.month) # 9 # print(e.day) # 15
# 实现思路: # MultipleMeta 元类使用它的 __prepare__() 方法 来提供一个 MultiDict() 实例的自定义字典。
这个跟普通字典不一样的是, MultiDict 会在元素被设置的时候检查是否已经存在,
如果存在的话,重复的元素会在 MultiMethod 实例中合并 # MultiMethod 实例通过构建从类型签名到函数的映射来收集方法。 在这个构建过程中,
函数注解被用来收集这些签名然后构建这个映射。 这个过程在 MultiMethod.register() 方法中实现。
这种映射的一个关键特点是对于多个方法,所有参数类型都必须要指定,否则就会报错 # 让 MultiMethod 实例模拟一个调用,它的 __call__() 方法被实现了。
这个方法从所有排除 self 的参数中构建一个类型元组,在内部map中查找这个方法,
然后调用相应的方法。为了能让 MultiMethod 实例在类定义时正确操作,__get__() 是必须得实现的。 它被用来构建正确的绑定方法 >>> b = s.bar >>> b <bound method Spam.bar of <__main__.Spam object at 0x1006a46d0>> >>> b.__self__ <__main__.Spam object at 0x1006a46d0> >>> b.__func__ <__main__.MultiMethod object at 0x1006a4d50> >>> b(2, 3) Bar 1: 2 3 >>> b('hello') Bar 2: hello 0 >>> # 本节的实现还有一些限制,其中一个是不能使用关键字参数
# __call__函数只有*args不能接收关键字 x="xxx"这种参数
>>> s.bar(x=2, y=3) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __call__() got an unexpected keyword argument 'y' >>> s.bar(s='hello') Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __call__() got an unexpected keyword argument 's' >>> # 有其他的方法能添加这种支持,但是它需要一个完全不同的方法映射方式。 问题在于关键字参数的出现是没有顺序的。
当它跟位置参数混合使用时, 那你的参数就会变得比较混乱了,这时候你不得不在 __call__() 方法中先去做个排序 # 对于继承也是有限制的,例如,类似下面这种代码就不能正常工作: class A: pass class B(A): pass class C: pass class Spam(metaclass=MultipleMeta): def foo(self, x:A): print('Foo 1:', x) def foo(self, x:C): print('Foo 2:', x) # 因为 x:A 注解不能成功匹配子类实例(比如B的实例),如下 >>> s = Spam() >>> a = A() >>> s.foo(a) Foo 1: <__main__.A object at 0x1006a5310> >>> c = C() >>> s.foo(c) Foo 2: <__main__.C object at 0x1007a1910> >>> b = B() >>> s.foo(b) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "multiple.py", line 44, in __call__ raise TypeError('No matching method for types {}'.format(types)) TypeError: No matching method for types (<class '__main__.B'>,) >>>
# 通过描述器来实现类似的效果方法重载 import types class multimethod: def __init__(self, func): self._methods = {} self.__name__ = func.__name__ self._default = func def match(self, *types): def register(func): ndefaults = len(func.__defaults__) if func.__defaults__ else 0 for n in range(ndefaults+1): self._methods[types[:len(types) - n]] = func return self return register def __call__(self, *args): types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: return self._default(*args) def __get__(self, instance, cls): if instance is not None: return types.MethodType(self, instance) else: return self class Spam: @multimethod def bar(self, *args): # Default method called if no match raise TypeError('No matching method for bar') @bar.match(int, int) def bar(self, x, y): print('Bar 1:', x, y) @bar.match(str, int) def bar(self, s, n = 0): print('Bar 2:', s, n) # 这种描述器方法也不支持关键字参数和继承
Python:函数默认值的作用域【__defaults__】【__kwdefaults__】
https://blog.csdn.net/Smart_look/article/details/114237347
# 描述器编写可重入方法详解 import types class multimethod: def __init__(self, func): self._methods = {} self.__name__ = func.__name__ self._default = func def match(self, *types): def register(func): ndefaults = len(func.__defaults__) if func.__defaults__ else 0 # func.__defaults__从函数属性查找参数的默认值,返回一个元组,如果没有默认找返回None # func.__defaults__ = Nonehi 或者 (0,) 所以 ndefaults = 0, 1 # types = (<class 'int'>, <class 'int'>)或者(<class 'str'>, <class 'int'>) # ndefaults = 0的时候 range(1),在self._methods字典里存储(<class 'int'>, <class 'int'>)[:2-0 ] # self._methods字典里存储存储(<class 'int'>, <class 'int'>)[:2-0 ] :func # 然后把self返回出去 for n in range(ndefaults + 1): self._methods[types[:len(types) - n]] = func return self return register def __call__(self, *args): types = tuple(type(arg) for arg in args[1:]) meth = self._methods.get(types, None) if meth: return meth(*args) else: return self._default(*args) def __get__(self, instance, cls): if instance is not None: return types.MethodType(self, instance) else: return self class Spam: # @multimethod def bar(self, *args): # 如果没有匹配到对应的bar方法那么调用这个bar raise TypeError('No matching method for bar') con1 = multimethod(bar) bar = con1 # # 重构bar函数,把bar函数传进multimethod类里实例化赋值给新的bar # multimethod()实例里:self._default = 这个bar函数(调用就报错) # :self.__name__ = 'bar' # @bar.match(int, int) def bar(self, x, y): print('Bar 1:', x, y) con2 = con1.match(int, int)(bar) bar = con2 # con2 = con1.match(int, int)(bar) 返回的是一个self也就是multimethod()这个类 # 这个调用一次match一次register # multimethod(bar).match(int, int)——>types = (<class 'int'>, <class 'int'>) # multimethod(bar).match(int, int)(bar)——>register(bar) # 本质上是调用multimethod(bar)match(int, int)(bar), # 本质上就是调用multimethod(bar1)这个对象内部的match方法然后再调用register方法, # 在multimethod(bar1)这个对象内部的_methods字典里写入键值对方法 # @bar.match(str, int) def bar(self, s, n=0): print('Bar 2:', s, n) con3 = con2.match(str, int)(bar) bar = con3 # 这个调用一次math一次register # con2本质上就是上面的multimethod(bar)实例化,这里同上面 # multimethod(bar).match(str, int)——>types = (<class 'str'>, <class 'int'>) # multimethod(bar).match(str, int)(bar)——>register(bar) # 现在因为这个bar函数的参数n有默认参数,所以调用这个bar函数的话有两种情况 s.bar(s, n)或者s.bar(s) # len(func.__defaults__) = 1 # 所以在register函数里,ndefaults # range(1+1)——>range(2)也就是循环两次需要给multimethod(bar)实例的self._methods 字典里添加两个键值对 # 第一个键值对(<class 'str'>, <class 'int'>):当前bar函数 # 第二个键值对(<class 'str'>,): 当前的bar函数 # 经过上面三个bar函数+装饰器,最终生成bar = multimethod()实例 # self._default = bar1 # self.__name__ = 'bar' # self._methods = {(<class 'int'>, <class 'int'>): <function Spam.bar at 0x000001F900C9C700>, # (<class 'str'>, <class 'int'>): <function Spam.bar at 0x000001F900C9C790>, # (<class 'str'>,): <function Spam.bar at 0x000001F900C9C790>} # 最终这个Spam类里没有三个bar函数了,只有一个 bar = multimethod()实例,而 multimethod类又是一个装饰器 # Spam.__dict__ 里有'bar': <__main__.multimethod object at 0x000001E634000FD0> # <__main__.multimethod object at 0x000001E634000FD0>这个是multimethod类描述器的实例化 # 外面s.bar调用__get__方法 return types.MethodType(self, instance) 返回一个类似self函数绑定了instance参数 # self = multimethod(), instance = s = Spam() # multimethodz类自己实现了__call__方法所以types.MethodType(self, instance)(10, 20) # 等同multimethodz().__call__(instance, 10, 20) # 等同multimethodz().__call__(Spam(), 10, 20) # 根据传递进来的10, 20两个参数找到方法meth,如果找到直接调用meth(Spam(), 10, 20)返回结果 # 如果找到调用默认的_default(*args)函数 s = Spam() s.bar(10, 20) s.bar('sb', 100) # s.bar() # TypeError: No matching method for bar
# 本质上写个三个bar方法,每个bar方法拥有不同的参数,
# 第一个bar方法bar1,使用bar1 = multimethod(bar1)重构了bar1把bar1传到multimethod类里生成一个对象multimethod(bar1)
# 第二个bar方法bar2,使用bar2 = bar1.match(int, int)(bar2)又重构了bar2,
在给对象multimethod(bar1)写入属性和方法同时又返回了sefmultimethod(bar1)对象赋值给bar2
# 第二个bar方法bar3,使用bar3 = bar2.match(int, int)(bar3)又重构了bar3,
在给对象multimethod(bar1)写入属性和方法同时又返回了sefmultimethod(bar1)对象赋值给bar3
# 所以最终Spam类内部只有一个bar = multimethod(bar)的实例,实例里面由于上面运行已经把self.methods和self.name等各种参数都写到类里面了
# 而multimethod(bar)又是一个描述器,所以外部s = Spam() ,
s.bar本质上调用的是描述器multimethod的__get__方法返回一个绑定函数self(也就是multimethod(bar)实例)绑定了第一个参数为instance(Spam()实例),
(因为multimethod实现了__call__方法可以看成可以调用的函数),所以s.bar本质上返回的是multimethod类的__call__方法并且绑定了第一个参数
后面s.bar(2, 3)类似__call__(Spam()实例, 2, 3),根据传递进来的参数(2, 3)找到self也就是multimethod(bar)实例内部的对应的meth函数
然后retun meth(*args)调用
本质上全部的步骤如上:这种描述器方法不支持关键字参数和继承
21:避免重复的属性方法 类中需要重复的定义一些执行相同逻辑的属性方法,进行类型检查,简化这些重复代码呢
# 简单的类,属性由属性方法包装: class Person: def __init__(self, name ,age): self.name = name self.age = age @property def name(self): return self._name @name.setter def name(self, value): if not isinstance(value, str): raise TypeError('name must be a string') self._name = value @property def age(self): return self._age @age.setter def age(self, value): if not isinstance(value, int): raise TypeError('age must be an int') self._age = value
@property本质上还是使用一个描述器,property是一个类,是一个描述器内部实现了__get__,__set__,__deleter__方法
和上面的可重入方法实现技术类似
上面为了实现属性值的类型检查写了很多的重复代码,
# 避免属性检查代码重复的优化 # 创建一个函数用来定义属性并返回它 def typed_property(name, expected_type): storage_name = '_' + name # @property def prop(self): return getattr(self, storage_name) con = property(prop) # @prop.setter def prop(self, value): if not isinstance(value, expected_type): raise TypeError('{} must be a {}'.format(name, expected_type)) setattr(self, storage_name, value) prop = con.setter(prop) return prop class Person: name = typed_property('name', str) age = typed_property('age', int) def __init__(self, name, age): self.name = name self.age = age p = Person('liergou', 18) # print(p.name) p.name = 'sb' print(p.name)
# propty详解 class A: _name = "liergou" # @property def name(self): return self._name con = property(name) print(con) # <property object at 0x000002407E0A0450> # @name.setter def name(self, in_data): self._name = in_data name = con.setter(name) print(name) # <property object at 0x000002407E0A0400>
print(A.__dict__) # {'__module__': '__main__', '_name': 'liergou', 'name': <property object at 0x000002407E0A0400>, # 'con': <property object at 0x000002407E0A0450>, '__dict__': <attribute '__dict__' of 'A' objects>, # '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None}
Property实现手段和上面的实现重载函数技术类似:
Property是一个类,是一个描述器,这个里在类A里使用Property这个类,@property等同把name1函数传递进去生成一个实例化对象property(name1)
然后@name.setter等同property(name1).setter(name) 类似实例对象property(name1)调用setter函数后再返回一个property()对象
下面的@name.setter也是如此,操作实例里面一些属性后返回一个property()实例,
上面使用函数本质上和在类里给两个同名方法使用@property和@name.setter操作一样,都是在类A或者Person类里写一个键值对 xxx = property()
所以上面的使用函数方法也可以,和类一样
typed_property函数内部生成了一个prop = property()实例化,property这个类本质也是个描述器,然后把prop返回出来赋值给name和age变量
所以name和age都指向一个property描述器实例化以后的实例,在外部p.name这样使用就会调用描述器的__get__方法
# typed_property() 所做的仅仅就是生成属性并返回这个属性对象
# 在一个类中使用typed_property() 的时候,效果跟将它里面的代码放到类定义中去是一样的(也就是类中直接写函数+@property装饰器)
# 属性的 getter 和 setter 方法访问了本地变量如 name , expected_type 以及 storate_name,这些变量的值会保存在闭包当中
# 使用 functools.partial() from functools import partial def typed_property(name, expected_type): storage_name = '_' + name @property def prop(self): return getattr(self, storage_name) @prop.setter def prop(self, value): if not isinstance(value, expected_type): raise TypeError('{} must be a {}'.format(name, expected_type)) setattr(self, storage_name, value) return prop String = partial(typed_property, expected_type=str) Integer = partial(typed_property, expected_type=int) class Person: name = String('name') age = Integer('age') def __init__(self, name, age): self.name = name self.age = age
其实和上面差不多,绕了一步而已
22:定义上下文管理器的简单方法 实现一个新的上下文管理器,以便使用with语句。
# 实现一个新的上下文管理器的最简单的方法就是使用 contexlib 模块中的 @contextmanager 装饰器 # 实现代码块计时功能的上下文管理器 import time from contextlib import contextmanager @contextmanager def timethis(label): start = time.time() print(start) try: yield finally: end = time.time() print(end) print('{}: {}'.format(label, end - start)) with timethis('counting'): n = 10000000 while n > 0: n -= 1 # 函数 timethis() 中,yield 之前的代码会在上下文管理器中作为 __enter__() 方法执行,
所有在 yield 之后的代码会作为 __exit__() 方法执行。 如果出现了异常,异常会在yield语句那里抛出
# 列表对象上的某种事务的上下文管理器 from contextlib import contextmanager @contextmanager def list_transaction(orig_list): working = list(orig_list) yield working orig_list[:] = working # 任何对列表的修改只有当所有代码运行完成并且不出现异常的情况下才会生效
因为只有不出错才会走orig_list[:] = working把orig_list重新赋值
items = [1, 2, 3] with list_transaction(items) as working: working.append(4) working.append(5) print(items) # [1, 2, 3, 4, 5] try: with list_transaction(items) as working: working.append(6) working.append(7) raise RuntimeError('oops') except: pass print(items) # [1, 2, 3, 4, 5]
# 正常情况下实现上下文管理器,定义一个类,里面包含一个 __enter__() 和一个 __exit__() 方法 import time class timethis: def __init__(self, label): self.label = label def __enter__(self): self.start = time.time() return self def __exit__(self, exc_ty, exc_val, exc_tb): end = time.time() print('{}: {}'.format(self.label, end - self.start)) def print_a(self): print(self.label, "李明是个傻逼哦") with timethis('continue') as f: f.print_a() 打印: continue 李明是个傻逼哦 continue: 0.0
# @contextmanager 应该仅仅用来写自包含的上下文管理函数。 如果你有一些对象(比如一个文件、网络连接或锁),
需要支持 with 语句,那么你就需要单独实现 __enter__() 方法和 __exit__() 方法类
23:在局部变量域中执行代码 使用范围内执行某个代码片段,并且希望在执行后所有的结果都不可见。
# Python中的eval()、exec()及其相关函数:
https://www.cnblogs.com/yyds/p/6276746.html
eval函数:计算指定表达式的值。也就是说它要执行的Python代码只能是单个运算表达式(注意eval不支持任意形式的赋值操作),
而不能是复杂的代码逻辑,这一点和lambda表达式比较相似
exec函数:动态执行Python代码。也就是说exec可以执行复杂的Python代码,而不像eval函数那么样只能计算一个表达式的值。
eval()函数与exec()函数的区别:
- eval()函数只能计算单个表达式的值,而exec()函数可以动态运行代码段。
- eval()函数可以有返回值,而exec()函数返回值永远为None。
# 全局命名空间内执行一个代码片段 a = 13 exec('b = a + 1') print(b) # 14 # 函数中执行同样的代码 def test(): a = 13 exec('b=a+1') print(b) test() # NameError: name 'b' is not defined, 报错 # 抛出了一个NameError异常,就跟在 exec() 语句从没执行过一样。 # 在后面的计算中使用到 exec() 执行结果的话就会有问题了 # 修复函数中使用的这这种错误,需要在调用 exec() 之前使用 locals() 函数来得到一个局部变量字典。
之后你就能从局部字典中获取修改过后的变量值了 def test(): a = 13 loc = locals() exec('b=a+1') b = loc['b'] print(b) test() # 14
# exec()的正确使用,默认情况下,exec() 会在调用者局部和全局范围内执行代码。然而,在函数里面,
传递给 exec() 的局部范围是拷贝实际局部变量组成的一个字典。
因此,如果 exec() 如果执行了修改操作,这种修改后的结果对实际局部变量值是没有影响的 def test1(): x = 0 exec('x += 1') print(x) test1() # 0
# 调用 locals() 获取局部变量时,你获得的是传递给 exec() 的局部变量的一个拷贝。
通过在代码执行后审查这个字典的值,那就能获取修改后的值了 def test2(): x = 0 loc = locals() print('before:', loc) # before: {'x': 0} exec('x += 1') print('after:', loc) # after: {'x': 1, 'loc': {...}} print('x =', x) # x = 0 test2() # 除非你将 loc 中被修改后的值手动赋值给x,否则x变量值是不会变的。
# 用 locals() 的时候,注意操作顺序,每次它被调用的时候,
locals() 会获取局部变量值中的值并覆盖字典中相应的变量 def test3(): x = 0 loc = locals() print(loc) # {'x': 0} exec('x += 1') print(loc) # {'x': 1, 'loc': {...}} locals() print(loc) # {'x': 0, 'loc': {...}} test3() # 最后一次调用 locals() 的时候x的值被覆盖掉了
理解:本质上locals返回的就是一个备份空间,而不是test3函数内部空间的属性变量属性
exec操作的也是这个备份空间,每次调用一次locals就会在这个备份空间去重载一些属性对应的值,
使用当前test3函数内部的属性变量去重载,所以当前test3函数真实的空间其实和备份空间不同
所以下面的代码报错,因为真实的test3函数空间是找不到b这个变量的,只是备份空间有b这个变量,打印b报错
def test3(): x = 0 loc = locals() exec('b=1') print(loc) # {'x': 0, 'loc': {...}, 'b': 1} print(b) test3() # NameError: name 'b' is not defined
# locals() 的一个替代方案,你可以使用你自己的字典,并将它传递给 exec() def test4(): a = 13 loc = {'a': a} glb = {} exec('b = a + 1', glb, loc) b = loc['b'] print(b) test4() # 14 # 这种方式是使用 exec() 的最佳实践。 只需要保证全局和局部字典在后面代码访问时已经被初始化 # 在使用 exec() 之前,你可能需要问下自己是否有其他更好的替代方案。
大多数情况下当你要考虑使用 exec() 的时候, 还有另外更好的解决方案,比如装饰器、闭包、元类,或其他一些元编程特性
24:解析与分析Python源码 解析并分析Python源代码的程序。
# Python能够计算或执行字符串形式的源代码:eval和exec函数 x = 42 eval('2 + 3*4 + x') # 56 exec("for i in range(10): print(i, end=' ')") 打印:0 1 2 3 4 5 6 7 8 9
# ast 模块能被用来将Python源码编译成一个可被分析的抽象语法树(AST) import ast ex = ast.parse('2 + 3*4 + x', mode='eval') print(ex) # <ast.Expression object at 0x000001F74FFDC0A0> print(ast.dump(ex)) # Expression(body=BinOp(left=BinOp(left=Constant(value=2),
op=Add(), right=BinOp(left=Constant(value=3),
op=Mult(), right=Constant(value=4))),
op=Add(), right=Name(id='x', ctx=Load()))) import ast top = ast.parse('for i in range(10): print(i)', mode='exec') print(top) # <ast.Module object at 0x000002785B28EDC0> print(ast.dump(top)) # Module(body=[For(target=Name(id='i', ctx=Store()),
iter=Call(func=Name(id='range', ctx=Load()),
args=[Constant(value=10)], keywords=[]),
body=[Expr(value=Call(func=Name(id='print', ctx=Load()),
args=[Name(id='i', ctx=Load())], keywords=[]))], orelse=[])], type_ignores=[]) # 分析源码树由一系列AST节点组成的,分析这些节点最简单的方法就是定义一个访问者类,
实现很多 visit_NodeName() 方法, NodeName() 匹配那些你感兴趣的节点
# 编写一个类,记录了哪些名字被加载、存储和删除的信息。 import ast class CodeAnalyzer(ast.NodeVisitor): def __init__(self): self.loaded = set() # 加载信息集合 self.stored = set() # 存储信息集合 self.deleted = set() # 删除信息集合 def visit_Name(self, node): if isinstance(node.ctx, ast.Load): self.loaded.add(node.id) elif isinstance(node.ctx, ast.Store): self.stored.add(node.id) elif isinstance(node.ctx, ast.Del): self.deleted.add(node.id) code = ''' for i in range(10): print(i) del i ''' # 解析为AST top = ast.parse(code, mode='exec') # exec解析 print(ast.dump(top)) # Module(body=[For(target=Name(id='i', ctx=Store()), # iter=Call(func=Name(id='range', ctx=Load()), # args=[Constant(value=10)], keywords=[]), # body=[Expr(value=Call(func=Name(id='print', ctx=Load()), # args=[Name(id='i', ctx=Load())], keywords=[]))], orelse=[]), # Delete(targets=[Name(id='i', ctx=Del())])], type_ignores=[]) # 馈送AST以分析名称使用情况 c = CodeAnalyzer() c.visit(top) print('Loaded:', c.loaded) # Loaded: {'print', 'range', 'i'} print('Stored:', c.stored) # Stored: {'i'} print('Deleted:', c.deleted) # Deleted: {'i'} # AST可以通过 compile() 函数来编译并执行 exec(compile(top, '<stdin>', 'exec')) # 输出:0 1 2 3 4 5 6 7 8 9
# 相比盲目的传递一些代码片段到类似 exec() 函数中,你可以先将它转换成一个AST, 观察它的细节看它到底是怎样做的 # 重写AST来表示新的代码, # 装饰器例子,可以通过重新解析函数体源码、 重写AST并重新创建函数代码对象来将全局访问变量降为函数体作用范围 import ast import inspect # 节点访问者,将全局访问的名称作为局部变量降低到函数体中 class NameLower(ast.NodeVisitor): def __init__(self, lowered_names): self.lowered_names = lowered_names def visit_FunctionDef(self, node): # 编译一些赋值以降低常数 code = '__globals = globals()\n' code += '\n'.join("{0} = __globals['{0}']".format(name) for name in self.lowered_names) code_ast = ast.parse(code, mode='exec') # 将新语句注入函数体 node.body[:0] = code_ast.body # 保存函数对象 self.func = node # 将全局名称转换为局部名称的装饰器 def lower_names(*namelist): def lower(func): srclines = inspect.getsource(func).splitlines() # 跳过@lower_names修饰符之前的源代码行 for n, line in enumerate(srclines): if '@lower_names' in line: break src = '\n'.join(srclines[n + 1:]) # 黑客处理缩进代码 if src.startswith((' ', '\t')): src = 'if 1:\n' + src top = ast.parse(src, mode='exec') # 转换AST cl = NameLower(namelist) cl.visit(top) # 执行修改后的AST temp = {} exec(compile(top, '', 'exec'), temp, temp) # 拉出修改过的代码对象 func.__code__ = temp[func.__name__].__code__ return func return lower INCR = 1 # 使用这个代码 @lower_names('INCR') def countdown(n): while n > 0: n -= INCR # 装饰器会将 countdown() 函数重写为类似下面这样子 # def countdown(n): # __globals = globals() # INCR = __globals['INCR'] # while n > 0: # n -= INCR # 性能测试中,它会让函数运行快20%
25:拆解Python字节码 代码反编译成低级的字节码来查看它底层的工作机制
# dis 模块可以被用来输出任何Python函数的反编译结果 import dis def countdown(n): while n > 0: print('T-minus', n) n -= 1 print('Blastoff!') print(dis.dis(countdown)) 输出: 5 >> 0 LOAD_FAST 0 (n) 2 LOAD_CONST 1 (0) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 28 6 8 LOAD_GLOBAL 0 (print) 10 LOAD_CONST 2 ('T-minus') 12 LOAD_FAST 0 (n) 14 CALL_FUNCTION 2 16 POP_TOP 7 18 LOAD_FAST 0 (n) 20 LOAD_CONST 3 (1) 22 INPLACE_SUBTRACT 24 STORE_FAST 0 (n) 26 JUMP_ABSOLUTE 0 8 >> 28 LOAD_GLOBAL 0 (print) 30 LOAD_CONST 4 ('Blastoff!') 32 CALL_FUNCTION 1 34 POP_TOP 36 LOAD_CONST 0 (None) 38 RETURN_VALUE None
# 要知道程序底层的运行机制的时候,dis 模块是很有用的 # 想试着理解性能特征。 被 dis() 函数解析的原始字节码 def countdown(n): while n > 0: print('T-minus', n) n -= 1 print('Blastoff!') print(countdown.__code__.co_code) # b'|\x00d\x01k\x04r\x1ct\x00d\x02|\x00\x83\x02\x01\x00|\x00d\x038\x00}\x00q\x00t\x00d\x04\x83\x01\x01\x00d\x00S\x00' # 自己解释这段代码,你需要使用一些在 opcode 模块中定义的常量 import opcode def countdown(n): while n > 0: print('T-minus', n) n -= 1 print('Blastoff!') c = countdown.__code__.co_code print(opcode.opname[c[0]]) # LOAD_FAST print(opcode.opname[c[2]]) # LOAD_CONST
# dis 模块没有函数让你以编程方式很容易的来处理字节码 # opcodes:操作码 # 下面的生成器函数可以将原始字节码序列转换成 opcodes 和参数 import opcode def countdown(n): while n > 0: print('T-minus', n) n -= 1 print('Blastoff!') def generate_opcodes(codebytes): extended_arg = 0 i = 0 n = len(codebytes) while i < n: op = codebytes[i] i += 1 if op >= opcode.HAVE_ARGUMENT: oparg = codebytes[i] + codebytes[i + 1] * 256 + extended_arg extended_arg = 0 i += 2 if op == opcode.EXTENDED_ARG: extended_arg = oparg * 65536 continue else: oparg = None yield (op, oparg) for op, oparg in generate_opcodes(countdown.__code__.co_code): print(op, opcode.opname[op], oparg) # 输出: 124 LOAD_FAST 25600 1 POP_TOP None 107 COMPARE_OP 29188 28 INPLACE_FLOOR_DIVIDE None 116 LOAD_GLOBAL 25600 2 ROT_TWO None 124 LOAD_FAST 33536 2 ROT_TWO None 1 POP_TOP None 0 <0> None 124 LOAD_FAST 25600 3 ROT_THREE None 56 INPLACE_SUBTRACT None 0 <0> None 125 STORE_FAST 28928 0 <0> None 116 LOAD_GLOBAL 25600 4 DUP_TOP None 131 CALL_FUNCTION 257 0 <0> None 100 LOAD_CONST 21248 0 <0> None
# 可以利用上面例子替换任何你想要替换的函数的原始字节码 >>> def add(x, y): ... return x + y ... >>> c = add.__code__ >>> c <code object add at 0x1007beed0, file "<stdin>", line 1> >>> c.co_code b'|\x00\x00|\x01\x00\x17S' >>> >>> # 用伪字节码创建一个全新的代码对象 >>> import types >>> newbytecode = b'xxxxxxx' >>> nc = types.CodeType(c.co_argcount, c.co_kwonlyargcount, ... c.co_nlocals, c.co_stacksize, c.co_flags, newbytecode, c.co_consts, ... c.co_names, c.co_varnames, c.co_filename, c.co_name, ... c.co_firstlineno, c.co_lnotab) >>> nc <code object add at 0x10069fe40, file "<stdin>", line 1> >>> add.__code__ = nc >>> add(2,3) Segmentation fault
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!