1、上下文管理的内容
在我初步的认知里,上下文管理,就是with,用处就是;为了减少代码量,提高代码可读性,同时避免犯一些缺少exit的错误。当然不止这些,但现在,我们只需要知道这些就已经足够。
上下文管理,首先需要一个类,这个类里面必须有两个方法:__enter__和__exit__。当一个类中含有这两个方法,那么解释器就认为这个类是一个上下文管理器,它遵循上下文管理协议。也就是说,上下文管理器是一类类。
当程序运行到含有with的一行时:
with f1 as a:
f2
后台实际运行如下:
f1
__enter__
f2
__exit__
这是一般的情形。在实际使用中,可以缺少f1,可以缺少as a,但是__enter__和__exit__是一定不能缺少的。
这里的变量a是由__enter__这个函数的返回值确定的。
举例如下,我们建立一个类:
class HaveATry:
def __init__(self)->None:
print('1')
def __enter__(self)->None:
print('2')
def __exit__(self,a,b,c)->None:
print('3')
很简单,在创建一个对象时打印1,在执行enter时打印2,在执行exit时打印3。注意一个细节,none必须写作None,全小写会报错。
注意一点,前面两个函数的参数可以只有self,但exit必须有四个参数:self、exc_type、exc_value、exc_trace。后面三个是用于异常处理的,暂时不用去管。
当我们执行with,有init部分时,如下:
印证了上面的顺序。
如果不用init,那么为了让with知道它在管理谁的的上下文,我们需要先建立一个对象,然后with这个对象即可,如下:
这样我们就明白了with背后发生了什么。
2、用上下文管理改写webapp的log_request函数
我们先看原来的代码:
import mysql.connector
def log_request(req:'flask_request',res:str)->None:
dbconfig={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
conn=mysql.connector.connect(**dbconfig)
cursor=conn.cursor()
_INSERT="""insert into log
(phrase,letters,ip,browser_string,results)
values
(%s,%s,%s,%s,%s)"""
cursor.execute(_INSERT,(req.form['phrase'],
req.form['letters'],
req.remote_addr,
req.user_agent.browser,
res,))
conn.commit()
cursor.close()
conn.close()
可以分成四部分:
第一部分初始化:
dbconfig={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
用于配置建立连接所需的设定。
第二部分建立连接:
conn=mysql.connector.connect(**dbconfig)
cursor=conn.cursor()
用于客户端和服务器建立连接,并生成一个游标。
第三部分实现函数功能:
_INSERT="""insert into log
(phrase,letters,ip,browser_string,results)
values
(%s,%s,%s,%s,%s)"""
cursor.execute(_INSERT,(req.form['phrase'],
req.form['letters'],
req.remote_addr,
req.user_agent.browser,
res,))
第四部分断开连接:
conn.commit()
cursor.close()
conn.close()
接下来,我们要将原代码改写成一个上下文管理器,将上面几部分有选择性的写入类中。
我们将类起名为UseDatabase。代码如下:
import mysql.connector
class UseDatabase:
def __init__(self,dbconfig:dict)->None:
self.dbconfig=dbconfig
def __enter__(self)->'cursor':
self.conn=mysql.connector.connect(**self.dbconfig)
self.cursor=self.conn.cursor()
return self.cursor
def __exit__(self,exc_type,exc_value,exc_trace)->None:
self.conn.commit()
self.cursor.close()
self.conn.close()
有以下几点要注意:
第一个细节,初始化函数中,有一个参数是配置字典,它的类型应该为dict,要记住。
第二个,__enter__函数的返回类型是cursor,但不能把cursor直接写上去,而是要用单引号括起来然后写上去,因为cursor是一个变量,解释器看了会犯迷糊。
第三个,一切需要多次使用的变量都要写入类的属性表,如何写入?在声明它的时候在前面加self即可。如dbconfig在__init__中赋值后,在__enter__中再次使用;conn和cursor也是一样。因此,它们前面都要加self。
这样,就写好了我们的上下文管理器。可以按如下形式使用它:
可以看到,已经正确连接。
这样一来,我们在与数据库交互时,只需要配置初始化条件、写下需要的SQL代码,连接与断开都不需要去关注了。多么美好。
3、用上下文管理器改写webapp中的view_the_log函数
在之前,我们的view_the_log函数如下:
def view_the_log()->str:
contents=[]
with open('vsearch.log') as log:#with 不具有循环功能
for line in log:
contents.append([])
for item in line.split('|'):
contents[-1].append(escape(item))
#return str(contents)
titles=('Form Data','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
当时我们的数据处理过程是这样的:从request中选择需要的数据,以用‘|’分隔开的形式把数据存在一个txt文件中,读取的时候也从这个txt文件中读取,根据‘|’来分隔不同部分。现在,我们的数据存在SQL中,SQL本身即是结构化存储数据,因此不再需要我们去处理数据了,直接读取即可。
在改写之前,先学习一条SQL语句:
select phrase,letters,ip,browser_string,results from log
这是一句查询语句,从名为log的表中查找名为phrase,letters,ip,browser_string,results的列。
改写函数如下:
def view_the_log()->str:
dbconfig={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
#contents=[]
with UseDatabase(dbconfig) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
有以下几点要注意:
首先,上述代码是一整个函数。也就是说,作用域是所有代码。因此,不需要在with前先声明contents变量,最开始我这么写是怕with部分结束后contents会被销毁,实际上,with部分不是一个独立的作用域,因此不用这么做。
其次,执行_SELECT这句代码并不会返回一个列表什么的,需要我们自己用fetchall函数去取得这个列表。
最后,虽然我们的显示列表由四列变成了五列,但是并不需要更改html代码,它适用于任意数量行的表格。
现在我们注意到了一个问题,那就是dbconfig部分我们用了两次,能不能在函数外声明,让它只需要创建一次,然后多次调用呢?
当然可以。只是我们最好不要直接在外面建立这个字典。而是将其写入Flask自带的配置字典app.config中。代码如下:
app.config['dbconfig']={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
这是一个字典的字典,若我们想在函数中调用dbconfig字典,只需调用aopp.config['dbconfig']即可。如下:
app.config['dbconfig']={'host':'127.0.0.1',
'user':'vsearch',
'password':'vsearchpasswd',
'database':'vsearchlogDB',}
def log_request(req:'flask_request',res:str)->None:
#with open('vsearch.log','a') as log:
#print(req.form,req.remote_addr,req.user_agent,res,file=log,sep='|')
with UseDatabase(app.config['dbconfig']) as cursor:
_INSERT="""insert into log
(phrase,letters,ip,browser_string,results)
values
(%s,%s,%s,%s,%s)"""
cursor.execute(_INSERT,(req.form['phrase'],
req.form['letters'],
req.remote_addr,
req.user_agent.browser,
res,))
@app.route('/viewlog')
def view_the_log()->str:
#contents=[]
with UseDatabase(app.config['dbconfig']) as cursor:
_SELECT="""select phrase,letters,ip,browser_string,results from log"""
cursor.execute(_SELECT)
contents=cursor.fetchall()
titles=('Phrase','Letters','Remote_addr','User_agent','Results')
return render_template('viewlog.html',
the_title='View Log',
the_row_titles=titles,
the_data=contents,)
这样就实现了webapp使用SQL储存并查找数据。