【跟着Head First学python】10、函数修饰符:包装函数

1、web的状态与访问权限

我们在上一章已经完成了日志的记录与SQL的处理。并且可以通过viewlog网址查询所有的日志。但是现在问题来了:作为日志数据这样较为敏感较为有价值的信息,应该是所有人可以随意看到的吗?

不应该。我们应添加一个功能,使得只有认证用户可以查询日志。

最初的想法可以是这样的:在我们的webapp中维护一个全局变量,如果这个变量值为True,说明有权限;如果变量值为False,说明没有权限。

看上去好想可以用。

但是实际上不可以。为什么呢?

之前我们讨论过所谓的变量作用域。即使是代码中的全局变量,它的作用范围也仅限于该程序。你总不能让程序A中的全局变量a在程序B中也可以使用吧。那样程序B不就把程序A给绿了。这不和谐。

那这样,只要有用户在访问我的网站,我的程序A就一直运行,全局变量a就一直在作用域内,一直都是有效的。这样不行吗?

不行。为什么不行?因为你没法保证“程序A一直运行”。

在我们自己的电脑上,我们打开一个程序,不自己关闭它,它就一直在内存里,但是在服务器上不是。服务器会根据需要,随时运行你的代码,当然也可能会随时关闭。这样变量作用域就会时有时无,一会是True一会是False。这样用户可能一会可以看,一会又不能看。很麻烦。

那我们把这个变量保存在SQL中吧。需要验证权限的时候就读取一下。

这样是可行的,而且在你的网站仅有一个用户的时候完全可行。这个唯一的用户对应唯一的变量,根据用户是否登录与登出改变变量的值。听起来好惨

要是用户多了就很麻烦。每个用户都要维护这样一个变量,有一种大炮打蚊子的感觉。

为什么会出现这种情况呢?因为在服务器看来,使用web时保存变量是一个很浪费的操作,为了实现高并发,就很难有保存变量的效率空间了。二者不可得兼。web选择了效率,因此它自己不会保存变量。这里谈到的变量,专业词汇应该为“状态”。web是无状态的。

那我们要用什么方法实现权限检查啊?也就是说,我们要用什么方法分别保存多个用户的变量啊?

大多数web应用开发框架都提供了一种名为session(会话)的技术来满足这个需求。

可以把会话看作是无状态web上面的一种状态。

session的原理如下:服务器给用户一小段认证数据,cookie,并且在服务器上建立一个与cookie对应的一段认证数据,会话ID。这样就可以让每个用户都有和服务器的唯一连接,可以持久储存数据,而且不同的用户也不会混淆了。

2、session的机制

我们提供了一段成品代码来演示session的会话机制如何工作。

from flask import Flask,session

app=Flask(__name__)

app.secret_key='YouWillNeverGuess'#秘密密钥

@app.route('/setuser/<user>')
def setuser(user:str)->str:
    session['user']=user
    return 'User value set to: '+session['user']


@app.route('/getuser')
def getuser()->str:
    return 'User value is currently set to:'+session['user']

if __name__=='__main__':
    app.run(debug=True)

首先要从flask中导入session模块。不用对session很害怕,可以把session看作是一个全局的python字典,我们用到的功能就是保存变量罢了。flask可以保证,无论应用的代码加载和卸载多少次,session中的变量都能够一直存在。

其次,存储在session中的所有数据都有一个唯一的浏览器cookie作为密钥,这就确保了并不是web应用的每一个用户都能访问你的会话数据。为了得到这些密钥,我们需要为flask提供一个秘密密钥作为种子,flask会用这个秘密密钥加密所有的cookie,保护它不被外人刺探。也就是说,我作为一个用户,我的会话数据保存在session中,因为我有cookie,所以我可以看我的数据。我的cookie又是经过flask加密的,这样就减少了泄露的概率。

在设置完秘密密钥之后,就可以直接把session当做一个字典使用了。

@app.route('/setuser/<user>')

