美文网首页Head First Python
《Head First Python》Ch10:函数修饰符

《Head First Python》Ch10:函数修饰符

作者: 老A不加V | 来源:发表于2021-02-27 16:39 被阅读0次

    版权声明:本文为CSDN博主「一笑照夜」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/erwugumo/article/details/95965235

    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,使用firefox。 设置第三个user为chy,使用edge。

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


    image
    image
    image

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

    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的信息,那样就很不喜人。

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

    接下来开始测试:

    首先我们不登录:


    image

    返回正确。

    接下来我们登录:


    image

    看状态:


    image
    注销:
    image

    看状态:


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

    4、引入函数修饰符

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


    image

    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也是一个对象。虽然它们类型相同,但是通过名字可以看出,第一个参数应该是一个函数,而第二个参数应是第一个函数参数所需要的参数。这是约定俗成的起名方法。效果如下:


    image

    可以看到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
    

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


    image

    直接运行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()
    

    其运行效果如下:


    image

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

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

    如下:


    image

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

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

    可以的,我们在写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就可以接收参数字典。效果如下:


    image

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

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

    报错如下图:


    image

    注意这里报的错指的是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()
    

    运行如下:

    image
    注意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=' ')
    

    效果如下:


    image

    没指定的默认放在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()
    

    相关文章

      网友评论

        本文标题:《Head First Python》Ch10:函数修饰符

        本文链接:https://www.haomeiwen.com/subject/jnlifltx.html