Redrock Postgres 文档
主页 切换暗/亮/自动模式 切换暗/亮/自动模式 切换暗/亮/自动模式 返回首页

适配基本的 Python 类型

许多标准的 Python 类型被适配到了 SQL,并在执行查询时作为 Python 对象返回。

在 Python 和 PostgreSQL 之间转换以下数据类型是自适应的,不需要任何配置。如果您需要配置类型转换,请查看数据适配配置

布尔型适配

Python 布尔型TrueFalse会被转换为等效的 PostgreSQL 布尔类型

>>> cur.execute("SELECT %s, %s", (True, False))
# equivalent to "SELECT true, false"

数字型适配

  • Python 中的 int 值可以根据其数值转换为 PostgreSQL 中的smallintintegerbigintnumeric。Psycopg 会选择可用的最小的数值类型,因为 PostgreSQL 可以自动地从小数值类型转换到大数值类型(例如,在 PostgreSQL 期望integer的地方传递一个smallint也会被接受),但不会自动从大数值类型转换到小数值类型(例如,如果一个函数有一个integer参数,传递一个bigint值会失败,即使数值是 1)。
  • Python 中的 float 值会被转换为 PostgreSQL 中的float8
  • Python 中的 Decimal 值会被转换为 PostgreSQL 中的numeric

当需要从 PostgreSQL 中的数值转换到 Python 中的数值类型时,较小的数值类型(int2int4float4)会被提升为 Python 中对应的较大的数值类型。

出于提升性能或易于操作的考虑,有时您可能更愿意将numeric类型的数值以float类型进行接收:您可以配置适配器以将 PostgreSQL 数字转换为 Python 浮点数。这当然可能会带来精度的损失。

字符串适配

Python 中的 str 会被转换为 PostgreSQL 字符串语法,而 PostgreSQL 中的字符串类型(如textvarchar)会被转换回 Python 中的str

conn = psycopg.connect()
conn.execute(
    "INSERT INTO menu (id, entry) VALUES (%s, %s)",
    (1, "Crème Brûlée at 4.99€"))
conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0]
'Crème Brûlée at 4.99€'

PostgreSQL 的数据库对象都有一个数据库编码,会话也有一个会话编码,在 Connection.info.encoding 属性中公开。如果您的数据库和连接采用 UTF-8 编码,则可能没有问题,否则您必须确保您的应用程序仅处理数据库可以处理的非 ASCII 字符;否则可能会导致编码/解码错误:

# The encoding is set at connection time according to the db configuration
conn.info.encoding
'utf-8'

# The Latin-9 encoding can manage some European accented letters
# and the Euro symbol
conn.execute("SET client_encoding TO LATIN9")
conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0]
'Crème Brûlée at 4.99€'

# The Latin-1 encoding doesn't have a representation for the Euro symbol
conn.execute("SET client_encoding TO LATIN1")
conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0]
# Traceback (most recent call last)
# ...
# UntranslatableCharacter: character with byte sequence 0xe2 0x82 0xac
# in encoding "UTF8" has no equivalent in encoding "LATIN1"

在极少数情况下,数据库中可能会有带有意外编码字符的字符串。使用客户端编码SQL_ASCII将禁用对来自数据库的数据的解码,这些数据将以bytes返回:

conn.execute("SET client_encoding TO SQL_ASCII")
conn.execute("SELECT entry FROM menu WHERE id = 1").fetchone()[0]
b'Cr\xc3\xa8me Br\xc3\xbbl\xc3\xa9e at 4.99\xe2\x82\xac'

或者,您可以将未知编码数据转换为bytea,以字节串形式对其进行检索,而其他字符串保持不变:请参阅二进制适配

请注意,PostgreSQL 文本不能包含字节值0x00。如果需要存储可能包含零值字节的 Python 字符串,则应使用bytea字段。

二进制适配

默认情况下,Python 中表示二进制类型(bytesbytearraymemoryview)的对象将被转换为bytea字段。默认情况下,接收到的数据返回为bytes