这行代码看起来很奇怪,因为之前我们的url中都没有尖括号包括的代码。它的用途是希望用户提供一个值来赋给user变量。得到user变量后,会在setuser函数中使用该变量,并在字典中保存这个值。之后但会保存成功的提示。

注意一点,虽然所有用户共用这一行保存变量的代码,而且表面上看session字典中的键都是user,但并不表示字典中只有一个名为user的键,这个键只有一个值。实际上,不同的用户有不同的键值,但是在这里不需要考虑键名问题,只需要当它是仅为一个用户服务的就好,其他的由session自己完成。

如果我们想查看user这个变量现在的值,访问/getuser这个url即可。它会调用一个函数访问字典中的user键,并返回它的值。

既然我们已经可以实现变量的保存和读取了,那么接下来可以进行测试了。

首先注意一点,web服务器把一个浏览器当做一个用户,因此你电脑上的firefox和chrome会被认作是两个用户。这样就很容易测试了:我们打开多个浏览器分别访问不同的/setuser/<user>,就会保存多个user值,然后在分别查看,如果它们的值不同,就说明已经成功的进行了分别保存。

效果如下:

首先设置第一个user为hry。使用chrome浏览器。

然后设置第二个、第三个user分别为chy、yrz。使用firefox和edge。

接下来分别查看这三个user的值,如下:

可以看出,不同的浏览器访问相同的网址,得到了不同的结果,说明变量保存成功,并且未发生混淆。

3、用session来控制登录

使用一个简单的实验代码来进行我们的探索:

from flask import Flask,session

app=Flask(__name__)

app.secret_key='YouWillNeverGuess'

@app.route('/')
def hello()->str:
    return 'Hello from the simple webapp.'

@app.route('/page1')
def page1()->str:
    return 'This is page 1.'

@app.route('/page2')
def page2()->str:
    return 'This is page 2.'

@app.route('/page3')
def page3()->str:
    return 'This is page 3.'

if __name__=='__main__':
    app.run(debug=True)

可以看到,代码创建了四个url。我们希望page1、page2、page3都只对登录用户可见。因此,我们首先要写一个登录页面。如下:

@app.route('/login')
def do_login()->str:
    session['logged_in']=True
    return 'You are now logged in'

很简单,实现的功能就是如果你访问了该url,则置session字典中的logged_in为True,并返回一个已登录的提示。

接下来写一个注销页面,如下:

@app.route('/logout')
def do_logout()->str:
    #session['logged_in'].clear()
    session.pop('logged_in')
    return 'You are now logged out.'

注销逻辑有两种:第一种是将logged_in的值由True改成False,第二种是直接删去logged_in。在这里我们选择后者。原因稍后再讲。另外注意一点,python中,删除字典的某一项使用的方法为pop,删除字典内所有数据才会用clear。

然后是一个查看当前状态的页面:

@app.route('/status')
def check_status()->str:
    #if session['logged_in']==True:
    if 'logged_in' in session:
        return 'You are currently logged in.'
    return 'You are NOT logged in.'

也有两种逻辑:第一种是判断键值是否为True,第二种是判断键值是否存在。同样选择第二种。为什么都选第二种呢?

因为python的字典机制:如果字典中某个键名不存在,就不能检查它的值。注销和查看状态中的第一种方法都会检查字典中某一键名的值,但是如果键名不存在呢?那样程序就会崩溃。而且我们无法要求用户一定是先访问login再访问status或logout,一旦不按这种顺序访问,网站就会出错,也不会返回You are NOT logged in的信息,那样就很不喜人。

因此我们选择直接判断是否存在,这样就避免了检查键值的操作。

接下来开始测试:

首先我们不登录:

返回正确。

接下来我们登录:

看状态:

注销:

看状态:

这样就简单实现了登陆的操作。

4、引入函数修饰符

在3中,我们实现了登陆与注销,对于status,登陆与注销后,它显示的内容不同,这就是限制访问url的雏形。现在想要实现对所有三个page实现这一点。

当然很容易,把status中的check_status代码在这三个url下面复制一下不就好了。

这样是好了,但也没好。为什么呢?

