鱼C论坛

 找回密码
 立即注册
查看: 2026|回复: 18

[技术交流] Python元编程(跟新中,请不要回复谢谢)

[复制链接]
发表于 2020-1-14 23:03:59 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 Stubborn 于 2020-1-18 00:56 编辑


前言


以上内容来自书籍《Python3-Cookbook第三版中文版》,有兴趣可以下载自己阅读包括(epub,awz3,mobi)。

游客,如果您要查看本帖隐藏内容请回复




  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20
  8. """
  9. import time
  10. from functools import wraps


  11. def timethis(function):
  12.     @wraps(function)
  13.     def wrapper(*args, **kwargs):
  14.         start = time.time()
  15.         function(*args, **kwargs)
  16.         print(function.__name__, start - time.time())

  17.     return wrapper


  18. @timethis
  19. def downout(n):
  20.     while n > 0:
  21.         n -= 1


  22. downout(10000)
复制代码


如果没有`@timethis`装饰器,等价于下面的调用方式。作为入门的小案例,没有很多好理解的,无非就是把函数包装成一个新的函数,同时在新函数里面运行函数,或者额外加点东西,比如上面的代码,实现了函数运行时间的打印功能。
  1. def downout(n):
  2.     while n > 0:
  3.         n -= 1


  4. downout = timethis(downout)
  5. downout(10000)
复制代码

评分

参与人数 1贡献 +8 收起 理由
zltzlt + 8

查看全部评分

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2020-1-14 23:04:55 | 显示全部楼层

装饰器--02节--编写装饰器时如何保存函数的元数据

本帖最后由 Stubborn 于 2020-1-15 16:14 编辑

目录层,一楼 - 编写装饰器时如何保存函数的元数据

什么叫函数的元数据呢,其实就是和函数一些相关的属性,比如函数的__name__(函数名字),__doc__(函数文档),__dict__(属性字典)等等和函数相关的。

其实`from functools import wraps`  `@wraps(function)`这个装饰器就是用来保存函数的元数据,在这个装饰器的作用下,函数的所有元数据会被保存,你可以试试,如果去掉`@wraps(function)`那么`metadata`函数的元数据,将会丢失。

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/8 下午2:20
  8. """
  9. import time
  10. from functools import wraps


  11. def timethis(function):
  12.     """wraps就是保存函数的元数据"""

  13.     @wraps(function)
  14.     def wrapper(*args, **kwargs):
  15.         start = time.time()
  16.         function(*args, **kwargs)
  17.         print(function.__name__, start - time.time())

  18.     return wrapper


  19. @timethis
  20. def metadata(n: int):
  21.     pass


  22. # 下面演示如何检视函数的元数据
  23. print(metadata.__name__)
  24. print(metadata.__doc__)
  25. print(metadata.__annotations__)
复制代码
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-15 00:17:02 | 显示全部楼层

解除一个装饰器

本帖最后由 Stubborn 于 2020-1-15 16:14 编辑

03节--解除一个装饰器

问题:一个装饰器已经作用在一个函数上,你想撤销它,直接访问原始的未包装的那个函数。

