Python学习笔记10:上下文协议

Python学习笔记10:上下文协议

我们从一门语言转到另一门新语言,最先注意到的无疑是这门语言有没有什么类似独门绝技一样的东西,而今天要说的就是这么一种Python独有的特性:上下文协议。

基本概念

之前我们介绍文件的时候有提到过使用with/as来实现自动打开与关闭文件,这样做可以避免开发者忘记关闭文件,无疑相当方便。

我们现在把眼光放高一点,从具体的开启、处理、关闭文件这个简单场景上升到这样一个模式:

  1. 在执行前进行一些准备活动。
  2. 执行一些行为。
  3. 在执行后进行一些收尾活动。

这个模式是不是具有一定的通用性?

比如数据库连接,在执行SQL前我们要进行数据库连接并创建游标,在执行后要提交SQL并断开连接。

如果我们能把这些行为抽象出来,进行一定的封装,就可以让开发者从频繁的准备活动或收尾活动中脱离开来,专注于业务代码,这无疑相当有用。

而这就是Python中上下文协议的用途。

在Python中,上下文协议的实现可以通过类来实现,只要定义了约定的方法就可以使用with语句进行上下文调用。

Python的上下文协议实现很像Java中的接口,这个接口定义了两个必须要实现的方法。

class ListReader():
    def __init__(self, aList: list):
        self.list = aList

    def __enter__(self) -> list:
        print("will print a list:")
        return self.list

    def __exit__(self, expType, expVal, expTrace):
        print("end")


aList = [1, 2, 3, 4, 5, 6]
with ListReader(aList) as lr:
    print(lr)

输出

will print a list:
[1, 2, 3, 4, 5, 6]
end

上边的例子展示了一个很简单的实现了上下文协议的类ListReader,如示例所示,要想实现上下文协议,需要在自定义类中实现__enter____exit__方法,他们分别用于准备阶段和清理阶段。

在这个例子中,在with语句执行的时候,解释器会先执行ListReader的构造函数初始化对象,然后再调用__enter__并返回一个list对象给lr,接着执行print(lr)输出这个列表,最后在with代码块执行完毕后,退出with语句的时候执行ListReader__exit__方法,进行扫尾工作。

可能有人会疑惑为什么__enter__一个参数都没有,而__exit__有三个参数。

这是因为执行上下文协议的场景通常是数据库操作或者文件操作,很容易产生异常,所以这三个参数都是异常产生时候的异常信息,可以在__enter__中根据出现的异常做不同处理。

改进web应用

在了解Python的上下文协议后,我们可以在之前的Web应用中应用上下文协议来改进数据库操作。

之前我们对数据库操作是封装了一个类,在执行SQL的时候直接调用以下方法:

    def executeSQL(self, _SQL: str, params: tuple) -> list:
        """执行SQL"""
        self.connect()
        self.cursor.execute(_SQL, params)
        results = self.cursor.fetchall()
        self.dbConnect.commit()
        self.close()
        return results

这存在一些问题,比如你说每次调用都要重复连接、提交、断开数据库操作,这存在一些资源浪费,比如在批量执行写入或读取的时候,这样效率很低。

现在我们使用上下文协议来新建一个数据库操作封装:

import mysql.connector
class MyDB2():
    def __init__(self):
        self.dbconfig = {"host": "127.0.0.1", "user": "root",
                         "password": "", "database": "myweb"}

    def __connect(self):
        self.dbConnect = mysql.connector.connect(**self.dbconfig)
        self.cursor = self.dbConnect.cursor()

    def __close(self):
        self.dbConnect.commit()
        self.cursor.close()
        self.dbConnect.close()

    def __enter__(self) -> 'cursor':
        self.__connect()
        return self.cursor

    def __exit__(self, expType, expVal, expTrace):
        self.__close()

然后使用上下文协议的方式调用:

def writeLog(logInfo: dict) -> None:
    with MyDB2() as cursor:
        _SQL = '''INSERT INTO LOG (phrase,letters,ip,browser_string,results)
                VALUES (%s,%s,%s,%s,%s)'''
        params = (logInfo['formData']['phrase'], logInfo['formData']
                  ['letters'], logInfo['userIp'], logInfo['userAgent'], logInfo['results'])
        cursor.execute(_SQL, params)