这样做难以维护,想一下要是我们要想改一下键名,或者更改返回的信息,那该多麻烦!

那把这写代码单独拎出来怎么样?写一个函数,这三个url下面只用一行代码,调用这个函数如何?

本质上没有区别,后者就是把复制黏贴几行代码变成了复制黏贴一行代码,仍然难以维护,而且这两者都会让代码真正要做的工作变得模糊:page1本来是返回一行通知的,你加的这个函数干嘛用啊?劣化了代码的可读性。

如果有一种方法,可以以某种方式为函数添加一个功能,比如说为page1、page2、page3这三个函数增加一个相同的检查状态功能就好了。为函数增加额外功能,这就是函数修饰符做的事情。

利用修饰符,可以用额外的代码增强现有的函数,从而改变现有函数的行为而不必修改它的代码。

也就是说,我现在有一个高达,给他加个喷气模块就能飞,而不用把它大卸八块从内而外的改造才能飞,这个喷气模块就是函数修饰符。

我们之前就有用过函数修饰符:所有的函数修饰符前面都有一个@作为前缀,它们很容易发现。

接下来我们将创建一个自己的函数修饰符。

5、创建函数修饰符的铺垫

创建函数修饰符,需要我们了解三个问题:

如何把一个函数作为参数传递到另一个函数?

如何从函数返回一个函数?

如何处理任意数量和类型的函数参数?

首先来解决第一个问题:如何把一个函数作为参数传递到另一个函数?

咋一眼好像很奇怪?函数的参数也可以是函数吗?当然可以。python中的一切都是对象,函数自然也是对象,可以通过下面的试验代码证明这一点:

hello是一个函数,id是另外一个函数,我们调用id(hello)也会得到一个结果,而不会报错。这里hello就是id的参数。但是注意一点,虽然hello是id的参数,但是id(hello)并没有调用hello,而只是返回了一个地址,实际上,函数可以选择是否调用它的函数参数。我们下面写一个调用函数参数的函数apply。如下:

def apply(func:object,value:object)->object:
    return func(value)

它的第一个参数func就是一个函数对象,这里的注解object可以帮助理解这一点,第二个参数value也是一个对象。虽然它们类型相同,但是通过名字可以看出,第一个参数应该是一个函数,而第二个参数应是第一个函数参数所需要的参数。这是约定俗成的起名方法。效果如下:

可以看到apply的确调用了它的函数参数,最后一个我是故意这么写的,有点好奇它会不会死循环。

然而知道参数可以是函数有什么用呢?这个问题暂时按下不表。接下来看第二个问题:如何从一个函数返回一个函数?

如果之前只学过C的话,这是不可想象的。C中return只能是一个值,连数据结构都不可以,怎么还可以是一个函数呢?的确可以。

为了从函数返回一个函数,首先来学习一下嵌套函数的知识。

嵌套函数,顾名思义,就是在一个函数中再定义一个函数,举例如下:

def outer():
    def inner():
        print("This is inner.")

    print("This is outer.")
    inner()

在outer内定义一个函数inner,然后就可以在outer内调用这个inner函数了。注意inner的作用域仅在outer内部,你在外面是无法调用inner的。

这有什么用呢?我把inner的代码写在调用的地方不就好了。

的确是这样。但是有一个问题,我们上文说过,函数可以作为返回值,你如何返回一大堆代码呢?这不可能吧。

如果你想返回一个函数中的一部分代码,一个做法就是把这个函数中要返回的这部分代码打包成一个函数,然后返回这个函数。

仍然以上面的函数为例,想返回inner。如下:

def outer():
    def inner():
        print("This is inner.")

    print("This is outer.")
    return inner

那么运行它会发生什么呢?如下:

直接运行outer。返回inner的类型和地址。因为outer有返回值,但我们没有指定返回值赋给谁,所以会出现这种情况。

将outer的返回值赋给i。然后输入i,情况和上面的类似。

输入i(),这时才会调用inner。说明只有加上括号了,才能够调用函数对象。只使用函数名就只是返回函数对象的类型和地址。