答案:一般来说我们可以通过访问__wrapped__属性来获取对原始函数的访问

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20
  8. """
  9. import time
  10. from functools import wraps


  11. def timethis(function):

  12.     @wraps(function)
  13.     def wrapper(*args, **kwargs):
  14.         print(f"this is one {function.__name__}")
  15.         return function(*args, **kwargs)

  16.     return wrapper


  17. def time_this(function):

  18.     @wraps(function)
  19.     def wrapper(*args, **kwargs):
  20.         print(f"this is two {function.__name__}")
  21.         return function(*args, **kwargs)

  22.     return wrapper


  23. @timethis
  24. def metadata(n: int):
  25.     print(f"this is metadata args {n}")
复制代码


访问__wrapped__属性来获取对原始函数的访问,前提是使用了`@wraps(function)`这个装饰器。
  1. meta = metadata.__wrapped__

  2. meta(5)
复制代码


直接访问装饰器背后的那个未包装过的函数对于调试、反射(introspection,也有译为“自省”)以及其他一些涉及函数的操作是很有帮助的。但是,这里讨论的技术只有在实现装饰器时利用functools模块中的@wraps对元数据进行了适当的拷贝,或者直接设定了__wrapped__属性时才有用。

如果有多个装饰器已经作用于某个函数上了,那么访问__wrapped__属性的行为目前是未定义的,应该避免这种情况!假如有如下代码:

  1. @timethis
  2. @time_this
  3. def add(x, y):
  4.     return f"{x},{y}"
复制代码

  1. a = add.__wrapped__  # 解除第一层装饰器
  2. print(a(3, 5))
  3. print(a.__wrapped__(7, 6))  # # 解除第二层装饰器
复制代码


最后但同样重要的是,请注意并不是所有的装饰器都使用了@wraps,因此有些装饰器的行为可能与我们期望的有所区别。特别是,由内建的装饰器@staticmethod和@classmethod创建的描述符(descriptor)对象并不遵循这个约定(相反,它们会把原始函数保存在__func__属性中)。所以,具体问题需要具体分析,每个人遇到的情况可能不同。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-15 00:54:23 | 显示全部楼层

定义一个带参数的装饰器

本帖最后由 Stubborn 于 2020-1-15 18:03 编辑

04节 - -  定义一个带参数的装饰器


  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20

  8. 让我们用一个例子来说明接受参数的过程。假设我们想编写一个为函数添加日志功能的装饰器,
  9.     但是又允许用户指定日志的等级以及一些其他的细节作为参数。下面是定义这个装饰器的可能做法:
  10. """
  11. import logging
  12. import os
  13. import time
  14. from functools import wraps


  15. def logged(level, name=None, message=None):
  16.     """
  17.     日志记录器
  18.     :param level:日志记录级别
  19.     :param name:记录器名称
  20.     :param message:是日志消息
  21.     # 如果未指定名称和消息,它们默认为函数的模块和名称。
  22.     """
  23.     def decorate(func):
  24.         logname = name if name else func.__module__
  25.         log = logging.getLogger(logname)
  26.         logmsg = message if message else func.__name__
  27.         meg = f"[level:{level}] {os.path.basename(__file__)} {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())} {logmsg}"

  28.         @wraps(func)
  29.         def wrapper(*args, **kwargs):
  30.             log.log(level, meg)
  31.             return func(*args, **kwargs)
  32.         return wrapper
  33.     return decorate


  34. # Example use
  35. @logged(logging.WARNING)
  36. def add(x, y):
  37.     return x + y

  38. @logged(logging.CRITICAL, 'example')
  39. def spam():
  40.     print('Spam!')
复制代码


代码如上,可以接受一些 参数,虽然看起来有点复杂, 核心却是很简单的。最外层的函数 logged() 接受参数并将它们作用在内部的装饰器函数上面。 内层的函数 decorate() 接受一个函数作为参数,然后在函数上面放置一个包装器。 这里的关键点是包装器是可以使用传递给 logged() 的参数的。
但是如果没有@logged()装饰器,它的调用等同

  1. func = logged(level, name=None, message=None)(add)
  2. fun()
复制代码



想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-15 01:02:42 | 显示全部楼层

可自定义属性的装饰器

本帖最后由 Stubborn 于 2020-1-16 20:37 编辑

05节 -_- 可自定义属性的装饰器

