Skip to content
0

Python项目部署

环境配置

在一个完整的 Python 项目生命周期中,通常涉及本地开发环境、测试环境以及生产环境。其中,本地开发环境往往因开发者个人习惯或系统差异而存在不同配置;而测试与生产环境则多为团队共享,配置要求相对统一且稳定。在这种情况下,如何实现项目在不同环境(服务器)之间部署时,无需手动修改配置文件呢?

方式一:dynaconf

dynaconf 支持多种配置文件格式,如 TOML、YAML、JSON、INI、Python、Dotenv 等,并且支持动态加载配置。

目录结构如下:

|——config
|       .secrets.toml
|       dev.toml
|       dev.local.toml
|       test.toml
|       settings.toml
|       online.toml
|       __init__.py
|  .gitignore
toml
[global]  # 全局配置,优先级高于默认配置
TABLE_PREFIX = "wd_"

[default] # 默认配置,优先级最低
TABLE_PREFIX = "df_"
LOG_LEVEL = "DEBUG"
db_url = "@format {this.db_config.host}:{this.db_config.port}"
toml
[dev]
TABLE_PREFIX = "dev_"  # 环境配置,优先级高于全局配置
LOG_LEVEL = "DEBUG"
DEBUG = true

# 第一种方式配置嵌套对象
[dev.db_config] 
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"
toml
[test]
TABLE_PREFIX="test_"
LOG_LEVEL = "INFO"
DEBUG = false

# 第二种方式配置嵌套对象
db_config.host = "172.168.1.105"
db_config.port = 3306
db_config.name = "toml"
db_config.user = "test"
db_config.pwd = "xxxxx"
python
from dynaconf import Dynaconf

Settings = Dynaconf(
    # 环境变量的前缀,比如你有一个配置项为 DEBUG,那么可以通过设置环境变量 WDADMIN_DEBUG = True 来设置 DEBUG 的值
    envvar_prefix="WDADMIN",
    # 配置文件的根路径
    root_path="",
    settings_files=['settings.toml', 'dev.toml', 'test.toml', '.secrets.toml'],
    # 使用分层的环境配置,为True时 default、global等分层的配置才生效
    environments=True,
    # 分层环境默认使用的配置,默认为 development
    env="dev",  # env 对应的环境变量 ENV_FOR_DYNACONF,如设置环境变量 ENV_FOR_DYNACONF=dev
    # 修改切换env的环境变量名,默认是 ENV_FOR_DYNACONF,现在改为 ENV_WDADMIN
    env_switcher="ENV_WDADMIN",  # env_switcher 对应的环境变量 ENVVAR_SWITCHER_FOR_DYNACONF

    # 加载.env文件
    load_dotenv=False,
)

使用方式如下:

python
from config import settings
import os

# 通过环境变量设置某个配置项
# os.environ["WDADMIN_DEBUG"] = True

# 使用不同的环境配置:开发、测试、生产环境
os.environ["ENV_WDADMIN"] = "test"

print(f"{settings.TABLE_PREFIX=}")
print(f"{settings.LOG_LEVEL=}")
print(f"{settings.db_config=}")
print(f"{settings.db_url=}")

## 输出结果:
## settings.TABLE_PREFIX='test_'
## settings.LOG_LEVEL='INFO'
## settings.db_config=<Box: {'host': '172.168.1.105', 'port': 3306, 'name': 'toml', 'user': 'test', 'pwd': 'xxxxx'}>
## settings.db_url='172.168.1.105:3306'
  1. 多种配置方式,dynaconf后加载的配置文件会覆盖前面的配置,所以文件的加载顺序和优先级顺序是相反的。
  2. 如果存在对应的*.local.toml文件,dynaconf会自动加载,不需要写额外的代码。
  3. 优先级: load_dotenv【.env文件】 > environments【环境变量】 > settings_files(local)【.local配置文件】 > settings_files【配置文件】
  4. 事实上,不同环境的配置可以全部写在同一个配置文件中:
toml
[global] # 全局配置,优先级低于当前环境配置
TABLE_PREFIX = "wd_"

