2024年3月

本文分享自华为云社区《
如何基于香橙派AIpro开发AI推理应用
》,作者:昇腾CANN。

01 简介

香橙派AIpro开发板采用昇腾AI技术路线,接口丰富且具有强大的可扩展性,提供8/20TOPS澎湃算力,可广泛使用于AI边缘计算、深度视觉学习及视频流AI分析、视频图像分析、自然语言处理等AI领域。通过昇腾CANN软件栈的AI编程接口,可满足大多数AI算法原型验证、推理应用开发的需求。

AscendCL(Ascend Computing Language,昇腾计算语言)是昇腾计算开放编程框架,是对底层昇腾计算服务接口的封装,提供Device管理、Context管理、Stream管理、内存管理、模型加载与执行、算子加载与执行、媒体数据处理等API,支持C&C++、Python编程语言,能够实现深度学习推理计算、图形图像预处理、单算子加速计算等能力。

掌握了AscendCL的编程方法,就意味着可以在香橙派AIpro开发板上充分利用昇腾的算力资源,能够基于深度学习算法开发图片分类、目标检测等一系列深度学习推理计算程序。

1.png

02 开发流程

使用AscendCL开发推理应用时,开发流程大致分为以下几步:

  1. AscendCL初始化:初始化AscendCL内部资源,为运行做准备
  2. 运行管理资源申请:申请运行时相关资源,例如计算设备
  3. 媒体数据处理:可实现抠图、缩放、视频或图片的编解码等
  4. 模型推理:包括模型加载、执行、卸载
  5. 运行管理资源释放:资源使用后及时释放
  6. AscendCL去初始化:与初始化配对使用

首先,我们得先了解下,使用AscendCL时,经常会提到的“
数据类型的操作接口
” ,
这是什么呢?为啥会存在?

在C/C++中,对用户开放的数据类型通常以Struct结构体方式定义、以声明变量的方式使用,但这种方式一旦结构体要增加成员参数,用户的代码就涉及兼容性问题,不便于维护,因此AscendCL对用户开放的数据类型,均以接口的方式操作该数据类型,例如,调用某个数据类型的Create接口创建该数据类型、调用Get接口获取数据类型内参数值、调用Set接口设置数据类型内的参数值、调用Destroy接口销毁该数据类型,用户无需关注定义数据类型的结构体长什么样,这样即使后续数据类型需扩展,只需增加该数据类型的操作接口即可,也不会引起兼容性问题。

所以,总结下,
“数据类型的操作接口”就是创建数据类型、Get/Set数据类型中的参数值、销毁数据类型的一系列接口
,存在的最大好处就是减少
兼容性问题

接下来,进入我们今天的主题,怎么用AscendCL的接口开发网络模型推理场景下的应用。看完本文介绍的关键知识点,也可以到 “昇腾文档中心[2]”查阅详细的文档介绍。

03 AscendCL初始化与去初始化

使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。在初始化时,还支持以下跟推理相关的配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入NULL,或者可将配置文件配置为空json串(即配置文件中只有{})。

有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用AscendCL接口实现AscendCL去初始化。

//此处以伪代码的形式展示接口的调用流程//初始化//此处的..表示相对路径,相对可执行文件所在的目录,例如,编译出来的可执行文件存放在out目录下,此处的..就表示out目录的上一级目录
const char *aclConfigPath = "../src/acl.json";
aclError ret
=aclInit(aclConfigPath);//......//去初始化 ret = aclFinalize();

04 运行管理资源申请与释放

运行管理资源包括Device、Context、Stream、Event等,此处重点介绍Device、Context、Stream,其基本概念如下图所示 。

3.PNG

您需要按顺序依次申请如下运行管理资源:
Device、Context、Stream
,确保可以使用这些资源执行运算、管理任务。所有数据处理都结束后,需要按顺序依次释放运行管理资源:
Stream、Context、Device

在申请运行管理资源时,Context、Stream支持隐式创建和显式创建两种申请方式。

4.PNG

//此处以伪代码的形式展示接口的调用流程,以显式创建Context和Stream为例//运行管理资源申请//1、指定运算的Device
aclError ret =aclrtSetDevice(deviceId);//2、显式创建一个Context,用于管理Stream对象
ret =aclrtCreateContext(context, deviceId);//3、显式创建一个Stream,用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序执行任务
ret =aclrtCreateStream(stream);//......//运行管理资源释放//1、销毁Stream
ret =aclrtDestroyStream(stream);//2、销毁Context
ret =aclrtDestroyContext(context);//3、释放Device资源
ret =aclrtResetDevice(deviceId);//......