在这之前,稍微介绍`functools.partial` 用于修改,固定,添加函数的参数,返回一个新的函数。下面看简单的例子。

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20
  8. """
  9. from functools import partial
  10. from inspect import signature

  11. def test(a , b, c=5):
  12.     return a * b * c

  13. print(signature(test)) # -> (a, b, c=5)
  14. test_one = partial(test, 2, c=4) # new func
  15. print(signature(test_one)) #(b, *, c=4)
  16. print(test_one(5)) # return

  17. def test_two(*args, **kwargs):
  18.     return sum(args) + sum(list(kwargs.values()))

  19. print(signature(test_two))
  20. test_three = partial(test_two, 2, 3, d=5,  c=4)
  21. print(signature(test_three))
  22. print(test_three(6))
复制代码


问题:我们想编写一个装饰器来包装函数,但是可以让用户调整装饰器的属性,这样在运行时能够控制装饰器的行为。

    解答:引入了访问器函数(accessor function),通过使用nonlocal关键字声明变量来修改装饰器内部的属性。之后把访问器函数作为函数属性附加到包装函数上。


代码如下
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20

  8. 我们想编写一个装饰器来包装函数,但是可以让用户调整装饰器的属性,这样在运行时能够控制装饰器的行为。

  9.     引入了访问器函数(accessor function),通过使用nonlocal关键字声明变量来修改装饰器内部的属性。
  10.         之后把访问器函数作为函数属性附加到包装函数上。
  11. """

  12. from functools import wraps, partial
  13. import logging


  14. def attach_wrapper(func_obj, func=None):
  15.     """将函数附加为obj的属性"""
  16.     if func is None:
  17.         return partial(attach_wrapper, func_obj)
  18.     setattr(func_obj, func.__name__, func)
  19.     return func


  20. def logged(level, name=None, message=None):
  21.     """     日志记录器
  22.     :param level:日志记录级别
  23.     :param name:记录器名称
  24.     :param message:是日志消息
  25.     # 如果未指定名称和消息,它们默认为函数的模块和名称。
  26.     """
  27.     def decorate(func):
  28.         logname = name if name else func.__module__
  29.         logmsg = message if message else func.__name__
  30.         @wraps(func)
  31.         def wrapper(*args, **kwargs):
  32.             print(f"[level:{level}]\tname:{logname}\tmessage:{logmsg}")
  33.             return func(*args, **kwargs)

  34.         # Attach setter functions
  35.         @attach_wrapper(wrapper)
  36.         def set_level(newlevel):
  37.             nonlocal level
  38.             level = newlevel

  39.         @attach_wrapper(wrapper)
  40.         def set_message(newmsg):
  41.             nonlocal logmsg
  42.             logmsg = newmsg

  43.         return wrapper
  44.     return decorate

  45. # Example use
  46. @logged(logging.DEBUG)
  47. def add(x, y):
  48.     return x + y


  49. def spam(x, y):
  50.     return x + y


  51. # -----一下是功能演示-------------------
  52. print("---Example use fun__add__")
  53. add(3, 2)
  54. add.set_message("Alex")
  55. add(3, 2)
  56. # 等价调用
  57. print("---Example use fun__spam__")
  58. spam = logged(level=25)(spam) # spam -> wrapper 包含闭包内的局部变量,以及方法
  59. spam(3, 2)
  60. spam.set_message("Alex is good girl") # 通过 nonlocal 修改局部变量
  61. spam(3, 2)
复制代码


能打到目的(调整装饰器的属性),示例中的关键,就是访问器函数(set_message()和set_level())。它们以属性的形式附加到了包装函数上。每个访问器函数允许对nonlocal变量赋值来调整内部参数。而且访问器函数可以跨越多个装饰器层进行传播(如果所有的装饰器都使用了@functools.wraps 的话)

  1. def time_this(function):
  2.     @wraps(function)
  3.     def wrapper(*args, **kwargs):
  4.         start = time.time()
  5.         function(*args, **kwargs)
  6.         print(function.__name__, start - time.time())

  7.     return wrapper


  8. @time_this
  9. @logged(logging.DEBUG)
  10. def countdown(n):
  11.     while n > 0:
  12.         n -= 1
复制代码



末尾补充:关于上面访问器函数和装饰器,如果看不明白,可以参考下面的代码和第一个介绍partial的代码
  1. def attach_wrapper(func_obj, func=None):
  2.     if func is None:
  3.         return partial(attach_wrapper, func_obj)
  4.     setattr(func_obj, func.__name__, func)
  5.     return func

  6. def function():
  7.     pass

  8. @attach_wrapper(function)
  9. def test(name):
  10.     print(name)

  11. function.test(123)
复制代码

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-15 16:15:45 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-16 21:35 编辑

06节 -_-  带可选参数的装饰器

问题:你想写一个装饰器,既可以不传参数给它,比如 @decorator , 也可以传递可选参数给它,比如 @decorator(x,y,z)

例子,05节修改版本:
  1. from functools import wraps, partial


  2. def logged(func=None, *, level=50, name=None, message=None):
  3.     if func is None:
  4.         return partial(logged, level=level, name=name, message=message)

  5.     logname = name if name else func.__module__
  6.     logmsg = message if message else func.__name__

  7.     @wraps(func)
  8.     def wrapper(*args, **kwargs):
  9.         print(f"[level:{level}]\tname:{logname}\tmessage:{logmsg}")
  10.         return func(*args, **kwargs)

  11.     return wrapper
复制代码


从示例中可以看到,现在这个装饰器既能够以简单的形式(即@logged)使用,也可以提供可选的参数给它(即,@logged(level=50, name='example'))。

