在了解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)
- with DataConn(db) as conn调用DataConn(db)时自动执行enter()函数,并返回一个连接对象self.conn赋值给as后面的变量c
- 然后创建一个游标,之后你就可以做一些sql查询等操作了。
- 做完这些操作之后,也就是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)
- 从这个函数中我们可以总结出该格式: 以
yield
分段,前面部分是__enter__
的逻辑,后面部分是__exit__
的逻辑。 - with下面的语句全部执行完成并且没有异常后,由with内部机制触发exit逻辑,也就是yield后面部分,即关闭连接。
- 上面的小例子并没有加入try...finally,更完整的格式请看第三部分。
with语句什么时候会触发上下文管理器中的exit()函数呢?
with包含的语句全部执行完成并且没有异常时
- 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
执行流程:
- 首先用@contextmanager定义了一个connection上下文管理器对象
- 登录请求来时,会走LDAPBackend中的authenticate方法,方法里面通过with处理connection上下文管理器对象
- with语句会将上下文管理器对象中的yield后面的值返回并赋值给as 后面的c变量,也就是将Connection(c)赋值给c
- 然后继续向下执行with执行体中的代码,直到执行完return c.get_user(username),with语句会返回到connection上下文管理器对象执行exit()清理工作,也就是yield Connection(c)后面的所有代码即c.unbind(),解除连接
- 为什么要在connection上下文管理器对象中用try 处理yield Connection(c)呢?别忘了我们之前讲的exit()函数执行时机。当yield Connection(c)一旦出现异常,将不会再执行exit,也就不会执行yield后面的代码即c.unbind()。因此要try。