05 媒体数据处理

如果模型对输入图片的宽高要求与用户提供的源图不一致,AscendCL提供了媒体数据处理的接口,可实现抠图、缩放、格式转换、视频或图片的编解码等,将源图裁剪成符合模型的要求。后续期刊中会展开说明这个功能,本期着重介绍模型推理的部分,以输入图片满足模型的要求为例。

06 模型加载

模型推理场景下,必须要有适配昇腾AI处理器的离线模型(*.om文件),我们可以使用ATC(Ascend Tensor Compiler)来构建模型。如果模型推理涉及动态Batch、动态分辨率等特性,需在构建模型增加相关配置。关于如何使用ATC来构建模型,请参见“昇腾文档中心[2]”。

有了模型,就可以开始加载了,当前AscendCL支持以下几种方式加载模型:

  • 从*.om文件中加载模型数据,由AscendCL管理内存
  • 从*.om文件中加载模型数据,由用户自行管理内存
  • 从内存中加载模型数据,由AscendCL管理内存
  • 从内存中加载模型数据,由用户自行管理内存

由用户自行管理内存时,需关注工作内存、权值内存。工作内存用于存放模型执行过程中的临时数据,权值内存用于存放权值数据。这个时候,是不是有疑问了,我怎么知道工作内存、权值内存需要多大?不用担心,AscendCL不仅提供了加载模型的接口,同时也提供了“根据模型文件获取模型执行时所需的工作内存和权值内存大小”的接口,方便用户使用 。

5.PNG

//此处以伪代码的形式展示接口的调用流程,以“由用户管理内存”为例//1.根据om模型文件获取模型执行时所需的权值内存大小、工作内存大小。
aclError ret = aclmdlQuerySize(omModelPath, &modelWorkSize,&modelWeightSize);//2.根据工作内存大小,申请Device上模型执行的工作内存。
ret = aclrtMalloc(&modelWorkPtr, modelWorkSize, 
ACL_MEM_MALLOC_HUGE_FIRST);
//3.根据权值内存的大小,申请Device上模型执行的权值内存。 ret = aclrtMalloc(&modelWeightPtr, modelWeightSize,
ACL_MEM_MALLOC_HUGE_FIRST);
//4.以从om模型文件加载模型、由用户管理工作内存和权值内存为例//模型加载成功,返回标识模型的ID。 ret = aclmdlLoadFromFileWithMem(modelPath, &modelId, modelWorkPtr,
modelWorkSize, modelWeightPtr,
modelWeightSize);

07 模型执行

在调用AscendCL接口进行模型推理时,模型推理有输入、输出数据,输入、输出数据需要按照AscendCL规定的数据类型存放。相关数据类型如下:

  • 使用
    aclmdlDesc类型
    的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。

模型加载成功后,用户可根据模型的ID,调用该数据类型下的操作接口获取该模型的描述信息,进而从模型的描述信息中获取模型输入/输出的个数、内存大小、维度信息、Format、数据类型等信息。

  • 使用
    aclDataBuffer类型
    的数据来描述每个输入/输出的内存地址、内存大小。

调用aclDataBuffer类型下的操作接口获取内存地址、内存大小等,便于向内存中存放输入数据、获取输出数据。

  • 使用
    aclmdlDataset类型
    的数据描述模型的输入/输出数据。

模型可能存在多个输入、多个输出,调用aclmdlDataset类型的操作接口添加多个aclDataBuffer类型的数据。

6.PNG