提到的实际上是一种编程一致性(programming consistency)的问题。当使用装饰器时,大部分程序员习惯于完全不使用任何参数,或者就像示例中那样使用参数。从技术上来说,如果装饰器的所有参数都是可选的,那么可以像这样来使用:

  1. @logged()  # 括号可以带,或者不带
  2. def sub(x, y, z):
  3.     return f"this is sub function {x}, {y}, {z}"
复制代码


要理解代码是如何工作的,就需要对装饰器是如何施加到函数上,以及对它们的调用约定有着透彻的理解才行。以def sub简单的装饰器为例:调用顺序是这样的:

  1. sub = logged(sub)  # -> 只是把sub函数,丢给logged函数,得到一个新的函数地址
  2. # 对于一个可接受参数的装饰器,例如 def spam 调用顺序是这样的:
  3. spam = logged(level=50, name="example")(spam)
复制代码


在初次调用logged()时,被包装的函数并没有传递给logged。因此在装饰器中,被包装的函数必须作为可选参数。这样一来,反过来迫使其他的参数都要通过关键字来指定。此外,当传递了参数后装饰器应该返回一个新函数,要包装的函数就作为参数传递给这个新函数(具体见05)。
要做到这一点,我们在解决方案中利用functools.partial来实现这个聪明的技巧。具体来说,它只是返回了一个部分完成的版本,除了要被包装的函数之外,其他所有的参数都已经确定好了。




想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-15 18:05:37 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 16:52 编辑

07节 -_- 利用装饰器强制函数上的类型检查

在这之前,稍微介绍`inspect.signature` 这个函数允许我们从一个可调用对象中提取出参数签名信息。下面看简单的例子。


  1. def spam(x, y, z=42):
  2.     pass

  3. from inspect import signature
  4. alex = signature(spam)
  5. print(f"The parameter tuple of sig is->{alex}") # -> (x, y, z=42)

  6. print(f"The parameter OrderedDict of sig is->{alex.parameters}")  # ->OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)])

  7. print(f"The parameter z.name of sig is -> {alex.parameters['z'].name}") # -> z

  8. print(f"The parameter z.default of sig is -> {alex.parameters['z'].default}") # -> 42

  9. print(f"The parameter z.kind of sig is -> {alex.parameters['z'].kind}") # -> 1

复制代码


我们想为函数参数添加强制性的类型检查功能,将其作为一种断言或者与调用者之间的契约。

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. """
  4. @ Version : ??
  5. @ Author : Alex
  6. @ Software : Pycharm
  7. @ Time : 2020/1/2 下午2:20
  8. """

  9. from inspect import signature
  10. from functools import wraps


  11. def typea_ssert(*ty_args, **ty_kwargs):
  12.     def decorate(func):
  13.         # If in optimized mode, disable type checking
  14.         if not __debug__:
  15.             return func

  16.         # Map function argument names to supplied types
  17.         sig = signature(func)
  18.         # 对参数进行绑定
  19.         bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments   #-> OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])

  20.         @wraps(func)
  21.         def wrapper(*args, **kwargs):
  22.             bound_values = sig.bind(*args, **kwargs)  # ->  <BoundArguments (x=1, y=2, z=3)>
  23.             # Enforce type assertions across supplied arguments
  24.             for name, value in bound_values.arguments.items():
  25.                 if name in bound_types:
  26.                     if not isinstance(value, bound_types[name]):
  27.                         raise TypeError(
  28.                             'Argument {} must be {}'.format(name, bound_types[name])
  29.                         )
  30.             return func(*args, **kwargs)

  31.         return wrapper

  32.     return decorate


  33. # 我们会发现这个装饰器相当灵活,既允许指定函数参数的所有类型,也可以只指定一部分子集。
  34. #       此外,类型既可以通过位置参数来指定,也可以通过关键字参数来指定。
  35. @typea_ssert(int, z=int)
  36. def spam(x, y, z=42):
  37.     pass

  38. spam(1, 2, 3) # Success
  39. spam(1, "Alex", "bye") # Error
复制代码


这节展示了一个高级的装饰器例子,引入了一些重要且有用的概念。首先,装饰器的一个特性就是它们只会在函数定义的时候应用一次。在某些情况下,我们可能想禁止由装饰器添加的功能。为了做到这点,只要让装饰器函数返回那个未经过包装的函数即可。在解决方案中,如果全局变量__debug__被设为False,下列代码就会返回未修改过的函数(当Python解释器以-O或-OO的优化模式执行的话,则属于这种情况)。接下来,编写这个装饰器比较棘手的地方在于要涉及对被包装函数的参数签名做检查。在这里,我们可选择的工具应该是inspect.signature()函数。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-16 21:40:55 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 17:15 编辑