如果要将大型二进制数据(例如二进制文档或图像)存储在bytea字段中,则可能应该使用二进制格式来传递和返回值,否则二进制数据将进行ASCII转义,从而占用一些CPU时间和更多网络带宽。有关详细信息,请参阅二进制参数和结果

日期/时间类型适配

  • Python 中的 date 对象被转换为 PostgreSQL 中的date
  • Python 中的 datetime 对象被转换为 PostgreSQL 中的timestamp(如果它们没有tzinfo集合)或timestamptz(如果有tzinfo集合)。
  • Python 中的 time 对象被转换为 PostgreSQL 中的time(如果它们没有tzinfo集合)或timetz(如果有tzinfo集合)。
  • Python 中的 timedelta 对象被转换为 PostgreSQL 中的interval

返回 PostgreSQL 的timestamptz类型值时,时区设置为会话时区配置值,该值记录在Connection.info.timezone属性中,可用作 Python ZoneInfo 对象:

>>> conn.info.timezone
zoneinfo.ZoneInfo(key='Europe/London')

>>> conn.execute("select '2048-07-08 12:00'::timestamptz").fetchone()[0]
datetime.datetime(2048, 7, 8, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/London'))

PostgreSQL 中的timestamptz不是以“附加了时区的时间戳”的格式进行存储的:它始终以 UTC 格式存储时间戳,该时间戳在输出时会转换为会话时区配置指定的时区:

>>> conn.execute("SET TIMEZONE to 'Europe/Rome'")  # UTC+2 in summer
>>> conn.execute("SELECT '2042-07-01 12:00Z'::timestamptz").fetchone()[0]  # UTC input
datetime.datetime(2042, 7, 1, 14, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Rome'))

查看有关时区的 PostgreSQL 文档以获取所有详细信息。

JSON 适配

Psycopg 可以在 Python 对象和 PostgreSQL 中的 json/jsonb 类型之间进行映射,允许自定义使用的加载和转储函数。

由于有多种 Python 对象可以被视为 JSON(字典、列表、标量,如果使用自定义的转储函数,甚至还有日期/时间),Psycopg 要求您将要转储的对象作为 JSON 包装到包装器中:psycopg.types.json.JsonJsonb

from psycopg.types.json import Jsonb

thing = {"foo": ["bar", 42]}
conn.execute("INSERT INTO mytable VALUES (%s)", [Jsonb(thing)])

默认情况下,Psycopg 使用标准库 json.dumpsjson.loads 函数将 Python 对象序列化和反序列化为 JSON。如果要自定义序列化的发生方式,例如更改序列化参数或使用不同的 JSON 库,可以使用 psycopg.types.json.set_json_dumps()set_json_loads() 函数指定自己的函数,以全局应用或应用于特定上下文(连接或游标)。

from functools import partial
from psycopg.types.json import Jsonb, set_json_dumps, set_json_loads
import ujson

# Use a faster dump function
set_json_dumps(ujson.dumps)

# Return floating point values as Decimal, just in one connection
set_json_loads(partial(json.loads, parse_float=Decimal), conn)

conn.execute("SELECT %s", [Jsonb({"value": 123.45})]).fetchone()[0]
# {'value': Decimal('123.45')}

如果只需要对某些对象(包括同一查询中的不同配置)进行更具体的转储自定义,则可以在 Json/Jsonb 包装器中指定一个dumps参数,该参数设置的生效将会优先于使用set_json_dumps()定义的行为。

from uuid import UUID, uuid4

class UUIDEncoder(json.JSONEncoder):
    """A JSON encoder which can dump UUID."""
    def default(self, obj):
        if isinstance(obj, UUID):
            return str(obj)
        return json.JSONEncoder.default(self, obj)

uuid_dumps = partial(json.dumps, cls=UUIDEncoder)
obj = {"uuid": uuid4()}
cnn.execute("INSERT INTO objs VALUES %s", [Json(obj, dumps=uuid_dumps)])
# will insert: {'uuid': '0a40799d-3980-4c65-8315-2956b18ab0e1'}

列表适配

Python list 对象对应于 PostgreSQL 数组。只有包含相同类型对象的列表才能转储到 PostgreSQL 数组(但列表可以包含值为None的元素)。

如果您有要与运算符IN一起使用的值列表,请不要这么做。它不会起作用(无论是列表还是元组):

>>> conn.execute("SELECT * FROM mytable WHERE id IN %s", [[10,20,30]])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
psycopg.errors.SyntaxError: syntax error at or near "$1"
LINE 1: SELECT * FROM mytable WHERE id IN $1
                                          ^

可行的办法是,您可以使用 ‘= ANY()’ 表达式并将值作为列表(而不是元组)传递。

>>> conn.execute("SELECT * FROM mytable WHERE id = ANY(%s)", [[10,20,30]])

这也具有支持空列表的优点,而IN ()并不是有效的SQL。

UUID 适配

Python uuid.UUID 对象对应于 PostgreSQL 中的 UUID 类型

>>> conn.execute("select gen_random_uuid()").fetchone()[0]
UUID('97f0dd62-3bd2-459e-89b8-a5e36ea3c16c')

>>> from uuid import uuid4
>>> conn.execute("select gen_random_uuid() = %s", [uuid4()]).fetchone()[0]
False  # long shot

网络数据类型适配

ipaddress 模块中的对象会被转换为 PostgreSQL 中的网络地址类型

  • IPv4Address, IPv4Interface 对象会被转换为 PostgreSQL 中的inet类型。在从 PostgreSQL 转换到 Python 对象时,指示单个地址的inet值会被转换为IPv4Address,否则它们将转换为IPv4Interface
  • IPv4Network 对象对应于 PostgreSQL 中的cidr类型。
  • IPv6AddressIPv6InterfaceIPv6Network对象遵循相同的规则,具有inetcidr类型的 IPv6 值。
>>> conn.execute("select '192.168.0.1'::inet, '192.168.0.1/24'::inet").fetchone()
(IPv4Address('192.168.0.1'), IPv4Interface('192.168.0.1/24'))

>>> conn.execute("select '::ffff:1.2.3.0/120'::cidr").fetchone()[0]
IPv6Network('::ffff:102:300/120')

枚举型适配

版本 3.1 中的新功能。

Psycopg 可以将 Python Enum 子类适配到 PostgreSQL 枚举类型(使用 CREATE TYPE ... AS ENUM (...) 命令创建)。

为了设置枚举值的双向映射,您应该使用 EnumInfo 类获取有关 PostgreSQL 枚举的信息,并使用 register_enum() 注册它。未注册枚举和已注册枚举的表现行为是不同的。

  • 如果枚举没有使用register_enum()进行注册:

    • Enum类作为普通字符串转储,并使用其成员名称作为值。使用 unknown 类型的 oid,因此 PostgreSQL 应该能够在大多数上下文(例如一个枚举或者文本字段)中使用此字符串。

      *在 3.1 版本更改:*在以前的版本中,不支持转储纯枚举,并会引发“cannot adapt”错误。

    • 混合枚举根据其混合类型进行转储(因为class MyIntEnum(int, Enum)使用intEnum表示更具体,因此默认情况下会根据int规则转储)。

    • PostgreSQL 枚举作为 Python 字符串加载。如果要加载此类枚举的数组,则必须使用 types.TypeInfo.fetch() 查找它们的 OID,并使用 register() 注册它们。

  • 如果枚举已注册(使用 EnumInfo.fetch()register_enum()):

    • 无论是纯枚举类还是混合枚举类,都按名称转储。
    • 注册的 PostgreSQL 枚举会被作为注册的 Python 枚举成员加载回去。

EnumInfo 类

class psycopg.types.enum.EnumInfo(name, oid, array_oid, labels)

管理有关枚举类型的信息。

EnumInfoTypeInfo 的一个子类:有关通用的用法,请参阅后者的文档,尤其是 fetch() 方法。

EnumInfo.labels

labels

在调用 fetch() 之后,它包含了 PostgreSQL 枚举类型中定义的标签。

EnumInfo.enum

enum

调用 register_enum() 后,它将包含映射到已注册枚举的 Python 类型。

register_enum()

psycopg.types.enum.register_enum(info, context=None, enum=None, *, mapping=None)

注册适配器以加载和转储枚举类型。

参数:

注册后,获取已注册枚举的数据会将 PostgreSQL 枚举标签转换为相应的 Python 枚举成员。

如果未指定enum参数,则基于 PostgreSQL 枚举标签创建一个新的Enum

例:

>>> from enum import Enum, auto
>>> from psycopg.types.enum import EnumInfo, register_enum

>>> class UserRole(Enum):
...     ADMIN = auto()
...     EDITOR = auto()
...     GUEST = auto()

>>> conn.execute("CREATE TYPE user_role AS ENUM ('ADMIN', 'EDITOR', 'GUEST')")

>>> info = EnumInfo.fetch(conn, "user_role")
>>> register_enum(info, conn, UserRole)

>>> some_editor = info.enum.EDITOR
>>> some_editor
<UserRole.EDITOR: 2>

>>> conn.execute(
...     "SELECT pg_typeof(%(editor)s), %(editor)s",
...     {"editor": some_editor}
... ).fetchone()
('user_role', <UserRole.EDITOR: 2>)

>>> conn.execute(
...     "SELECT ARRAY[%s, %s]",
...     [UserRole.ADMIN, UserRole.GUEST]
... ).fetchone()
[<UserRole.ADMIN: 1>, <UserRole.GUEST: 3>]

如果 Python 和 PostgreSQL 枚举不能做到 1:1 匹配(例如,如果成员具有不同的名称,或者多个 Python 枚举值可以映射到同一个 PostgreSQL 枚举值,反之亦然),则可以使用mapping参数指定例外。

mapping应该是一个字典,其中 Python 枚举成员作为键,匹配的 PostgreSQL 枚举标签作为值,或者是由具有相同含义的(member, label)配对组成的列表(在某些成员重复时很有用)。顺序很重要:如果任意一侧的元素被多次指定,则序列中的最后一对会被优先匹配:

# Legacy roles, defined in medieval times.
>>> conn.execute(
...     "CREATE TYPE abbey_role AS ENUM ('ABBOT', 'SCRIBE', 'MONK', 'GUEST')")

>>> info = EnumInfo.fetch(conn, "abbey_role")
>>> register_enum(info, conn, UserRole, mapping=[
...     (UserRole.ADMIN, "ABBOT"),
...     (UserRole.EDITOR, "SCRIBE"),
...     (UserRole.EDITOR, "MONK")])

>>> conn.execute("SELECT '{ABBOT,SCRIBE,MONK,GUEST}'::abbey_role[]").fetchone()[0]
[<UserRole.ADMIN: 1>,
 <UserRole.EDITOR: 2>,
 <UserRole.EDITOR: 2>,
 <UserRole.GUEST: 3>]

>>> conn.execute("SELECT %s::text[]", [list(UserRole)]).fetchone()[0]
['ABBOT', 'MONK', 'GUEST']

一个特别有用的情况是,当 PostgreSQL 标签与基于str的枚举值匹配时,可以使用类似{m: m.value for m in enum}的映射:

>>> class LowercaseRole(str, Enum):
...     ADMIN = "admin"
...     EDITOR = "editor"
...     GUEST = "guest"

>>> conn.execute(
...     "CREATE TYPE lowercase_role AS ENUM ('admin', 'editor', 'guest')")

>>> info = EnumInfo.fetch(conn, "lowercase_role")
>>> register_enum(
...     info, conn, LowercaseRole, mapping={m: m.value for m in LowercaseRole})

>>> conn.execute("SELECT 'editor'::lowercase_role").fetchone()[0]
<LowercaseRole.EDITOR: 'editor'>