一文搞懂yield生成器、@contextmanager 装饰器、with语句、以及实际开发中三者的结合使用

在了解contextmanager 之前有必要解释下生成器的概念:

一、生成器

生成器:

生长器是一个可迭代对象,主要用于生成一个序列,能够用next()获取生成器的下一个值,在需要生成的序列元素较多时,使用生成器可以节省内存空间。

生成器与普通函数的本质区别:

普通函数一次返回所有结果(比如一个包含1亿个值的列表),而生成器只有你调用next()时,才会返回一个值,并且终止生成器的运行,记住这个值的位置。当再次调用next()时,从上次终止的位置继续执行,返回下一个值...

那么生成器长什么样呢?

生成器本质就是一个函数,只不过用yield代替return来返回值。

def fun():
    for i in range(20):
        print('start')
        yield i
        print('good',i)

if __name__ == '__main__':
    a=fun()   #创建一个生成器
    a.__next__()或next(a)   #用next取值,输出‘start’,然后执行x=yield i 并终止函数运行并返回i
    a.__next__()或next(a)   #再次调用next,从上次终止的位置继续执行,输出'good',0  然后再次输出‘start’,执行x=yield i 并终止函数运行并返回i 

结论:

yield与return类似,会返回一个值,终止函数运行,但是yield能记住上次函数运行的位置,再次调用函数时,会从上次终止位置继续运行。

 

 

二、with语句

下面再讲with语句:

用上下文管理器封装语句的执行,使原来的try...except...finally变得更加方便。即通过使用with语句,可以自动调用对象的资源释放、异常捕获等操作,减少用户手动调用的繁琐与可能遗漏的危险。

比如你经常用到的with open ()  as file

手动实现with上下文管理器对象

那么如何成为with语句支持的上下文管理器类呢?你只需要在类中实现__enter__()__exit__()两个方法。

import sqlite3

class DataConn:
    def __init__(self,db_name):
        self.db_name = db_name

    def __enter__(self):
        self.conn = sqlite3.connect(self.db_name)
        return self.conn

    def __exit__(self,exc_type,exc_val,exc_tb):
        self.conn.close()
        if exc_val:
            raise

if __name__ == "__main__":
    db = "test/test.db"
    with DataConn(db) as c:
        cursor = c.cursor()
                .
                .
                .
        (do some you sql action) 
  1. with DataConn(db) as conn调用DataConn(db)时自动执行enter()函数,并返回一个连接对象self.conn赋值给as后面的变量c
  2. 然后创建一个游标,之后你就可以做一些sql查询等操作了。
  3. 做完这些操作之后,也就是with下面的语句全部执行完成并且没有异常后,就会执行exit()函数,来关闭数据库连接。

 

装饰器实现with上下文管理器对象

python自带的contextlib.contextmanager装饰器配合yield关键字就可以可轻松实现。但是contextmanager 只是省略了 __enter__() / __exit__() 的编写,但并不负责实现资源的“获取”和“清理”工作;“获取”操作需要定义在 yield 语句之前,“清理”操作需要定义 yield 语句之后,这样 with 语句在执行 __enter__() / __exit__() 方法时会执行这些语句以获取/释放资源。

from contextlib import contextmanager

@contextmanager
def connection(db_name):
    conn = sqlite3.connect(db_name)
    yield coon
    conn.close()


with connection("test/test.db") as c:
    cursor = c.cursor()
        .
        .
        .
    (do some you sql action)
  1. 从这个函数中我们可以总结出该格式: 以yield分段,前面部分是__enter__的逻辑,后面部分是__exit__的逻辑。
  2. with下面的语句全部执行完成并且没有异常后,由with内部机制触发exit逻辑,也就是yield后面部分,即关闭连接。
  3. 上面的小例子并没有加入try...finally,更完整的格式请看第三部分。

with语句什么时候会触发上下文管理器中的exit()函数呢?

  1. with包含的语句全部执行完成并且没有异常时
  2. with包含的语句遇到return,break,continue结束函数运行时

 

 

三、实际开发运用

下面例子是一个ldap认证的一个过程

@contextmanager
def connection(username, password):
    # Connect.
    auto_bind = ldap3.AUTO_BIND_NO_TLS
    try:
        c = ldap3.Connection(
            ldap3.Server(
                settings.LDAP_AUTH_URL,
                allowed_referral_hosts=[("*", True)],
                get_info=ldap3.NONE,
                connect_timeout=settings.LDAP_AUTH_CONNECT_TIMEOUT,
            ),
            user=username,
            password=password,
            auto_bind=auto_bind,
            raise_exceptions=True,
            receive_timeout=settings.LDAP_AUTH_RECEIVE_TIMEOUT,
        )
    except LDAPException as ex:
        logger.warning("LDAP connect failed: {ex}".format(ex=ex))
        yield None
        return
    # Return the connection.
    logger.info("LDAP connect succeeded")
    try:
        yield Connection(c)
    finally:
        c.unbind()



class LDAPBackend(ModelBackend):

    def authenticate(self, request, username=None, password=None, **kwargs):
        if not all([password, username]):
            return None
        # Connect to LDAP.
        username = username + '@' + settings.LDAP_AUTH_ACTIVE_DIRECTORY_DOMAIN

        with connection(username, password) as c:
            if c is None:
                return None
            return c.get_user(username)


class Connection(object):

    def __init__(self, connection):
        self._connection = connection

    def _get_or_create_user(self, user_data):
        attributes = user_data.get("attributes")
        if attributes is None:
            logger.warning("LDAP user attributes empty")
            return None

        User = get_user_model()

        # Update or create the user.
        username = attributes['userPrincipalName'][0]
        email = attributes['mail'][0]
        user_fields = {'username': username, 'email': email, 'is_staff': True,}
        user_lookup = {'username': username}
        user, created = User.objects.update_or_create(
            defaults=user_fields,
            **user_lookup,
        )
        if created:
            user.set_unusable_password()
            interviewer_group = Group.objects.filter(name=settings.CONSTANTS.get('INTERVIEWER_GROUP_NAME')).first()
            if interviewer_group:
                user.groups.add(interviewer_group)
            user.save()
        # All done!
        logger.info("LDAP user lookup succeeded")
        return user

    def get_user(self, username):
        if self._connection.search(
            search_base=settings.LDAP_AUTH_SEARCH_BASE,
            search_filter="(&(userPrincipalName={}))".format(username),
            search_scope=ldap3.SUBTREE,
            attributes=ldap3.ALL_ATTRIBUTES,
            get_operational_attributes=True,
            size_limit=1,
        ):
            return self._get_or_create_user(self._connection.response[0])
        logger.warning("LDAP user lookup failed")
        return None


执行流程:

  1. 首先用@contextmanager定义了一个connection上下文管理器对象
  2. 登录请求来时,会走LDAPBackend中的authenticate方法,方法里面通过with处理connection上下文管理器对象
  3. with语句会将上下文管理器对象中的yield后面的值返回并赋值给as 后面的c变量,也就是将Connection(c)赋值给c
  4. 然后继续向下执行with执行体中的代码,直到执行完return c.get_user(username),with语句会返回到connection上下文管理器对象执行exit()清理工作,也就是yield Connection(c)后面的所有代码即c.unbind(),解除连接
  5. 为什么要在connection上下文管理器对象中用try 处理yield Connection(c)呢?别忘了我们之前讲的exit()函数执行时机。当yield Connection(c)一旦出现异常,将不会再执行exit,也就不会执行yield后面的代码即c.unbind()。因此要try。

 

 

 

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值