08节 -_-  将装饰器定义为类的一部分

在类中定义一个装饰器是很直接的,但是首先我们需要理清装饰器将以什么方式来应用。具体来说就是以实例方法还是以类方法的形式应用。

  1. from functools import wraps


  2. class FunctionEmbellish(object):

  3.     def function_one(self, function):
  4.         """This is the instance decorator for FunctionEmbellish"""

  5.         @wraps(function)
  6.         def wrapper(*args, **kwargs):
  7.             print("Tis is FunctionEmbellish instance decorator")
  8.             return function(*args, **kwargs)

  9.         return wrapper

  10.     @classmethod
  11.     def function_two(cls, function):
  12.         """This is the static method"""

  13.         @wraps(function)
  14.         def wrapper(*args, **kwargs):
  15.             print("Tis is FunctionEmbellish static method")
  16.             return function(*args, **kwargs)

  17.         return wrapper


  18. # 应用展示,你应该要知道,有一个装饰器是来自实例化方法
  19. embellish = FunctionEmbellish()


  20. @embellish.function_one
  21. def spam():
  22.     ...


  23. @FunctionEmbellish.function_two
  24. def grok():
  25.     ...
复制代码


在类中定义装饰器乍看起来可能有些古怪,但是在标准库中也可以找到这样的例子。尤其是,内建的装饰器@property实际上是一个拥有getter()、setter()和deleter()方法的类, 每个方法都可作为一个装饰器。
示例如下:

  1. class Person:
  2.     # Create a property instance
  3.     first_name = property()

  4.     # Apply decorator methods
  5.     @first_name.getter
  6.     def first_name(self):
  7.         return self._first_name

  8.     @first_name.setter
  9.     def first_name(self, value):
  10.         if not isinstance(value, str):
  11.             raise TypeError('Expected a string')
  12.         self._first_name = value
复制代码


在类中定义装饰器有个难理解的地方就是对于额外参数 self 或 cls 的正确使用。 尽管最外层的装饰器函数比如 function_one() 或 function_two() 需要提供一个 self 或 cls 参数, 但是在两个装饰器内部被创建的 wrapper() 函数并不需要包含这个 self 参数。 你唯一需要这个参数是在你确实要访问包装器中这个实例的某些部分的时候。其他情况下都不用去管它。

另外额外注意!!就是在涉及到继承的时候。 例如,假设你想让在A中定义的装饰器作用在子类B中。你需要像下面这样写:

  1. class B(A):
  2.     @A.decorator2
  3.     def bar(self):
  4.         pass
复制代码

也就是说,装饰器要被定义成类方法并且你必须显式的使用父类名去调用它。 你不能使用 @B.decorator2 ,因为在方法定义时,这个类B还没有被创建。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-16 21:47:10 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 18:18 编辑

09节 -_- 将装饰器定义为类

问题->我们想用装饰器来包装函数,但是希望得到的结果是一个可调用的实例。
        我们需要装饰器既能在类中工作,也可以在类外部使用。

解决方法->要把装饰器定义成类实例,需要确保在类中实现__call__()和__get__()方法。
    例如,下面的代码定义了一个类,可以在另一个函数上添加一个简单的性能分析层:


  1. import types
  2. from functools import wraps


  3. class Profiled:
  4.     def __init__(self, func):
  5.         wraps(func)(self)
  6.         self.ncalls = 0

  7.     def __call__(self, *args, **kwargs):
  8.         self.ncalls += 1
  9.         return self.__wrapped__(*args, **kwargs)

  10.     def __get__(self, instance, cls):
  11.         if instance is None:
  12.             return self
  13.         else:
  14.             return types.MethodType(self, instance)


  15. # 应用展示
  16. @Profiled
  17. def add(x, y):
  18.     return x + y


  19. class Span:
  20.     @Profiled
  21.     def bar(self, x):
  22.         print(self, x)


  23. print("--" * 5)
  24. print(add(2, 3))
  25. print(add(2, 3))
  26. print(add.ncalls) # add 调用两次 -> return 2

  27. print("--" * 5)
  28. s = Span()
  29. s.bar(2)
  30. print(s.bar.ncalls)  # bar 调用一次 -> return 1
复制代码


