展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > Python利用contextvars如何实现管理上下文变量?

Python利用contextvars如何实现管理上下文变量?

Python在3.7的时候引入了一个模块:contextvars,从名字上很容易看出它指的是上下文变量(ContextVariables),所以在介绍contextvars之前我们需要先了解一下什么是上下文(Context)。Context是一个包含了相关信息内容的对象,举个例子:"比如一部13集的动漫,你直接点进第八集,看到女主角在男主角面前流泪了"。相信此时你是不知道为什么女主角会流泪的,因为你没有看前面几集的内容,缺失了相关的上下文信息。所以Context并不是什么神奇的东西,它的作用就是携带一些指

本文目录

    Python利用contextvars如何实现管理上下文变量?

    Python 在 3.7 的时候引入了一个模块:contextvars,从名字上很容易看出它指的是上下文变量(Context Variables),所以在介绍 contextvars 之前我们需要先了解一下什么是上下文(Context)。

    Context 是一个包含了相关信息内容的对象,举个例子:"比如一部 13 集的动漫,你直接点进第八集,看到女主角在男主角面前流泪了"。相信此时你是不知道为什么女主角会流泪的,因为你没有看前面几集的内容,缺失了相关的上下文信息。

    所以 Context 并不是什么神奇的东西,它的作用就是携带一些指定的信息。

    web 框架中的 request

    我们以 fastapi 和 sanic 为例,看看当一个请求过来的时候,它们是如何解析的。

    # fastapi
    from fastapi import FastAPI, Request
    import uvicorn
    
    app = FastAPI()
    
    
    @app.get("/index")
    async def index(request: Request):
        name = request.query_params.get("name")
        return {"name": name}
    
    
    uvicorn.run("__main__:app", host="127.0.0.1", port=5555)
    
    # -------------------------------------------------------
    
    # sanic
    from sanic import Sanic
    from sanic.request import Request
    from sanic import response
    
    app = Sanic("sanic")
    
    
    @app.get("/index")
    async def index(request: Request):
        name = request.args.get("name")
        return response.json({"name": name})
    
    
    app.run(host="127.0.0.1", port=6666)

    发请求测试一下,看看结果是否正确。

    可以看到请求都是成功的,并且对于 fastapi 和 sanic 而言,其 request 和 视图函数是绑定在一起的。也就是在请求到来的时候,会被封装成一个 Request 对象、然后传递到视图函数中。

    但对于 flask 而言则不是这样子的,我们看一下 flask 是如何接收请求参数的。

    from flask import Flask, request
    
    app = Flask("flask")
    
    
    @app.route("/index")
    def index():
        name = request.args.get("name")
        return {"name": name}
    
    
    app.run(host="127.0.0.1", port=7777)

    我们看到对于 flask 而言则是通过 import request 的方式,如果不需要的话就不用 import,当然我这里并不是在比较哪种方式好,主要是为了引出我们今天的主题。首先对于 flask 而言,如果我再定义一个视图函数的话,那么获取请求参数依旧是相同的方式,但是这样问题就来了,不同的视图函数内部使用同一个 request,难道不会发生冲突吗?

    显然根据我们使用 flask 的经验来说,答案是不会的,至于原因就是 ThreadLocal。

    ThreadLocal

    ThreadLocal,从名字上看可以得出它肯定是和线程相关的。没错,它专门用来创建局部变量,并且创建的局部变量是和线程绑定的。

    import threading
    
    # 创建一个 local 对象
    local = threading.local()
    
    def get():
        name = threading.current_thread().name
        # 获取绑定在 local 上的 value
        value = local.value
        print(f"线程: {name}, value: {value}")
    
    def set_():
        name = threading.current_thread().name
        # 为不同的线程设置不同的值
        if name == "one":
            local.value = "ONE"
        elif name == "two":
            local.value = "TWO"
        # 执行 get 函数
        get()
    
    t1 = threading.Thread(target=set_, name="one")
    t2 = threading.Thread(target=set_, name="two")
    t1.start()
    t2.start()
    """
    线程 one, value: ONE
    线程 two, value: TWO
    """

    可以看到两个线程之间是互不影响的,因为每个线程都有自己唯一的 id,在绑定值的时候会绑定在当前的线程中,获取也会从当前的线程中获取。可以把 ThreadLocal 想象成一个字典:

    {
        "one": {"value": "ONE"},
        "two": {"value": "TWO"}
    }

    更准确的说 key 应该是线程的 id,为了直观我们就用线程的 name 代替了,但总之在获取的时候只会获取绑定在该线程上的变量的值。

    而 flask 内部也是这么设计的,只不过它没有直接用 threading.local,而是自己实现了一个 Local 类,除了支持线程之外还支持 greenlet 的协程,那么它是怎么实现的呢?首先我们知道 flask 内部存在 "请求 context" 和 "应用 context",它们都是通过栈来维护的(两个不同的栈)。

    # flask/globals.py
    _request_ctx_stack = LocalStack()
    _app_ctx_stack = LocalStack()
    current_app = LocalProxy(_find_app)
    request = LocalProxy(partial(_lookup_req_object, "request"))
    session = LocalProxy(partial(_lookup_req_object, "session"))

    每个请求都会绑定在当前的 Context 中,等到请求结束之后再销毁,这个过程由框架完成,开发者只需要直接使用 request 即可。所以请求的具体细节流程可以点进源码中查看,这里我们重点关注一个对象:werkzeug.local.Local,也就是上面说的 Local 类,它是变量的设置和获取的关键。直接看部分源码:

    # werkzeug/local.py
    
    class Local(object):
        __slots__ = ("__storage__", "__ident_func__")
    
        def __init__(self):
            # 内部有两个成员:__storage__ 是一个字典,值就存在这里面
            # __ident_func__ 只需要知道它是用来获取线程 id 的即可
            object.__setattr__(self, "__storage__", {})
            object.__setattr__(self, "__ident_func__", get_ident)
    
        def __call__(self, proxy):
            """Create a proxy for a name."""
            return LocalProxy(self, proxy)
    
        def __release_local__(self):
            self.__storage__.pop(self.__ident_func__(), None)
    
        def __getattr__(self, name):
            try:
                # 根据线程 id 得到 value(一个字典)
                # 然后再根据 name 获取对应的值
                # 所以只会获取绑定在当前线程上的值
                return self.__storage__[self.__ident_func__()][name]
            except KeyError:
                raise AttributeError(name)
    
        def __setattr__(self, name, value):
            ident = self.__ident_func__()
            storage = self.__storage__
            try:
                # 将线程 id 作为 key,然后将值设置在对应的字典中
                # 所以只会将值设置在当前的线程中
                storage[ident][name] = value
            except KeyError:
                storage[ident] = {name: value}
    
        def __delattr__(self, name):
            # 删除逻辑也很简单
            try:
                del self.__storage__[self.__ident_func__()][name]
            except KeyError:
                raise AttributeError(name)

    所以我们看到 flask 内部的逻辑其实很简单,通过 ThreadLocal 实现了线程之间的隔离。每个请求都会绑定在各自的 Context 中,获取值的时候也会从各自的 Context 中获取,因为它就是用来保存相关信息的(重要的是同时也实现了隔离)。

    相应此刻你已经理解了上下文,但是问题来了,不管是 threading.local 也好、还是类似于 flask 自己实现的 Local 也罢,它们都是针对线程的。如果是使用 async def 定义的协程该怎么办呢?如何实现每个协程的上下文隔离呢?所以终于引出了我们的主角:contextvars。

    contextvars

    该模块提供了一组接口,可用于在协程中管理、设置、访问局部 Context 的状态。

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试")
    
    async def get():
        # 获取值
        return c.get() + "~~~"
    
    async def set_(val):
        # 设置值
        c.set(val)
        print(await get())
    
    async def main():
        coro1 = set_("协程1")
        coro2 = set_("协程2")
        await asyncio.gather(coro1, coro2)
    
    
    asyncio.run(main())
    """
    协程1~~~
    协程2~~~
    """

    ContextVar 提供了两个方法,分别是 get 和 set,用于获取值和设置值。我们看到效果和 ThreadingLocal 类似,数据在协程之间是隔离的,不会受到彼此的影响。

    但我们再仔细观察一下,我们是在 set_ 函数中设置的值,然后在 get 函数中获取值。可 await get() 相当于是开启了一个新的协程,那么意味着设置值和获取值不是在同一个协程当中。但即便如此,我们依旧可以获取到希望的结果。因为 Python 的协程是无栈协程,通过 await 可以实现级联调用。

    我们不妨再套一层:

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试")
    
    async def get1():
        return await get2()
    
    async def get2():
        return c.get() + "~~~"
    
    async def set_(val):
        # 设置值
        c.set(val)
        print(await get1())
        print(await get2())
    
    async def main():
        coro1 = set_("协程1")
        coro2 = set_("协程2")
        await asyncio.gather(coro1, coro2)
    
    
    asyncio.run(main())
    """
    协程1~~~
    协程1~~~
    协程2~~~
    协程2~~~
    """

    我们看到不管是 await get1() 还是 await get2(),得到的都是 set_ 中设置的结果,说明它是可以嵌套的。

    并且在这个过程当中,可以重新设置值。

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试")
    
    async def get1():
        c.set("重新设置")
        return await get2()
    
    async def get2():
        return c.get() + "~~~"
    
    async def set_(val):
        # 设置值
        c.set(val)
        print("------------")
        print(await get2())
        print(await get1())
        print(await get2())
        print("------------")
    
    async def main():
        coro1 = set_("协程1")
        coro2 = set_("协程2")
        await asyncio.gather(coro1, coro2)
    
    
    asyncio.run(main())
    """
    ------------
    协程1~~~
    重新设置~~~
    重新设置~~~
    ------------
    ------------
    协程2~~~
    重新设置~~~
    重新设置~~~
    ------------
    """

    先 await get2() 得到的就是 set_ 函数中设置的值,这是符合预期的。但是我们在 get1 中将值重新设置了,那么之后不管是 await get1() 还是直接 await get2(),得到的都是新设置的值。

    这也说明了,一个协程内部 await 另一个协程,另一个协程内部 await 另另一个协程,不管套娃(await)多少次,它们获取的值都是一样的。并且在任意一个协程内部都可以重新设置值,然后获取会得到最后一次设置的值。再举个栗子:

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试")
    
    async def get1():
        return await get2()
    
    async def get2():
        val = c.get() + "~~~"
        c.set("重新设置啦")
        return val
    
    async def set_(val):
        # 设置值
        c.set(val)
        print(await get1())
        print(c.get())
    
    async def main():
        coro = set_("古明地觉")
        await coro
    
    asyncio.run(main())
    """
    古明地觉~~~
    重新设置啦
    """

    await get1() 的时候会执行 await get2(),然后在里面拿到 c.set 设置的值,打印 "古明地觉~~~"。但是在 get2 里面,又将值重新设置了,所以第二个 print 打印的就是新设置的值。\

    如果在 get 之前没有先 set,那么会抛出一个 LookupError,所以 ContextVar 支持默认值:

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试",
                               default="哼哼")
    
    async def set_(val):
        print(c.get())
        c.set(val)
        print(c.get())
    
    async def main():
        coro = set_("古明地觉")
        await coro
    
    asyncio.run(main())
    """
    哼哼
    古明地觉
    """

    除了在 ContextVar 中指定默认值之外,也可以在 get 中指定:

    import asyncio
    import contextvars
    
    c = contextvars.ContextVar("只是一个标识, 用于调试",
                               default="哼哼")
    
    async def set_(val):
        print(c.get("古明地恋"))
        c.set(val)
        print(c.get())
    
    async def main():
        coro = set_("古明地觉")
        await coro
    
    asyncio.run(main())
    """
    古明地恋
    古明地觉
    """

    所以结论如下,如果在 c.set 之前使用 c.get:

    • 当 ContextVar 和 get 中都没有指定默认值,会抛出 LookupError;

    • 只要有一方设置了,那么会得到默认值;

    • 如果都设置了,那么以 get 为准;

    如果 c.get 之前执行了 c.set,那么无论 ContextVar 和 get 有没有指定默认值,获取到的都是 c.set 设置的值。

    所以总的来说还是比较好理解的,并且 ContextVar 除了可以作用在协程上面,它也可以用在线程上面。没错,它可以替代 threading.local,我们来试一下:

    import threading
    import contextvars
    
    c = contextvars.ContextVar("context_var")
    
    def get():
        name = threading.current_thread().name
        value = c.get()
        print(f"线程 {name}, value: {value}")
    
    def set_():
        name = threading.current_thread().name
        if name == "one":
            c.set("ONE")
        elif name == "two":
            c.set("TWO")
        get()
    
    t1 = threading.Thread(target=set_, name="one")
    t2 = threading.Thread(target=set_, name="two")
    t1.start()
    t2.start()
    """
    线程 one, value: ONE
    线程 two, value: TWO
    """

    和 threading.local 的表现是一样的,但是更建议使用 ContextVars。不过前者可以绑定任意多个值,而后者只能绑定一个值(可以通过传递字典的方式解决这一点)。

    c.Token

    当我们调用 c.set 的时候,其实会返回一个 Token 对象:

    import contextvars
    
    c = contextvars.ContextVar("context_var")
    token = c.set("val")
    print(token)
    """
     at 0x00...>
    """

    Token 对象有一个 var 属性,它是只读的,会返回指向此 token 的 ContextVar 对象。

    import contextvars
    
    c = contextvars.ContextVar("context_var")
    token = c.set("val")
    
    print(token.var is c)  # True
    print(token.var.get())  # val
    
    print(
        token.var.set("val2").var.set("val3").var is c
    )  # True
    print(c.get())  # val3

    Token 对象还有一个 old_value 属性,它会返回上一次 set 设置的值,如果是第一次 set,那么会返回一个 。

    import contextvars
    
    c = contextvars.ContextVar("context_var")
    token = c.set("val")
    
    # 该 token 是第一次 c.set 所返回的
    # 在此之前没有 set,所以 old_value 是 
    print(token.old_value)  # 
    
    token = c.set("val2")
    print(c.get())  # val2
    # 返回上一次 set 的值
    print(token.old_value)  # val

    那么这个 Token 对象有什么作用呢?从目前来看貌似没太大用处啊,其实它最大的用处就是和 reset 搭配使用,可以对状态进行重置。

    import contextvars
    #### 
    c = contextvars.ContextVar("context_var")
    token = c.set("val")
    # 显然是可以获取的
    print(c.get())  # val
    
    # 将其重置为 token 之前的状态
    # 但这个 token 是第一次 set 返回的
    # 那么之前就相当于没有 set 了
    c.reset(token)
    try:
        c.get()  # 此时就会报错
    except LookupError:
        print("报错啦")  # 报错啦
    
    # 但是我们可以指定默认值
    print(c.get("默认值"))  # 默认值

    contextvars.Context

    它负责保存 ContextVars 对象和设置的值之间的映射,但是我们不会直接通过 contextvars.Context 来创建,而是通过 contentvars.copy_context 函数来创建。

    import contextvars
    
    c1 = contextvars.ContextVar("context_var1")
    c1.set("val1")
    c2 = contextvars.ContextVar("context_var2")
    c2.set("val2")
    
    # 此时得到的是所有 ContextVar 对象和设置的值之间的映射
    # 它实现了 collections.abc.Mapping 接口
    # 因此我们可以像操作字典一样操作它
    context = contextvars.copy_context()
    # key 就是对应的 ContextVar 对象,value 就是设置的值
    print(context[c1])  # val1
    print(context[c2])  # val2
    for ctx, value in context.items():
        print(ctx.get(), ctx.name, value)
        """
        val1 context_var1 val1
        val2 context_var2 val2
        """
    
    print(len(context))  # 2

    除此之外,context 还有一个 run 方法:

    import contextvars
    
    c1 = contextvars.ContextVar("context_var1")
    c1.set("val1")
    c2 = contextvars.ContextVar("context_var2")
    c2.set("val2")
    
    context = contextvars.copy_context()
    
    def change(val1, val2):
        c1.set(val1)
        c2.set(val2)
        print(c1.get(), context[c1])
        print(c2.get(), context[c2])
    
    # 在 change 函数内部,重新设置值
    # 然后里面打印的也是新设置的值
    context.run(change, "VAL1", "VAL2")
    """
    VAL1 VAL1
    VAL2 VAL2
    """
    
    print(c1.get(), context[c1])
    print(c2.get(), context[c2])
    """
    val1 VAL1
    val2 VAL2
    """

    我们看到 run 方法接收一个 callable,如果在里面修改了 ContextVar 实例设置的值,那么对于 ContextVar 而言只会在函数内部生效,一旦出了函数,那么还是原来的值。但是对于 Context 而言,它是会受到影响的,即便出了函数,也是新设置的值,因为它直接把内部的字典给修改了。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发 Python
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    IntelliJ IDEA macOS版
    IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    PyCharm macOS版
    PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    WebStorm macOS版
    WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    After Effects macOS版
    After Effects macOS版

    Adobe After Effects 是面向动态图形、影视特效与视频合成的专业创作软件。它提供关键帧动画、遮罩抠像、运动跟踪、三维空间、文字动画和表达式等工具,并可与 Premiere Pro、Photoshop、Illustrator

    Audition macOS版
    Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Bridge macOS版
    Bridge macOS版

    Adobe Bridge 是面向摄影师、设计师及内容团队的免费数字资产管理工具,可集中预览照片、视频和 Adobe 项目文件,并利用评级、标签、关键词、元数据、筛选与收藏集完成分类检索。它还支持批量重命名、格式导出、相机导入及 Camera

    Illustrator macOS版
    Illustrator macOS版

    Adobe Illustrator 是 Adobe 推出的专业矢量图形设计软件,适合在 Mac 上制作标志、图标、插画、包装、信息图及印刷版面。它以路径和锚点构建可无损缩放的图形,并提供钢笔、形状、文字、图像描摹、渐变、画板及生成式功能。

    InDesign macOS版
    InDesign macOS版

    Adobe InDesign 是面向印刷品与数字出版物的专业版面设计软件,可在 Mac 上制作书籍、杂志、宣传册、海报、报告及交互式文档。它提供精细的文字样式、网格、母版、长文档管理和印前输出能力。

    Lightroom Classic macOS版
    Lightroom Classic macOS版

    Adobe Lightroom Classic 是面向桌面摄影工作流的照片管理与后期处理软件,可将原始照片保存在本地硬盘,通过目录、关键词、评分和收藏夹高效整理图库,并提供 RAW 冲印、蒙版、镜头校正、批量同步及多格式导出等功能。

    Media Encoder macOS版
    Media Encoder macOS版

    Adobe Media Encoder 是 Adobe 推出的专业音视频编码与转码工具,可通过队列、预设和监视文件夹批量完成格式转换、代理文件创建及多平台成片输出,并与 Premiere Pro、After Effects 等软件紧密协作。

    Photoshop macOS版
    Photoshop macOS版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理软件,提供图层、蒙版、选区、修复、调色、文字排版、智能对象及生成式编辑等能力。Mac 版支持 Intel 与 Apple Silicon 处理器。

    Premiere Pro macOS版
    Premiere Pro macOS版

    Adobe Premiere Pro 是面向专业视频制作的非线性剪辑软件,提供多轨时间线、调色、音频处理、字幕、代理工作流和多格式输出能力,并可与 After Effects、Audition、Photoshop 及 Frame.io 协同

    WINDOWS 更多
    IntelliJ IDEA Windows版
    IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    PhpStorm Windows版
    PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    PyCharm Windows版
    PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    WebStorm Windows版
    WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    Photoshop Windows版
    Photoshop Windows版

    Adobe Photoshop 是面向摄影师、设计师和内容创作者的专业图像处理工具,可在 Windows 上完成照片修饰、选区抠图、图层合成、文字排版、绘画及生成式编辑。软件采用订阅授权,由 Creative Cloud 管理安装、更新与许

    InDesign Windows版
    InDesign Windows版

    Adobe InDesign 是面向印刷与数字出版的专业页面排版工具,可制作书籍、杂志、宣传册、海报、交互式 PDF 和电子书。它提供精细的文字样式、多页面管理、印前检查及多格式输出能力,并能与 Photoshop、Illustrator

    Illustrator Windows版
    Illustrator Windows版

    Adobe Illustrator 是用于绘制矢量图形、插画、图标、徽标和版式的专业设计工具。它以路径和锚点为核心,作品放大后仍能保持清晰,并提供画板、文字、渐变、图像描摹及精确变换等功能,适合品牌视觉、印刷物料、数字界面和商业插画创作。

    Bridge Windows版
    Bridge Windows版

    Adobe Bridge 是 Adobe 推出的创意资产管理工具,可集中预览、分类和查找照片、视频、PDF及多种设计文件,并支持元数据、关键词、评级、批量重命名和格式导出。它可与 Photoshop、Illustrator 等应用衔接,

    Lightroom Classic Windows版
    Lightroom Classic Windows版

    Adobe Lightroom Classic 是面向 Windows 桌面摄影工作流的照片管理与无损编辑工具,可通过目录集中整理大型图库,并完成 RAW 显影、局部蒙版、批量调整、打印和多格式导出。它强调本地文件管理与精细控制,

    Acrobat Pro Windows版
    Acrobat Pro Windows版

    Adobe Acrobat Pro 是面向专业 PDF 工作的综合解决方案,可在 Windows 中创建、编辑、转换、比较、整理和保护文档,并支持表单、电子签名、扫描件文字识别及敏感信息永久遮盖。订阅还包含网页、移动服务和云端协作能力,

    After Effects Windows版
    After Effects Windows版

    Adobe After Effects 是面向动态图形、动画与影视视觉特效制作的专业工具,可完成关键帧动画、图层合成、遮罩跟踪、抠像、文字动画和二维三维元素整合。Windows 版通过 Adobe Creative Cloud 安装及更新,

    Media Encoder Windows版
    Media Encoder Windows版

    Adobe Media Encoder 是 Adobe 面向视频与音频制作流程提供的专业编码和转码工具,可批量处理媒体文件、创建代理、套用输出预设并监控文件夹自动执行任务。它与 Premiere、After Effects、Audition