社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

说说 Python 的元编程

Python猫 • 4 年前 • 632 次点击  

提到元这个字,你也许会想到元数据,元数据就是描述数据本身的数据,元类就是类的类,相应的元编程就是描述代码本身的代码,元编程就是关于创建操作源代码(比如修改、生成或包装原来的代码)的函数和类。主要技术是使用装饰器、元类、描述符类。

本文的主要目的是向大家介绍这些元编程技术,并且给出实例来演示它们是怎样定制化源代码的行为。

装饰器

装饰器就是函数的函数,它接受一个函数作为参数并返回一个新的函数,在不改变原来函数代码的情况下为其增加新的功能,比如最常用的计时装饰器:

from functools import wraps

def timeit(logger=None):
    """
    耗时统计装饰器,单位是秒,保留 4 位小数
    """


    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            start = time.time()
            result = func(*args, **kwargs)
            end = time.time()
            if logger:
                logger.info(f"{func.__name__} cost {end - start :.4f} seconds")
            else:
                print(f"{func.__name__} cost {end - start :.4f} seconds")
            return result

        return wrapper

    return decorator

(注:比如上面使用 @wraps(func) 注解是很重要的, 它能保留原始函数的元数据) 只需要在原来的函数上面加上 @timeit() 即可为其增加新的功能:

@timeit()
def test_timeit():
    time.sleep(1)

test_timeit()
#test_timeit cost 1.0026 seconds

上面的代码跟下面这样写的效果是一样的:

test_timeit = timeit(test_timeit)
test_timeit()

装饰器的执行顺序

当有多个装饰器的时候,他们的调用顺序是怎么样的?

假如有这样的代码,请问是先打印 Decorator1 还是 Decorator2 ?

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

add(1,2)

# Decorator 1
# Decorator 2

回答这个问题之前,我先给你打个形象的比喻,装饰器就像函数在穿衣服,离它最近的最先穿,离得远的最后穿,上例中 decorator1 是外套,decorator2 是内衣。




    
add = decorator1(decorator2(add))

在调用函数的时候,就像脱衣服,先解除最外面的 decorator1,也就是先打印 Decorator1,执行到 return func(*args, **kwargs) 的时候会去解除 decorator2,然后打印 Decorator2,再次执行到 return func(*args, **kwargs) 时会真正执行 add() 函数。

需要注意的是打印的位置,如果打印字符串的代码位于调用函数之后,像下面这样,那输出的结果正好相反:

def decorator1(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print('Decorator 1')
        return result
    return wrapper

def decorator2(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print('Decorator 2')
        return result
    return wrapper

装饰器不仅可以定义为函数,也可以定义为类,只要你确保它实现了__call__() 和 __get__() 方法。

关于装饰器的其他用法,可以参考前文:

元类

Python 中所有类(object)的元类,就是 type 类,也就是说 Python 类的创建行为由默认的 type 类控制,打个比喻,type 类是所有类的祖先。我们可以通过编程的方式来实现自定义的一些对象创建行为。

定一个类继承 type 类 A,然后让其他类的元类指向 A,就可以控制 A 的创建行为。典型的就是使用元类实现一个单例:

class Singleton(type):
    def __init__(self, *args, **kwargs):
        self._instance = None
        super().__init__(*args, **kwargs)

    def __call__(self, *args, **kwargs):
        if self._instance is None:
            self._instance = super().__call__(*args, **kwargs)
            return self._instance
        else:
            return self._instance


class Spam(metaclass=Singleton):
    def __init__(self):
        print("Spam!!!")

元类 Singleton 的__init__和__new__ 方法会在定义 Spam 的期间被执行,而 __call__方法会在实例化 Spam 的时候执行。

如果想更好的理解元类,可以阅读Python黑魔法之metaclass

descriptor 类(描述符类)

descriptor 就是任何一个定义了 __get__(),__set__() 或 __delete__()的对象,描述器让对象能够自定义属性查找、存储和删除的操作。这里举官方文档[1]一个自定义验证器的例子。

定义验证器类,它是一个描述符类,同时还是一个抽象类:

from abc import ABC, abstractmethod

class Validator(ABC):

    def __set_name__(self, owner, name):
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    def validate(self, value):
        pass

自定义验证器需要从 Validator 继承,并且必须提供 validate() 方法以根据需要测试各种约束。

这是三个实用的数据验证工具:

OneOf 验证值是一组受约束的选项之一。

class OneOf(Validator):

    def __init__(self, *options):
        self.options = set(options)

    def validate(self, value):
        if value not in self.options:
            raise ValueError(f'Expected {value!r} to be one of {self.options!r}')

Number 验证值是否为 int 或 float。根据可选参数,它还可以验证值在给定的最小值或最大值之间。

class Number(Validator):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value             raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )

String 验证值是否为 str。根据可选参数,它可以验证给定的最小或最大长度。它还可以验证用户定义的 predicate。

class String(Validator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and  len(value)             raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

实际应用时这样写:

class Component:

    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('wood', 'metal', 'plastic')
    quantity = Number(minvalue=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

描述器阻止无效实例的创建:

>>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
    ...
ValueError: Expected 'isupper' of 'str' objects> to be true for 'Widget'

>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
    ...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

>>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
Traceback (most recent call last):
    ...
ValueError: Expected -5 to be at least 0
>>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
Traceback (most recent call last):
    ...
TypeError: Expected 'V' to be an int or float

>>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid

最后的话

关于 Python 的元编程,总结如下:

如果希望某些函数拥有相同的功能,希望不改变原有的调用方式、不写重复代码、易维护,可以使用装饰器来实现。

如果希望某一些类拥有某些相同的特性,或者在类定义实现对其的控制,我们可以自定义一个元类,然后让它类的元类指向该类。

如果希望实例的属性拥有某些共同的特点,就可以自定义一个描述符类。

参考资料

[1]

管方文档: https://docs.python.org/zh-cn/3/howto/descriptor.html

Python猫技术交流群开放啦!群里既有国内一二线大厂在职员工,也有国内外高校在读学生,既有十多年码龄的编程老鸟,也有中小学刚刚入门的新人,学习氛围良好!想入群的同学,请在公号内回复『交流群』,获取猫哥的微信(谢绝广告党,非诚勿扰!)~


还不过瘾?试试它们




▲趣漫画:Java 对 Python 的渗透能成功吗?

▲Python 神器 Celery 源码解析(2)

▲Python进阶:探秘描述符的工作原理

▲Python 之父爆料:明年至少令 Python 提速 1 倍!

▲Python 如何仅用 5000 行代码,实现强大的 logging 模块?

▲Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?


如果你觉得本文有帮助
请慷慨分享和点赞,感谢啦!
Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/123428