把装饰器定义成类通常是简单明了的。
但是,这里有一些相当微妙的细节值得做进一步的解释,尤其是计划将装饰器应用在实例的方法上时。
首先,这里对functools.wraps()函数的使用和在普通装饰器中的目的一样——意在从被包装的函数中拷贝重要的元数据到可调用实例中。
其次,解决方案中所展示的__get__()方法常常容易被忽视。如果省略掉__get__()并保留其他所有的代码,会发现当尝试调用被装饰的实例方法时会出现怪异的行为。
例如:


  1. >>> s = Spam()
  2. >>> s.bar(3)
  3. Traceback (most recent call last):
  4. ...
  5. TypeError: spam() missing 1 required positional argument: 'x'
复制代码


出错的原因在于每当函数实现的方法需要在类中进行查询时,作为描述符协议(descriptor protocol)的一部分,它们的__get__()方法都会被调用,这部分内容在8.9节中已描述过。在这种情况下,__get__()的目的是用来创建一个绑定方法对象(最终会给方法提供self参数)。
下面的例子说明了其中的机理:

  1. >>> s = Spam()
  2. >>> def grok(self, x):
  3. ...     pass
  4. ...
  5. >>> grok.__get__(s, Spam)
  6. <bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
  7. >>>
复制代码


在本节中,__get__()方法在这里确保了绑定方法对象会恰当地创建出来。type. MethodType()手动创建了一个绑定方法在这里使用。绑定方法只会在使用到实例的时候才创建。如果在类中访问该方法,__get__()的instance参数就设为None,直接返回Profiled实例本身。这样就使得获取实例的ncalls属性成为可能。
如果想在某些方面避免这种混乱,可以考虑装饰器的替代方案,即(05节 -_- 可自定义属性的装饰器)中描述过的利用闭包和nonlocal变量。示例如下:


  1. def profiled(func):
  2.     ncalls = 0
  3.     @wraps(func)
  4.     def wrapper(*args, **kwargs):
  5.         nonlocal ncalls
  6.         ncalls += 1
  7.         return func(*args, **kwargs)
  8.     wrapper.ncalls = lambda: ncalls
  9.     return wrapper

  10. # Example
  11. @profiled
  12. def add(x, y):
  13.     return x + y
复制代码
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-16 22:36:47 | 显示全部楼层

10节 -_- 为类和静态方法提供装饰器

本帖最后由 Stubborn 于 2020-1-17 18:23 编辑

10节 -_- 为类和静态方法提供装饰器

将装饰器作用到类和静态方法上是简单而直接的,但是要保证装饰器在应用的时候需要放在@classmethod和@staticmethod之前。

示例如下:

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3
  3. import time
  4. from functools import wraps


  5. # A simple decorator
  6. def timethis(func):
  7.     @wraps(func)
  8.     def wrapper(*args, **kwargs):
  9.         start = time.time()
  10.         r = func(*args, **kwargs)
  11.         end = time.time()
  12.         print(end - start)
  13.         return r

  14.     return wrapper


  15. # Class illustrating application of the decorator to different kinds of methods
  16. class Spam:
  17.     @timethis
  18.     def instance_method(self, n):
  19.         print(self, n)
  20.         while n > 0:
  21.             n -= 1

  22.     @classmethod
  23.     @timethis
  24.     def class_method(cls, n):
  25.         print(cls, n)
  26.         while n > 0:
  27.             n -= 1

  28.     @staticmethod
  29.     @timethis
  30.     def static_method(n):
  31.         print(n)
  32.         while n > 0:
  33.             n -= 1
复制代码


如果装饰器的顺序搞错了,那么将得到错误提示。例如,如果像下面这样使用装饰器:
  1. class Spam:
  2.     ...
  3.     @timethis
  4.     @staticmethod
  5.     def static_method(n):
  6.         print(n)
  7.         while n > 0:
  8.             n -= 1
  9. >>> Spam.static_method(1000000)
  10. Traceback (most recent call last):
  11.   File "<stdin>", line 1, in <module>
  12.   File "timethis.py", line 6, in wrapper
  13.     start = time.time()
  14. TypeError: 'staticmethod' object is not callable
  15. >>>
复制代码


这里的问题在于@classmethod和@staticmethod并不会实际创建可直接调用的对象。相反,它们创建的是特殊的描述符对象。因此,如果尝试在另一个装饰器中像函数那样使用它们,装饰器就会崩溃。确保这些装饰器出现在@classmethod和@staticmethod之前就能解决这个问题。

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-16 23:24:00 | 显示全部楼层

