适配基本的 Python 类型
许多标准的 Python 类型被适配到了 SQL,并在执行查询时作为 Python 对象返回。
在 Python 和 PostgreSQL 之间转换以下数据类型是自适应的,不需要任何配置。如果您需要配置类型转换,请查看数据适配配置。
Python 布尔型值True
和False
会被转换为等效的 PostgreSQL 布尔类型:
>>> cur.execute("SELECT %s, %s", (True, False))
# equivalent to "SELECT true, false"
- Python 中的
int
值可以根据其数值转换为 PostgreSQL 中的smallint
、integer
、bigint
或numeric
。Psycopg 会选择可用的最小的数值类型,因为 PostgreSQL 可以自动地从小数值类型转换到大数值类型(例如,在 PostgreSQL 期望integer
的地方传递一个smallint
也会被接受),但不会自动从大数值类型转换到小数值类型(例如,如果一个函数有一个integer
参数,传递一个bigint
值会失败,即使数值是 1)。 - Python 中的
float
值会被转换为 PostgreSQL 中的float8
。 - Python 中的
Decimal
值会被转换为 PostgreSQL 中的numeric
。
当需要从 PostgreSQL 中的数值转换到 Python 中的数值类型时,较小的数值类型(int2
,int4
,float4
)会被提升为 Python 中对应的较大的数值类型。
出于提升性能或易于操作的考虑,有时您可能更愿意将numeric
类型的数值以float
类型进行接收:您可以配置适配器以将 PostgreSQL 数字转换为 Python 浮点数。这当然可能会带来精度的损失。
Python 中的 str
会被转换为 PostgreSQL 字符串语法,而 PostgreSQL 中的字符串类型(如text
和varchar
)会被转换回 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 中表示二进制类型(bytes
、bytearray
、memoryview
)的对象将被转换为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 文档以获取所有详细信息。
Psycopg 可以在 Python 对象和 PostgreSQL 中的 json/jsonb 类型之间进行映射,允许自定义使用的加载和转储函数。
由于有多种 Python 对象可以被视为 JSON(字典、列表、标量,如果使用自定义的转储函数,甚至还有日期/时间),Psycopg 要求您将要转储的对象作为 JSON 包装到包装器中:psycopg.types.json.Json
或 Jsonb
。
from psycopg.types.json import Jsonb
thing = {"foo": ["bar", 42]}
conn.execute("INSERT INTO mytable VALUES (%s)", [Jsonb(thing)])
默认情况下,Psycopg 使用标准库 json.dumps
和 json.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。
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
类型。IPv6Address
、IPv6Interface
、IPv6Network
对象遵循相同的规则,具有inet
和cidr
类型的 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)
使用int
比Enum
表示更具体,因此默认情况下会根据int
规则转储)。 -
PostgreSQL 枚举作为 Python 字符串加载。如果要加载此类枚举的数组,则必须使用
types.TypeInfo.fetch()
查找它们的 OID,并使用register()
注册它们。
-
-
如果枚举已注册(使用
EnumInfo
.fetch()
和register_enum()
):- 无论是纯枚举类还是混合枚举类,都按名称转储。
- 注册的 PostgreSQL 枚举会被作为注册的 Python 枚举成员加载回去。
class psycopg.types.enum.EnumInfo(name, oid, array_oid, labels)
管理有关枚举类型的信息。
EnumInfo
是 TypeInfo
的一个子类:有关通用的用法,请参阅后者的文档,尤其是 fetch()
方法。
labels
在调用 fetch()
之后,它包含了 PostgreSQL 枚举类型中定义的标签。
enum
调用 register_enum()
后,它将包含映射到已注册枚举的 Python 类型。
psycopg.types.enum.register_enum(info, context=None, enum=None, *, mapping=None)
注册适配器以加载和转储枚举类型。
参数:
- info (
EnumInfo
) – 包含有关要注册的枚举的信息的对象。 - context (
Optional
[AdaptContext
]) – 注册类型适配器的上下文。如果为None
,注册将会应用到全局。 - enum (
Optional
[Type
[TypeVar
(E
, bound=Enum
)]]) – 与 PostgreSQL 匹配的 Python 枚举类型。如果为None
,将以EnumInfo.enum
的形式生成一个新的枚举。 - mapping (
Union
[Mapping
[TypeVar
(E
, bound=Enum
),str
],Sequence
[Tuple
[TypeVar
(E
, bound=Enum
),str
]],None
]) – 覆盖enum
成员和info
标签之间的映射。
注册后,获取已注册枚举的数据会将 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'>