要想调用outer可以直接调用inner,只需要在return后面加上括号就行了。

这样就体现出嵌套函数的作用了。

第三个问题:如何处理任意数量和类型的函数参数?

假设我现在有一个函数,它可以接受任意多个参数,例如没有参数,一个参数,9527个参数。如何实现呢?总不可能是对每种情况分别写一个函数吧。

python解决这一问题的方法是传入一个参数元组,元组内保存参数。元组内的元素数量可以是任意个,因此函数可以接收任意个参数。这里使用*代表任意数量,如下:

def myfunc(*args):
    for a in args:
        print(a,end=' ')
    if args:
        print()

其运行效果如下:

注意,我们是直接输入参数的,而不需要自己先声明一个元组,初始化,然后再将元组作为参数输入。将多个参数变为元组这一步是由解释器完成的,我们在调用时只需要把它看作是能够接收任意个参数即可。而且类型不限哦。

那么问题来了,我提供一个列表行不行。比如说我在上文得到了一个列表,我想用这个函数处理列表中所有元素,难不成还得一个个拆开?当然不用,要想处理列表中的所有参数,只需要在调用时,列表参数前面加一个*即可,函数会自动展开这个列表。

如下:

可以很明显的看出来,前者是直接打印列表,也就是把列表当做一个元素打印;后者则是把列表展开后一个个打印元素。

现在我们更贪心了,可不可以直接指定函数内的若干个变量,直接让参数赋给变量呢?毕竟,如果一个函数的参数太多的话,记住参数顺序也有点烦,要是可以直接指定,那不就不用记住顺序了。

可以的,我们在写vsearch4web函数里就用过这种调用方式,这称为接收一个函数字典。要想让函数接收一个参数字典,需要在参数前面加两个*。如下:

def myfunc(*args):
    for a in args:
        print(a,end=' ')
    if args:
        print()

def myfunc2(**kwargs):
    for k,v in kwargs.items():
        print(k,v,sep='->',end=' ')
    if kwargs:
        print()

这里的myfunc2就可以接收参数字典。效果如下:

但是注意,这种接收参数的方式只能用于字典,而不能给函数里面的某一个特定参数指定赋值,如下:

def myfunc2(**kwargs):
    print(sec)
    for k,v in kwargs.items():
        print(k,v,sep='->',end=' ')
    if kwargs:
        print()

报错如下图:

注意这里报的错指的是print(sec)这一行的sec未定义,说明参数中的sec没有被正确赋值,而是被加入到了参数字典中。若想指定sec,需要在定义函数时把参数更改如下:

def myfunc2(sec,**kwargs):
    print(sec)
    for k,v in kwargs.items():
        print(k,v,sep='->',end=' ')
    if kwargs:
        print()

运行如下:

注意sec在定义函数中必须在**kwargs之前,否则会报错。在运行的时候就没有关系了。另外,在调用这个函数的时候也可以用**直接传入一个字典,之前传送SQL配置的时候就用过,在这里不再赘述。

现在总结一下,若干参数的传入其实就是在函数中先定义一个列表或者字典,然后用*args或者**kwargs来填这个字典。

最后,我们可以把这两者结合,传入一个列表,再传入一个字典,如下:

def myfunc3(*args,**kwargs):
    if args:
        for a in args:
            print(a,end=' ')
    if kwargs:
        for k,v in kwargs.items():
            print(k,v,sep='->',end=' ')

效果如下:

没指定的默认放在args中,指定的就放在kwargs中。

6、创建函数修饰符

前面铺垫了这么一大堆,有什么用呢?在这里就可以描述一下函数修饰符的功能了:

函数修饰符的参数是一个函数,它会自己定义一个函数,称作wrapper(包装),在自己定义的这个函数里调用参数函数,然后返回定义函数。也就是说,自己定义的这个函数wrapper,就是把参数函数包装了一下,然后返回。因此需要用到以上的三大功能:

以函数为参数,从而使得我们可以传入一个函数;

可以返回一个函数,从而使得我们能够得到新的函数;