[default]  # 默认配置,优先级低于全局配置
TABLE_PREFIX = "df_"
LOG_LEVEL = "DEBUG"

db_url = "@format {this.db_config.host}:{this.db_config.port}"

[dev]
TABLE_PREFIX = "dev_"
LOG_LEVEL = "DEBUG"
DEBUG = true

[dev.db_config] # 第一种方式配置嵌套对象
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"

[test]
TABLE_PREFIX="test_"
LOG_LEVEL = "DEBUG"
DEBUG = false

# 第二种方式配置嵌套对象
db_config.host = "172.168.1.105"
db_config.port = 3306
db_config.name = "toml"
db_config.user = "test"
db_config.pwd = "xxxxx"

方式二:python文件

目录结构如下:

|——config
│      dev.py
|      dev_local.py
│      online.py
│      test.py
│      __init__.py
|  .gitignore
python
DEBUG = True

DB_CONF = {
    'ENGINE': 'django.db.backends.sqlite3',
    'NAME': 'db.sqlite3',
}
python
DEBUG = False

DB_CONF = {
    "ENGINE": 'django.db.backends.mysql',
    'NAME': 'server',
    'USER': 'root',
    'PASSWORD': 'root123456',
    'HOST': '127.0.0.1',
    'PORT': '3306'
}
python
import os

ENV_NAME = "WDADMIN_ENV"
env = os.getenv(ENV_NAME)
if not env:
    raise Exception(f"get {ENV_NAME} failed, set it IN CAUTION!!!")
elif env == "online":
    from .online import *
elif env == "test":
    from .test import *
elif env == "dev":
    try:
        from .dev_local import *
    except ImportError:
        from .dev import *
else:
    raise Exception(f"{ENV_NAME} not in (online, test, dev)")


MEDIA_URL = "media/"

在需要配置项的地方直接导入即可,简单粗暴。这也是在这些配置库没出来前,我们团队一直使用的方式。

方式三:pydantic_settings

pydantic_settings 不仅可以对配置字段进行检查,并且可以使用.来提示访问属性,使用上更方便。其支持 .envtomlyamljson 等文件,并支持自定义扩展,比如从配置中心获取配置。

使用.env文件

目录结构大差不差:

|——settings
│      .env.dev
|      .env.dev.local
│      .env.test
│      .env.online
│      __init__.py
|  .gitignore
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=env
DB_USER=dev
DB_PWD=123456

APP_LOG_LEVEL=DEBUG
DB_HOST=172.168.1.105
DB_PORT=3306
DB_NAME=env
DB_USER=test
DB_PWD=xxxxx

APP_LOG_LEVEL=INFO
python
import os
from pathlib import Path
from functools import partial
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field, IPvAnyAddress, computed_field


def get_env_file():
    env = os.getenv("MY_ENV", "dev")
    file_map = {
        "dev": ".env.dev",
        "test": ".env.test",
        "online": ".env.online",
    }
    file = file_map.get(env)
    if file is None:
        raise Exception(f"Unknow {env}")
    cur_dir = Path(__file__).resolve().parent
    if env == "dev" and (cur_dir / ".env.dev.local").exists():
        return cur_dir / ".env.dev.local"
    return cur_dir / file


# .env文件中配置项名称规范是全部大写
PSettingsConfigDict = partial(SettingsConfigDict, env_file=get_env_file(), case_sensitive=False, extra="ignore")


class DBConfig(BaseSettings):
    host: IPvAnyAddress = "127.0.0.1"
    port: int = Field(default=3306, gt=1000, lt=65535)
    name: str = None
    user: str = None
    pwd: str = None

    @computed_field
    @property
    def url(self) -> str:
        return f"{self.host}:{self.port}@{self.user}:{self.pwd}"

    model_config = PSettingsConfigDict(env_prefix="DB_")


class MySettings(BaseSettings):
    APP_NAME: str = "MyAPP"
    LOG_LEVEL: str = "INFO"
    DB_CONFIG: DBConfig = DBConfig()

    model_config = PSettingsConfigDict(env_prefix="APP_", case_sensitive=True)
  