11节 -_- 装饰器为被包装函数增加参数

本帖最后由 Stubborn 于 2020-1-17 19:26 编辑

11节 -_- 装饰器为被包装函数增加参数

先前介绍,`inspect.signature` 这个函数允许我们从一个可调用对象中提取出参数签名信息。同时这个模块可以给我们创建函数的签名信息。示例如下:


  1. from inspect import Signature, Parameter

  2. parms = [
  3.     Parameter("x",Parameter.POSITIONAL_OR_KEYWORD),
  4.     Parameter("y",Parameter.POSITIONAL_OR_KEYWORD, default=5),
  5. ]
  6. s = Signature(parms)
  7. print(s)
复制代码


我们既可以,获取到函数的签名(参数),又可以重新创建签名。所以编写一个装饰器为被包装的函数添加额外的参数,而且添加的参数不能影响到该函数已有的调用约定。就会变的简单起来示例如下。
  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3

  3. from functools import wraps
  4. import inspect

  5. def optional_debug(func):
  6.     """"""
  7.     # 检查fun参数,是否带debug参数
  8.     arg = inspect.signature(func).parameters.get("debug", None)
  9.     if arg is not None :
  10.         raise TypeError("debug argument already defined")

  11.     @wraps(func)
  12.     def wrapper(*args, debug=False, **kwargs):
  13.         if debug: print("Calling", func.__name__)
  14.         return func(*args, **kwargs)

  15.     # 管理函数签名
  16.     sig = inspect.signature(func)
  17.     params = list(sig.parameters.values())
  18.     params.append(inspect.Parameter(
  19.         "debug",
  20.         inspect.Parameter.KEYWORD_ONLY,
  21.         default=False
  22.     ))
  23.     # 把原有的函数签名,附加到wrapper上
  24.     wrapper.__signature__ = sig.replace(parameters=params)
  25.     return wrapper


  26. @optional_debug
  27. def spam(a, b, c):
  28.     return a+b+c

  29. print(inspect.signature(spam))
  30. print(spam(1, 2, 3))
  31. print(spam(1, 2, 3, debug=True))
复制代码


想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-16 23:28:55 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 20:15 编辑

12节 -_- 利用装饰器给类定义打补丁

你想通过反省或者重写类定义的某部分来修改它的行为,但是你又不希望使用继承或元类的方式。

这种情况可能是类装饰器最好的使用场景了。例如,下面是一个重写了特殊方法 __getattribute__ 的类装饰器, 可以打印日志:

  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3

  3. def log_getattribute(cls):
  4.     """Get the original implementation"""
  5.     orig_getattribute = cls.__getattribute__

  6.     # Make a new definition
  7.     def new_getattribute(self, name):
  8.         print(f"getting: {name}")
  9.         return orig_getattribute(self, name)

  10.     # Attach to the class and return
  11.     cls.__getattribute__ = new_getattribute
  12.     return cls

  13. # Example use
  14. @log_getattribute
  15. class test:
  16.     def __init__(self, x):
  17.         self.x = x

  18.     def spam(self):
  19.         """"""

  20. T = test(3)
  21. print(T.x)
  22. T.spam()
复制代码


或者你也可以使用继承,但是为了去理解它,你就必须知道方法调用顺序、super() 以及其它的继承知识。 某种程度上来讲,类装饰器方案就显得更加直观,并且它不会引入新的继承体系。它的运行速度也更快一些, 因为他并不依赖 super() 函数。

如果你系想在一个类上面使用多个类装饰器,那么就需要注意下顺序问题。 例如,一个装饰器A会将其装饰的方法完整替换成另一种实现, 而另一个装饰器B只是简单的在其装饰的方法中添加点额外逻辑。 那么这时候装饰器A就需要放在装饰器B的前面。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:26:57 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 20:32 编辑

13节 -_- 使用元类控制实例的创建

你想通过改变实例创建方式来实现单例、缓存或其他类似的特性。