//此处以伪代码的形式展示如何准备模型的输入、输出数据结构//1.根据加载成功的模型的ID,获取该模型的描述信息
aclmdlDesc *modelDesc =aclmdlCreateDesc();
aclError ret
=aclmdlGetDesc(modelDesc, modelId);//2.准备模型推理的输入数据结构//(1)申请输入内存//当前示例代码中的模型只有一个输入,所以index为0,如果模型有多个输入,则需要先调用aclmdlGetNumInputs接口获取模型输入的数量 void *modelInputBuffer =nullptr;
size_t modelInputSize
= aclmdlGetInputSizeByIndex(modelDesc, 0);
ret
= aclrtMalloc(&modelInputBuffer, modelInputSize, ACL_MEM_MALLOC_NORMAL_ONLY);//(2)准备模型的输入数据结构//创建aclmdlDataset类型的数据,描述模型推理的输入 aclmdlDataset *input =aclmdlCreateDataset();
aclDataBuffer
*inputData =aclCreateDataBuffer(modelInputBuffer, modelInputSize);
ret
=aclmdlAddDatasetBuffer(input, inputData);//3.准备模型推理的输出数据结构//(1)创建aclmdlDataset类型的数据output,描述模型推理的输出 aclmdlDataset *output =aclmdlCreateDataset();//(2)获取模型的输出个数. size_t outputSize =aclmdlGetNumOutputs(modelDesc);//(3)循环为每个输出申请内存,并将每个输出添加到aclmdlDataset类型的数据中 for (size_t i = 0; i < outputSize; ++i) {
size_t buffer_size
=aclmdlGetOutputSizeByIndex(modelDesc, i);void *outputBuffer =nullptr;
ret
= aclrtMalloc(&outputBuffer, buffer_size,
ACL_MEM_MALLOC_NORMAL_ONLY);
aclDataBuffer
*outputData =aclCreateDataBuffer(outputBuffer, buffer_size);
ret
=aclmdlAddDatasetBuffer(output, outputData);
}

准备好模型执行所需的输入和输出数据类型、且存放好模型执行的输入数据后,可以执行模型推理了,如果模型的输入涉及动态Batch、动态分辨率等特性,则在模型执行前,还需要调用AscendCL接口告诉模型本次执行时需要用的Batch数、分辨率等。

当前AscendCL支持同步模型执行、异步模型执行两种方式,这里说的同步、异步是站在调用者和执行者的角度。

  • 若调用模型执行的接口后需等待推理完成再返回,则表示模型执行是同步的。当用户调用同步模型执行接口后,可直接从该接口的输出参数中获取模型执行的结果数据,如果需要推理的输入数据量很大,同步模型执行时,需要等所有数据都处理完成后,才能获取推理的结果数据。
  • 若调用模型执行的接口后不等待推理完成完成再返回,则表示模型执行是异步的。当用户调用异步模型执行接口时,需指定Stream(Stream用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序在Device上执行),另外,还需调用aclrtSynchronizeStream接口阻塞程序运行,直到指定Stream中的所有任务都完成,才可以获取推理的结果数据。如果需要推理的输入数据量很大,异步模型执行时,AscendCL提供了Callback机制,触发回调函数,在指定时间内一旦有推理的结果数据,就获取出来,达到分批获取推理结果数据的目的,提高效率。