Settings = MySettings()

用起来爽,就是代码量有点多。为防止变量名冲突,需要使用前缀来定义不同的配置项。

使用方式如下:

python
from settings import Settings


print(Settings.LOG_LEVEL)
print(Settings.DB_CONFIG.url)
print(Settings.DB_CONFIG.model_dump())
## 输出:
## DEBUG
## 127.0.0.1:3307@devlocal:123456
## {'host': IPv4Address('127.0.0.1'), 'port': 3307, 'name': 'env', 'user': 'devlocal', 'pwd': '123456', 'url': '127.0.0.1:3307@devlocal:123456'}

使用toml文件

目录结构如下:

|——settings
│      dev.toml
|      dev.local.toml
│      test.toml
│      online.toml
│      __init__.py
|——.gitignore
toml
LOG_LEVEL = "DEBUG"

DEBUG = true

[DB_CONFIG]
host = "127.0.0.1"
port = 3306
name = "toml"
user = "dev"
pwd = "123456"
toml
LOG_LEVEL = "DEBUG"

DEBUG = false

[DB_CONFIG]
host = "172.168.1.105"
port = 3306
name = "toml"
user = "test"
pwd = "xxxxx"
python
import os
from pathlib import Path
from pydantic_settings import (
    BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource,
    TomlConfigSettingsSource,
)
from pydantic import Field, IPvAnyAddress, computed_field, BaseModel


def get_toml_file():
    env = os.getenv("MY_ENV", "dev")
    file_map = {
        "dev": "dev.toml",
        "test": "test.toml",
        "online": "online.toml",
    }
    file = file_map.get(env)
    if file is None:
        raise Exception(f"Unknow {env}")
    cur_dir = Path(__file__).resolve().parent
    if env == "dev" and (cur_dir / "dev.local.toml").exists():
        return cur_dir / "dev.local.toml"
    return cur_dir / file


class DBConfig(BaseModel):
    host: IPvAnyAddress = None
    port: int = Field(default=3306, gt=1000, lt=65535)
    name: str = None
    user: str = None
    pwd: str = None

    @computed_field
    @property
    def url(self) -> str:
        return f"{self.host}:{self.port}@{self.user}:{self.pwd}"