元类(继承自type)可以控制类的实例化步骤,分别实现类的缓存,和单例类


  1. import weakref

  2. class Cached(type): # 缓存实例
  3.     def __init__(self, *args, **kwargs):
  4.         super().__init__(*args, **kwargs)
  5.         self.__cache = weakref.WeakValueDictionary()

  6.     def __call__(self, *args):
  7.         if args in self.__cache:
  8.             return self.__cache[args]
  9.         else:
  10.             obj = super().__call__(*args)
  11.             self.__cache[args] = obj
  12.             return obj

  13. # Example
  14. class Spam(metaclass=Cached):
  15.     def __init__(self, name):
  16.         print('Creating Spam({!r})'.format(name))
  17.         self.name = name
  18. >>> a = Spam('Guido')
  19. Creating Spam('Guido')
  20. >>> b = Spam('Diana')
  21. Creating Spam('Diana')
  22. >>> c = Spam('Guido') # Cached
  23. >>> a is b
  24. False
  25. >>> a is c # Cached value returned
  26. True
  27. >>>
复制代码

  1. class Singleton(type):
  2.     def __init__(self, *args, **kwargs):
  3.         self.__instance = None
  4.         super().__init__(*args, **kwargs)

  5.     def __call__(self, *args, **kwargs):
  6.         if self.__instance is None:
  7.             self.__instance = super().__call__(*args, **kwargs)
  8.             return self.__instance
  9.         else:
  10.             return self.__instance

  11. # Example
  12. class Spam(metaclass=Singleton): # 单例类
  13.     def __init__(self):
  14.         print('Creating Spam')

  15. >>> a = Spam()
  16. Creating Spam
  17. >>> b = Spam()
  18. >>> a is b
  19. True
  20. >>> c = Spam()
  21. >>> a is c
  22. True
  23. >>>
复制代码


想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:39:20 | 显示全部楼层
本帖最后由 Stubborn 于 2020-1-17 21:17 编辑

14节 -_-  捕获类的属性定义顺序

你想自动记录一个类中属性和方法定义的顺序, 然后可以利用它来做很多操作(比如序列化、映射到数据库等等)。

利用元类可以很容易的捕获类的定义信息。下面是一个例子,使用了一个OrderedDict来记录描述器的定义顺序:


  1. # -*- coding: utf-8 -*-
  2. # !/usr/bin/python3

  3. from collections import OrderedDict


  4. # 一组用于各自类型的描述符

  5. class Typed:
  6.     _expected_type = type(None)

  7.     def __init__(self, name=None):
  8.         self._name = name

  9.     def __set__(self, instance, value):
  10.         if not isinstance(value, self._expected_type):
  11.             raise TypeError(f"Expected {self._expected_type}")
  12.         instance.__dict__[self._name] = value


  13. class Integer(Typed):
  14.     _expected_type = int


  15. class Float(Typed):
  16.     _expected_type = float


  17. class String(Typed):
  18.     _expected_type = str


  19. # Metaclass that uses an OrderedDict for class body
  20. class OrderedMeta(type):

  21.     def __new__(cls, name, bases, attrs):
  22.         d = dict(attrs)
  23.         order = []
  24.         for name, value in attrs.items():
  25.             if isinstance(value, Typed):
  26.                 value._name = name
  27.                 order.append(name)
  28.         d["_order"] = order
  29.         return type.__new__(cls, name, bases, d)

  30.     @classmethod
  31.     def __prepare__(mcs, name, bases):
  32.         return OrderedDict()
复制代码


在这个元类中,执行类主体时描述器的定义顺序会被一个 OrderedDict``捕获到, 生成的有序名称从字典中提取出来并放入类属性 ``_order 中。这样的话类中的方法可以通过多种方式来使用它。 例如,下面是一个简单的类,使用这个排序字典来实现将一个类实例的数据序列化为一行CSV数据:

  1. class Structure(metaclass=OrderedMeta):
  2.     def as_csv(self):
  3.         return ','.join(str(getattr(self,name)) for name in self._order)


  4. class Stock(Structure):
  5.     name = String()
  6.     shares = Integer()
  7.     price = Float()
  8.     def __init__(self, name, shares, price):
  9.         self.name = name
  10.         self.shares = shares
  11.         self.price = price

  12. s = Stock("Good", 100, 101.1)
  13. print(s.as_csv())
复制代码


想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:41:29 | 显示全部楼层
14节 -_-  捕获类的属性定义顺序
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:46:04 | 显示全部楼层
15节 -_- 定义有可选参数的元类
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:47:30 | 显示全部楼层
16节 -_- *args和**kwargs的强制参数签名
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

 楼主| 发表于 2020-1-17 19:48:06 | 显示全部楼层
17节
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

 楼主| 发表于 2020-1-17 19:48:38 | 显示全部楼层
19节
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-20 08:33

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表