可以传入任意参数,使得wrapper能够包装任何函数。

下面开始创建函数修饰符。它本质上还是一个函数,它必须维护被修饰函数的签名。什么叫被修饰函数的签名?它返回的函数要和被修饰函数有同样的参数,个数和类型都得相同,参数的个数和类型就叫签名。

代码如下:

from flask import session
from functools import wraps

def check_logged_in(func):
    @wraps(func)
    def wrapper(*args,**kwargs):
        if 'logged_in' in session:
            return func(*args,**kwargs)
        return "You are NOT logged in"
    return wrapper

首先是import,要从flask中引入session,这是要实现函数功能必须的;引入wraps是函数修饰符的要求,这个有点复杂,我们只需要知道要先用一个这个修饰符,然后定义wrap函数。

然后就可以定义我们自己的函数修饰符了,因为是函数,所以也用def定义,参数只有一个,就是被修饰的函数。

接下来调用修饰符,并定义wrap函数,为了使其具有通用性,要用*args和**kwargs来让其可以接收任意参数。接下来是重头戏,要给参数函数增加的功能就写在这里面。我们的功能就是判断字典中有没有登录记录,有的话,就返回参数函数,注意这里是有括号的,因此是调用;没有的话,就返回通知。

最后返回定义的函数即可。这里没有括号,因此不是调用。因为我们修饰一个函数,要得到的应该是一个新的函数对象,而不是直接就让被修饰函数调用。

用修饰符修饰函数使得对page1、2、3进行限制访问的代码如下:

from flask import Flask,session
from checker import check_logged_in

app=Flask(__name__)

app.secret_key='YouWillNeverGuess'

@app.route('/login')
def do_login()->str:
    session['logged_in']=True
    return 'You are now logged in'

@app.route('/logout')
def do_logout()->str:
    #session['logged_in'].clear()
    session.pop('logged_in')
    return 'You are now logged out.'

@app.route('/status')
def check_status()->str:
    #if session['logged_in']==True:
    if 'logged_in' in session:
        return 'You are currently logged in.'
    return 'You are NOT logged in.'
        

@app.route('/')
def hello()->str:
    return 'Hello from the simple webapp.'

@app.route('/page1')
@check_logged_in
def page1()->str:
    return 'This is page 1.'

@app.route('/page2')
@check_logged_in
def page2()->str:
    return 'This is page 2.'

@app.route('/page3')
@check_logged_in
def page3()->str:
    return 'This is page 3.'

if __name__=='__main__':
    app.run(debug=True)

先引入修饰符,再添加三行@即可,很方便。

7、更新我们的webapp代码

from flask import Flask, render_template,request,redirect,escape,session
from vsearch import search4letters
from DBcm import UseDatabase
from checker import check_logged_in

app=Flask(__name__)

app.secret_key='YouWillNeverGuess'

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('/search4',methods=['POST'])
def do_search() -> 'html':
    phrase=request.form['phrase']
    letters=request.form['letters']
    results=str(search4letters(phrase,letters))
    log_request(request,results)
    return render_template('results.html',
                           the_title='Here are your results',
                           the_phrase=phrase,
                           the_letters=letters,
                           the_results=results)


@app.route('/')
@app.route('/entry')
def entry_page() -> 'html':
    return render_template('entry.html',
                           the_title='Welcome to search4letters on the web!')


@app.route('/viewlog')
@check_logged_in
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,)  
						   

@app.route('/login')
def do_login()->str:
    session['logged_in']=True
    return 'You are now logged in'
	

@app.route('/logout')
def do_logout()->str:
    #session['logged_in'].clear()
    session.pop('logged_in')
    return 'You are now logged out.'
	

@app.route('/status')
def check_status()->str:
    #if session['logged_in']==True:
    if 'logged_in' in session:
        return 'You are currently logged in.'
    return 'You are NOT logged in.'
	
	
if __name__=='__main__':
    from werkzeug.contrib.fixers import ProxyFix
    app.wsgi_app=ProxyFix(app.wsgi_app)
    app.run()

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值