class MySettings(BaseSettings):
    APP_NAME: str = "MyAPP"
    LOG_LEVEL: str = ""
    DB_CONFIG: DBConfig = None
    DEBUG: bool = False

    model_config = SettingsConfigDict(
        toml_file=get_toml_file(),
        extra="ignore",
    )

    @classmethod
    def settings_customise_sources(
            cls,
            settings_cls: type[BaseSettings],
            init_settings: PydanticBaseSettingsSource,
            env_settings: PydanticBaseSettingsSource,
            dotenv_settings: PydanticBaseSettingsSource,
            file_secret_settings: PydanticBaseSettingsSource,
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (
            init_settings,
            env_settings,
            TomlConfigSettingsSource(settings_cls),
            dotenv_settings,
            file_secret_settings,
        )

Settings = MySettings()

可以看到,利用toml文件的嵌套特性 [DB_CONFIG] 配置结构化对象比较简单。

总结

本质上是在不同的服务器中设置某个环境变量的不同值来加载不同的配置文件。此外,还要注意.gitignore 忽略掉密钥文件和.local文件。

使用embeddable版本部署

Python的embeddable版本是一种轻量级的Python环境,它被设计为可以嵌入到其他应用程序中。这种版本的Python环境通常用于分发Python应用程序,因为它不包含文档、测试套件、工具(如IDLE)和Tkinter等。

此部署方式适用于文件较多的项目,比如Django服务。

  1. 下载对应 embeddable python 版本,下面以3.10版本为例,下载进入到解压后的文件夹内。
  2. 安装 pip 。访问网站,将里面的内容全部复制粘贴到一个py文件中,如 get-pip.py 。使用当前环境目录下的 python 解释器运行 get-pip.py 文件:如果系统安装了 python 并设置了环境变量,注意不要丢掉前面的.\,否则可能会安装到系统的 python 环境中。
powershell
.\python.exe get-pip.py

检查下是否安装成功:

powershell
.\python.exe -m pip -V

安装成功后可看到一个 Scripts 文件夹,里面就是 pip 的可执行文件。

  1. 编辑 python310._pth 文件(如果下载的是3.12版本,那就是 python312._pth 文件)

取消下面这行代码的注释

import site
  1. 使用 pip 安装自己工程的三方库。比如安装 requests:
powershell
.\python.exe -m pip install requests
# 或者进入到Scripts文件夹内
.\pip.exe install requests

会看到一个 Lib\site-packages 文件夹,里面就是三方库文件。

  1. 至此,这个文件夹就是项目的 python 运行环境了。你可以将此文件夹重命令为 venv 等,然后放到你的工程目录内,搞一个bat脚本,这样别人就能直接双击 bat 脚本启动你的工程了。

下面是一个运行 Django 的 bat 示例:( venv、 manage.py、 bat 脚本在同一目录下)

bat
@echo off
%~dp0venv/python.exe manage.py runserver 0.0.0.0:8000

%~dp0表示当前执行 bat 脚本的路径

注意

  1. 在使用 DevOps 工具等进行流水线构建时,注意不要忽略了运行环境内的 python310.zip,比如使用7z打包忽略掉了所有的zip文件。
  2. 一旦你移动或重命名这个环境目录,那么直接执行 Scripts 里面的 exe 就会失败,因为通常这些 exe 调用的 python 解释器采用的是固定路径,移动或重命名就找不到固定路径下 python.exe 文件了。可以使用模块的方式来运行:.\python.exe -m pip -V
  3. 如果需要保护源码,可以将工程内的所有 py 文件编译成 pyc 或 pyd 文件。

生成可执行文件

对于单个脚本文件的情况,生成可执行文件则更加简单。目前将py文件生成可执行文件的主流工具是nuitka和pyinstaller。

nuitka

原理是根据py文件生成C代码,再将C代码编译为exe文件,所以使用此工具需要安装C编译器。

shell
pip install nuitka  # 安装nuitka

python -m nuitka --onefile script.py

--standalone--onefile的区别:

  • standalone将程序及其所有依赖项打包成一个独立的文件夹,生成的文件夹中包含可执行文件和所有必要的依赖文件。
  • onefile将程序及其所有依赖项打包成一个单独的可执行文件。运行时,该文件会自动解压到临时目录中,然后执行,通常执行完之后会自动删除此临时目录。 测试文件func.py
python
import os

print(__file__)
print(os.getcwd())

下面是使用standalone生成的结果(func.dist文件夹下): onefile只是把standalone生成的文件夹打包成一个exe文件了,运行时会解压到临时目录运行。所以在使用onefile时,需要注意__file__是临时目录下的func.py路径。

注意

确保打包的文件路径不存在中文,虽然Nuitka会处理中文路径,但也可能会在编译时异常。

nuitak还能很方便的生成pyd文件:

python -m nuitka --module some_module.py

在windows平台下,将会生成类似some_module.cp310-win_amd64.pyd的pyd文件,其它py程序直接导入模块名some_module即可。注意:

  1. 编译生成的模块名不能修改。
  2. 扩展模块不能包含其它扩展模块。
  3. 生成的扩展模块只能用于相同版本的CPython。

pyinstaller

原理是将py文件及其依赖项打包到一个独立的可执行文件或文件夹中,并嵌入Python解释器,在运行时动态解压和加载资源,用解释器运行脚本文件。

shell
pip install pyinstaller  # 安装pyinstaller

pyinstaller -F script.py
常用参数描述
-F--onefile,类似nutika的onefile
-D--onedir,类似nutika的standalone
-w使用窗口,无控制台
-c使用控制台,无窗口(默认)

总结

  1. 打包项目最简单的方式是使用embeddable版本。
  2. 打包单个脚本文件使用nuitka或pyinstaller更方便。
  3. 打包后的exe执行时__file__会有所不同,应该使用os.path.getcwd()等方法。