def getLog() -> list:
    lines = []
    results = []
    with MyDB2() as cursor:
        _SQL = '''SELECT * FROM LOG'''
        cursor.execute(_SQL)
        results = cursor.fetchall()
    for logInfo in results:
        lines.append(
            [logInfo[4], logInfo[3], logInfo[1], logInfo[2], logInfo[5]])
    return lines

可能这个例子中并不能显示这样改带来的好处,但如果遇到需要批量执行SQL的时候就能显出性能差异。当然我们要清醒地认识到这样改变后不是每次执行SQL立即生效,所以with语句块中的SQL不能相互冲突,比如后一步的查询需要依赖于前一步的变更或插入,如果那样你就不能放在同一个上下文中。

修改以后的完整web应用代码已上传到百度盘:

链接:https://pan.baidu.com/s/1vL3hUxnOEa89dqee_a8tZg
提取码:wd70
复制这段内容后打开百度网盘手机App,操作更方便哦--来自百度网盘超级会员V1的分享

用PHP实现上下文协议

准确的来说,上下文协议本身是一种设计模式的思想,不过python提供了语言级别的支持,让写法显得很简洁。我们同样可以尝试在其它语言中实现这一设计模式。

在这个例子中我使用PHP来实现一个上下文模式协议:

我们先建立一个上下文协议接口ContextInterface.php

<?php
interface ContextInterface{
    /**
     * 上下文准备环节
     * @return Object
     */
    public function enter();
    /**
     * 上下文清理环节
     */
    public function exit();
}

再建立一个数据库实现MyDB.php,并实现上下文接口,以起到自动切换上下文的功能。

<?php
require_once(".\ContextInterface.php");
class MyDB implements ContextInterface
{
    private $dbConnect;
    private $dbConfig;
    function __construct()
    {
        $this->dbConfig = array(
            'db' => 'myweb',
            'servername' => '127.0.0.1',
            'username' => 'root',
            'password' => ''
        );
    }
    public function enter()
    {
        $this->dbConnect = mysqli_connect($this->dbConfig['servername'],
                                             $this->dbConfig['username'], 
                                             $this->dbConfig['password'],
                                            $this->dbConfig['db']);
        // mysql_select_db($this->dbConfig['db'], $this->dbConnect);
        return $this->dbConnect;
    }
    public function exit()
    {
        mysqli_close($this->dbConnect);
    }
}

最后再实现一个抽象类ContextCallable.php,用于实现自动切换上下文的功能,具体的业务逻辑可以通过实现相应的抽象方法来实现。

<?php
require_once ".\ContextInterface.php";
abstract class ContextCallable
{
    /**
     * @param ContextInterface $contextInterface
     */
    private $contextInterface;
    function __construct(ContextInterface $contextInterface)
    {
        $this->contextInterface = $contextInterface;
    }
    public function run()
    {
        $callBack = $this->contextInterface->enter();
        $this->action($callBack);
        $this->contextInterface->exit();
    }
    /**
     * 实现上下文中的业务逻辑
     * @param object $callBack 上下文协议接口返回的句柄
     */
    abstract protected function action($callBack);
}

我们现在测试一下这个上下文协议:

<?php
require_once ".\ContextCallable.php";
require_once ".\MyDB.php";
$mydb = new MyDB();
$sqlExcuter = new class($mydb) extends ContextCallable
{
    protected function action($callBack)
    {
        $dbConn = $callBack;
        $SQL = "SELECT * FROM log";
        $result = mysqli_query($dbConn, $SQL);
        $logs = mysqli_fetch_all($result, MYSQLI_ASSOC);
        mysqli_free_result($result);
        print_r($logs);
    }
};
$sqlExcuter->run();

这其中通过创建一个继承自Contextcallable的匿名类填充业务逻辑,最后就可以实现上下文调用。

  • PHP的mysqli调用可以参考这里
  • PHP中的匿名类使用可以参考这里
本篇文章首发自魔芋红茶的博客https://www.cnblogs.com/Moon-Face/ 请尊重其他人的劳动成功,转载请注明。
原文地址:https://www.cnblogs.com/Moon-Face/p/14551000.html