//此处以伪代码的形式展示同步模型执行的过程//1. 由用户自行编码,将模型所需的输入数据读入内存//如果模型推理之前先进行媒体数据处理,则此处可以将媒体数据处理后的输出内容作为模型推理的输入内存,//......//2. 执行模型推理//modelId表示模型ID,在模型加载成功后,会返回标识模型的ID//input、output分别表示模型推理的输入、输出数据,在准备模型推理的输入、输出数据结构时已定义
aclError ret =aclmdlExecute(modelId, input, output)//3. 处理模型推理的输出数据
for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(output); ++i) {//获取每个输出的内存地址和内存大小
aclDataBuffer* dataBuffer =aclmdlGetDatasetBuffer(output, i);void* data =aclGetDataBufferAddr(dataBuffer);
size_t len
=aclGetDataBufferSizeV2(dataBuffer);//获取到输出数据后,由用户自行编码,处理输出数据//...... }//4.销毁模型输入、输出数据结构//释放输入资源,包括数据结构和内存 (void)aclDestroyDataBuffer(dataBuffer);
(
void)aclmdlDestroyDataset(mdlDataset);//5.释放内存资源,防止内存泄露//......

推理结束后,如果需要获取并进一步处理推理结果数据,则由用户自行编码实现。最后,别忘了,我们还要销毁aclmdlDataset、aclDataBuffer等数据类型,释放相关内存,防止内存泄露。

08 模型卸载

在模型推理结束后,还需要通过aclmdlUnload接口卸载模型,并销毁aclmdlDesc类型的模型描述信息、释放模型运行的工作内存和权值内存。

//此处以伪代码的形式展示模型卸载的过程//1. 卸载模型
aclError ret =aclmdlUnload(modelId);//2. 释放模型描述信息
(void)aclmdlDestroyDesc(modelDesc);//3. 释放模型运行的工作内存和权值内存
(void)aclrtFree(modelWorkPtr);
(
void)aclrtFree(modelWeightPtr);

09 更多介绍

[1]香橙派AIpro开源样例代码:
https://gitee.com/ascend/EdgeAndRobotics

[2]昇腾文档中心:
https://www.hiascend.com/zh/document

[3]香橙派AIpro学习资源一站式导航:
https://www.hiascend.com/forum/thread-0285140173361311056-1-1.html

今天看了一篇 IT 之家关于
AMD 处理器受 RowHammer 内存攻击影响
的报道,心血来潮了解了一下 RowHammer 攻击的原理,把了解到的知识记录下来。

RowHammer 攻击
是一种相对较新的攻击方式,它利用了现代动态随机存取存储器(DRAM)的物理缺陷,这种攻击方式不同于传统的软件漏洞利用,它直接针对硬件的弱点。这种攻击利用了 DRAM 在运行过程中产生的意外电荷泄漏效应,可能导致存储器单元泄露电荷并造成比特翻转。这个问题主要是由于现在 DRAM 存储单元高密度排列造成的。通过在一定模式下的高频率反复内存访问,攻击者可以在其原本无权访问的内存区域引发存储值的变化。这种攻击可以导致权限提升、数据泄漏和拒绝服务等安全问题。

要理解 RowHammer 攻击,首先需要了解 DRAM 的工作原理。DRAM 通过
存储电荷在电容中
来保存信息,每个电容与一个访问晶体管相连,共同构成一个
存储单元
(Cell,如下图所示)。电容充满电,存储单元就是 1,电容放完电,存储单元就是 0,晶体管用来控制电容充放电。

这些存储单元被组织成多行多列,形成一个二维阵列(Bank,如下图所示)。当读取或写入内存数据时,一个存储单元行会被激活,
整行的数据会被加载到行缓冲器
(row-buffer)中,同时存储单元中的电容放电,在行缓冲器内完成数据的读写操作。在操作完成后,行缓冲器内的数据会写入原来的存储单元行之中,同时存储单元中的电容充电。所以,不论是读还是写数据,存储单元都会充放电。

在内存如“白菜”价的今天,不改变内存电路板面积大小的前提下,为了能存储更多的数据,只能将存储单元排列的越来越近,密度大幅度增加。虽然芯片的制程有所提升,但每个电容之间的隔离不是完全的。当一个行被频繁激活时,
相邻行的电容可能会受到电磁干扰,导致存储的比特从 1 翻转为 0,或者相反
。这种比特翻转可以被恶意利用,攻击者可以通过精心设计的内存访问模式来控制比特翻转的位置和结果。
通过高频率地读取 DRAM 中的某一行,可以影响到相邻行中的数据,这种现象被称为 RowHammer,因为它就像用锤子反复敲击内存行一样
。如下图所示,第 1、3 行是攻击行,这两行中间是被攻击行,攻击行不停地充放电,中间行大概率会出现比特翻转。

即使理解了 RowHammer 攻击的原理,如果不知道哪些存储单元里隐藏着关键数据,毫无目的的去攻击也没有意义。更何况程序在访问内存数据时,并不会直接使用内存中的真实地址,而是使用
虚拟地址映射
的方式进行访问。这使得找到数据的真实地址变得更加困难,所以很长时间以来,对于 RowHammer 攻击的研究只停留在理论阶段,解决这个问题的关键在于如何实现
逆向 DRAM 地址映射
。由百度安全所研究设计的逆向工具 DRAMDig 能够在平均 7 ~ 8 分钟时间,快速、可靠地逆向出 DRAM 地址映射,解决了这一复杂的问题。感兴趣的朋友可以阅读这篇论文
《DRAMDig: A Knowledge-assisted Tool to Uncover DRAM Address Mapping》(DOI: 10.1109/DAC18072.2020.9218599)

尽管 RowHammer 攻击非常强大,但也不是无法防御的。从硬件层面,新的 DRAM 芯片中实现了各种缓解措施,如目标行刷新(Target Row Refresh, TRR),或者使用支持 ECC 的 DRAM 内存。软件层面的解决方案包括增加内存访问的随机性,以减少连续访问同一行的可能性。

Python中的
inspect
模块解析

Python的
inspect
模块是一个强大的内省工具,允许开发者检查(inspect)活动对象和源代码。它提供了一系列函数,用于获取信息关于正在运行的程序和调用堆栈,非常适合进行调试和动态分析。本文将通过介绍
inspect
模块的关键功能,并结合实际案例代码,来探索其在日常开发中的应用。

常用方法

1. 获取当前执行的函数或方法名、文件路径【并不是调用方】

在日志记录或调试时,知道当前执行的函数名是非常有用的

import inspect

def who_am_i():
    # 输出当前文件绝对路径
    print(inspect.currentframe().f_code.co_filename)
    return inspect.currentframe().f_code.co_name

print(who_am_i())  # 输出: who_am_i

image.png
个人认为比较有用的就是 co_filename、co_name

2. 获取调用者信息

获取当前函数或方法的调用者信息

import inspect

def caller_info():
    frame = inspect.currentframe().f_back
    print(f调用者 {frame.f_code.co_filename} 调用行号 d{frame.f_lineno}")

def test():
    caller_info()  # 调用以获取调用者信息

test()

这个例子显示了如何获取调用当前函数的代码位置,非常有助于调试复杂的调用链

3. 查看函数参数

inspect
模块可以用来检查函数或方法的参数,这对于动态分析和生成文档非常有用

import inspect

def sample_function(name, age=25):
    pass

sig = inspect.signature(sample_function)
print(sig)  # 输出: (name, age=25)

4. 获取源代码

inspect
还可以用来获取函数、类或模块的源代码

import inspect

def my_function():
    """A simple function."""
    pass

print(inspect.getsource(my_function))

5. 检查类和实例

inspect
模块提供了多种方式来检查类和实例,比如获取类的所有方法、属性等

class MyClass:
    def method_one(self):
        pass
    
    def method_two(self):
        pass

# 获取类的所有成员方法
methods = inspect.getmembers(MyClass, predicate=inspect.isfunction)
print(methods)  # 输出 MyClass 中定义的方法

实际案例:自动化场景下的应用

一个常见的使用场景是动态地调用函数或方法,并基于它们的签名自动生成文档。


def test():
    # 获取调用方
    frame = inspect.currentframe().f_back
    # 获取调用方文件绝对路径
    caller_file = inspect.getfile(frame)
    # 这种方式也可以
    caller_file = frame.f_code.co_filename

    ...
    
    params = [
        caller_file,
        "--env-data",
        env_data.json(),
        f"--count={count}",
        "-m",
        mark,
    ]

一个基于 Pytest 自动化测试项目

  1. 每个 py 模块都会调用这个方法来执行 Pytest 命令来跑测试用例
  2. 那怎么才能准确知道要跑哪个文件呢?
  3. 通过第一、二行代码即可

更便捷的方式

本文分享自华为云社区《
构建大型Web应用Flask中的Blueprints指南
》,作者: 柠檬味拥抱。

什么是Blueprints?

Blueprints是Flask中的一种模式,用于将应用程序分解为可重用的模块。每个蓝图实际上是一个包含一组路由、视图和静态文件的Python模块。通过使用蓝图,我们可以将相关功能的代码组织在一起,从而更容易地管理和维护我们的应用程序。
image.png

为什么要使用Blueprints?

  1. 模块化组织:将相关功能的代码放在一起,使得代码更易于理解和维护。
  2. 路由命名空间:通过在蓝图中定义路由,可以避免路由冲突,并更好地组织应用程序的URL结构。
  3. 可重用性:蓝图可以在多个应用程序中重复使用,从而促进了代码的可重用性和可扩展性。

如何使用Blueprints?

首先,让我们创建一个简单的Flask应用,并使用蓝图来组织路由和视图。

# app.pyfromflask import Flaskfromauth import auth_bpfromblog import blog_bp

app
=Flask(__name__)

# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)
if __name__ == "__main__":
app.run(debug
=True)

现在,让我们定义两个蓝图:一个用于身份验证,另一个用于博客功能。

# auth.pyfromflask import Blueprint

auth_bp
= Blueprint('auth', __name__)

@auth_bp.route(
'/login')
def login():
return 'Login Page'@auth_bp.route('/logout')
def logout():
return 'Logout Page'# blog.pyfromflask import Blueprint

blog_bp
= Blueprint('blog', __name__)

@blog_bp.route(
'/')
def index():
return 'Blog Home Page'@blog_bp.route('/post/<int:post_id>')
def post(post_id):
return f'Viewing post {post_id}'

在上面的代码中,我们定义了两个蓝图:
auth_bp
用于身份验证相关的路由,
blog_bp
用于博客相关的路由。

代码解析

  • 我们首先导入了
    Blueprint
    类以及
    Flask
    类。
  • 然后我们创建了Flask应用程序实例。
  • 接着,我们将定义好的蓝图注册到应用程序中,每个蓝图都有一个唯一的名称和一组路由。
  • 最后,我们运行应用程序。

在每个蓝图中,我们使用
@blueprint.route()
装饰器定义了不同的路由。在实际应用中,我们可以将相关功能的路由和视图添加到相应的蓝图中,以实现模块化的组织。

高级用法:蓝图之间的通信

除了简单的路由注册外,Blueprints还可以通过一些高级技巧实现更复杂的功能,例如蓝图之间的通信。让我们通过一个示例来说明这一点。

假设我们的博客应用需要在登录后显示用户的个人资料。我们可以在
auth
蓝图中处理登录逻辑,并在
blog
蓝图中显示用户的个人资料。为了实现这一点,我们可以在蓝图之间共享数据。

# auth.pyfromflask import Blueprint, session

auth_bp
= Blueprint('auth', __name__)

@auth_bp.route(
'/login')
def login():
# 模拟登录,将用户信息存储在session中
session[
'user'] = {'username': 'example_user'}return 'Login Successful'@auth_bp.route('/logout')
def logout():
# 模拟登出,清除session中的用户信息
session.pop(
'user', None)return 'Logout Successful'# blog.pyfromflask import Blueprint, session

blog_bp
= Blueprint('blog', __name__)

@blog_bp.route(
'/')
def index():
if 'user' insession:
username
= session['user']['username']return f'Welcome, {username}! This is your Blog Home Page' else:return 'Welcome to the Blog Home Page'@blog_bp.route('/profile')
def profile():
if 'user' insession:
username
= session['user']['username']return f'Hello, {username}! This is your Profile Page' else:return 'Please login to view your Profile'

在上面的示例中,我们使用了Flask的
session
对象来在蓝图之间共享用户信息。在
auth
蓝图中,用户成功登录后,我们将用户信息存储在
session
中;而在
blog
蓝图中,我们可以访问
session
中的用户信息来显示用户的个人资料。

高级用法解析

  • 我们使用了Flask的
    session
    对象来在不同请求之间存储用户信息。
    session
    是一个类似字典的对象,可以用来存储和访问用户的会话数据。

  • auth
    蓝图中,我们在用户登录成功后将用户信息存储在
    session
    中;而在
    blog
    蓝图中,我们通过访问
    session
    中的用户信息来显示用户的个人资料。
  • 这种方式使得不同的蓝图可以共享数据,实现了更灵活和可扩展的应用程序结构。

蓝图的模板和静态文件

除了路由和视图之外,Blueprints还可以用于组织模板和静态文件,使得应用程序的文件结构更加清晰。让我们通过一个例子来说明如何在蓝图中使用模板和静态文件。

首先,我们创建一个包含模板和静态文件的蓝图。

# blog.pyfromflask import Blueprint, render_template

blog_bp
= Blueprint('blog', __name__, template_folder='templates', static_folder='static')

@blog_bp.route(
'/')
def index():
return render_template('index.html')

@blog_bp.route(
'/about')
def about():
return render_template('about.html')

在上面的示例中,我们在创建
blog_bp
蓝图时指定了模板文件夹和静态文件夹的路径。这样,Flask就知道在哪里查找模板和静态文件。

接下来,我们在相应的模板文件夹中创建模板文件。

<!-- templates/index.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Blog Home</title>
    <link rel="stylesheet" href="{{ url_for('blog.static', filename='style.css') }}">
</head>
<body>
    <h1>Welcome to the Blog</h1>
    <p>This is the home page of our blog.</p>
</body>
</html>
<!-- templates/about.html -->
<!DOCTYPE html>
<html>
<head>
    <title>About</title>
    <link rel="stylesheet" href="{{ url_for('blog.static', filename='style.css') }}">
</head>
<body>
    <h1>About Us</h1>
    <p>Learn more about our blog and team.</p>
</body>
</html>

在模板文件中,我们使用
url_for()
函数来生成静态文件的URL,并指定了
blog.static
作为蓝图的静态文件路径。

最后,我们在静态文件夹中添加样式表文件。

/*static/style.css*/body {
font
-family: Arial, sans-serif;
background
-color: #f0f0f0;
margin:
0;
padding:
0;
}
h1 {
color: #
333;
}
p {
color: #
666;
}

解析

  • 我们使用了
    template_folder

    static_folder
    参数来指定蓝图的模板文件夹和静态文件夹的路径。
  • 在模板文件中,我们使用
    url_for()
    函数生成静态文件的URL,并指定了蓝图的静态文件路径。这样做可以确保在蓝图之间的移动时静态文件路径仍然有效。
  • 静态文件的引用方式与普通的Flask应用程序中相同,但需要明确指定蓝图的静态文件路径。

通过这种方式,我们可以将模板和静态文件与特定的蓝图相关联,使得文件结构更加清晰,并使应用程序更易于维护和扩展。

测试和文档

在构建大型Web应用程序时,测试和文档是不可或缺的组成部分。Blueprints可以与测试框架和文档生成工具集成,以便更好地管理和维护我们的应用程序。

测试

在使用Blueprints时,我们可以针对每个蓝图编写单元测试,以确保其功能正常。通常,测试蓝图的方法与测试普通的Flask应用程序相同,只需导入相应的蓝图并模拟请求即可。

# test_blog.py
import unittest
fromapp import appclassTestBlogBlueprint(unittest.TestCase):

def setUp(self):
self.app
=app.test_client()

def test_index(self):
response
= self.app.get('/blog/')
self.assertEqual(response.status_code,
200)
self.assertIn(b
'Welcome to the Blog', response.data)

def test_about(self):
response
= self.app.get('/blog/about')
self.assertEqual(response.status_code,
200)
self.assertIn(b
'About Us', response.data)if __name__ == '__main__':
unittest.main()

在上面的示例中,我们编写了针对
blog
蓝图的单元测试,以确保其
index

about
路由能够正常工作。

文档

在使用Blueprints时,我们还可以通过文档生成工具自动生成API文档,以便开发人员和团队成员更好地理解应用程序的结构和功能。

# 使用Flask-APIDoc生成API文档fromflask_apidoc import ApiDoc

apidoc
=ApiDoc()

# 将蓝图注册到apidoc
apidoc.register_blueprint(auth_bp)
apidoc.register_blueprint(blog_bp)
if __name__ == '__main__':
apidoc.run(debug
=True)

通过将蓝图注册到文档生成工具中,我们可以自动生成包含所有蓝图路由和视图的API文档。这样,开发人员就可以更轻松地查看和理解应用程序的结构和功能。

部署和扩展

一旦我们构建了具有模块化结构的大型Web应用程序,就需要考虑如何部署和扩展该应用程序,以确保其性能和可用性。让我们讨论一下在部署和扩展过程中如何处理Blueprints。

部署

在部署Flask应用程序时,可以使用各种Web服务器和部署工具,例如Gunicorn、uWSGI和Docker。部署过程中,只需确保将应用程序实例化的代码和蓝图注册的代码包含在主应用程序文件中即可。

# app.pyfromflask import Flaskfromauth import auth_bpfromblog import blog_bp

app
=Flask(__name__)

# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)
if __name__ == "__main__":
app.run(debug
=True)

将所有蓝图注册到主应用程序文件中可以确保在部署时所有路由和视图都能正确加载。

扩展

当我们的应用程序需要扩展时,例如增加新的功能模块或处理更多的用户请求,Blueprints可以帮助我们轻松地扩展应用程序。我们只需创建新的蓝图,并将其注册到主应用程序中即可。

# admin.pyfromflask import Blueprint

admin_bp
= Blueprint('admin', __name__)

@admin_bp.route(
'/dashboard')
def dashboard():
return 'Admin Dashboard'# app.pyfromflask import Flaskfromauth import auth_bpfromblog import blog_bpfromadmin import admin_bp

app
=Flask(__name__)

# 注册蓝图
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)
app.register_blueprint(admin_bp, url_prefix
='/admin')if __name__ == "__main__":
app.run(debug
=True)

在上面的示例中,我们创建了一个名为
admin_bp
的新蓝图,并将其注册到主应用程序中。通过使用
url_prefix
参数,我们可以指定蓝图的URL前缀,从而轻松地组织不同模块的路由。

性能优化

在构建大型Web应用程序时,性能是一个关键问题。Blueprints可以帮助我们实现更好的性能优化,通过合理的路由分发和模块化设计来提高应用程序的响应速度和可伸缩性。

蓝图的惰性加载

Flask中的Blueprints是惰性加载的,这意味着只有在应用程序第一次收到请求时才会注册和初始化蓝图。这种机制确保了应用程序在启动时加载的速度较快,因为只有在需要时才会加载相关的功能模块。

路由分发

通过合理地组织和分发路由,可以进一步提高应用程序的性能。例如,可以将具有相似功能的路由放在同一个蓝图中,以减少路由匹配的开销。

# blog.pyfromflask import Blueprint

blog_bp
= Blueprint('blog', __name__)

@blog_bp.route(
'/')
def index():
return 'Blog Home Page'@blog_bp.route('/post/<int:post_id>')
def post(post_id):
return f'Viewing post {post_id}'

在上面的示例中,所有与博客相关的路由都放在了一个名为
blog_bp
的蓝图中,这样可以提高路由匹配的效率。

静态文件和缓存

对于静态文件,可以使用Nginx、CDN或Flask的静态文件缓存等方式来加速静态文件的访问。另外,对于动态内容,可以使用缓存技术来减少数据库查询和计算的次数,从而提高响应速度。

安全性考虑

在构建大型Web应用程序时,安全性是至关重要的。Blueprints可以帮助我们实现一些安全性措施,以保护应用程序免受常见的安全威胁。

蓝图级别的中间件

Flask允许我们在蓝图级别应用中间件,这样我们就可以针对特定的蓝图应用安全性措施。

# auth.pyfromflask import Blueprint, request, abort

auth_bp
= Blueprint('auth', __name__)

@auth_bp.before_request
def check_request():
ifnot request.is_secure:
abort(
403)

在上面的示例中,我们在
auth
蓝图中应用了一个中间件,用于检查请求是否是安全的(即使用HTTPS)。如果请求不是安全的,就会返回403禁止访问的响应。

蓝图的权限控制

通过在蓝图中实现权限控制逻辑,我们可以限制用户对特定功能的访问。

# admin.pyfromflask import Blueprint, abort

admin_bp
= Blueprint('admin', __name__)

@admin_bp.route(
'/dashboard')
def dashboard():
ifnot current_user.is_admin:
abort(
403)return 'Admin Dashboard'

在上面的示例中,我们在
admin
蓝图中的
dashboard
路由中实现了权限控制逻辑,只有管理员用户才能访问该页面。

安全头部设置

Flask提供了一些内置的安全头部设置,可以在应用程序中设置以增强安全性,例如
X-Content-Type-Options

X-Frame-Options

Content-Security-Policy
等。

# app.pyfromflask import Flaskfromflask_talisman import Talisman

app
=Flask(__name__)
talisman
= Talisman(app)

在上面的示例中,我们使用Flask-Talisman扩展来设置一些安全头部,以保护应用程序免受XSS和点击劫持等攻击。

总结

总的来说,本文深入探讨了在Flask中使用Blueprints来构建大型Web应用程序的方法。Blueprints提供了一种模块化的方式来组织应用程序的路由、视图、模板和静态文件,使得应用程序更易于管理和维护。通过合理利用Blueprints,我们可以实现以下几个方面的优势:

  1. 模块化组织: 将相关功能的代码放在一起,使得代码更易于理解和维护。
  2. 路由命名空间: 避免路由冲突,并更好地组织应用程序的URL结构。
  3. 可重用性: 蓝图可以在多个应用程序中重复使用,促进了代码的可重用性和可扩展性。
  4. 高级功能支持: 可以实现蓝图之间的通信、模板和静态文件的组织、测试和文档的生成、部署和扩展以及性能优化和安全性考虑等功能。

通过本文所介绍的内容,开发人员可以更好地利用Blueprints来构建大型、模块化的Web应用程序,并在实践中不断优化和完善应用程序的结构和功能,以满足不断变化的需求和挑战。

点击关注,第一时间了解华为云新鲜技术~

继续上次的代码整理,此应用主要是一个VB6开发的颜色取色器,简单的界面,能够直接将屏幕上的颜色进行获取,该应用是笔者以前编写的代码,这次开源出来了。

1、
项目目录;

2、
源码介绍;

3、
运行界面;

4、
使用介绍 ;

打开应用,按Enter键启动,移动到要获取的颜色处,点击Esc键退出捕获。

5、
源码下载;

提供源码下载:
https://download.csdn.net/download/lzhdim/89011828

6、
其它建议;

该应用只是一个简单的例子。具体的其它扩展请读者自己去完善。

上面介绍了用VB6开发的颜色取色器,需要的读者请自己对代码进行复用。