SQLAlchemy类的__str__()方法如何正确自动生成?

8
我想要展示/打印我的Sqlalchemy类,使其看起来整洁明了。
在《Is there a way to auto generate a __str__() implementation in python?》中,答案你可以使用vars、dir等迭代实例属性的方法...对于简单的类很有帮助。
但是当我尝试将其应用于Sqlalchemy类(比如Python's SQLAlchemy入门教程中的类),除了成员变量之外,我还得到了以下条目作为成员变量: _sa_instance_state=<sqlalchemy.orm.state.InstanceState object at 0x000000004CEBCC0> 我该如何避免这个条目出现在__str__表示中?
为了完整起见,我也把链接的stackoverflow问题的解决方案放在下面。
import os
import sys
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy import create_engine

Base = declarative_base()

class Person(Base):
    __tablename__ = 'person'
    # Here we define columns for the table person
    # Notice that each column is also a normal Python instance attribute.
    id = Column(Integer, primary_key=True)
    name = Column(String(250), nullable=False)

如上所述,这是在Python中是否有一种自动生成__str__()实现的方法?的解决方案:
def auto_str(cls):
    def __str__(self):
        return '%s(%s)' % (
            type(self).__name__,
            ', '.join('%s=%s' % item for item in vars(self).items())
        )
    cls.__str__ = __str__
    return cls

@auto_str
class Foo(object):
    def __init__(self, value_1, value_2):
        self.attribute_1 = value_1
         self.attribute_2 = value_2

应用:

>>> str(Foo('bar', 'ping'))
'Foo(attribute_2=ping, attribute_1=bar)'

类似问题:https://dev59.com/LlMI5IYBdhLWcg3wn9H6 - Intrastellar Explorer
2个回答

18

这是我使用的:

def todict(obj):
    """ Return the object's dict excluding private attributes, 
    sqlalchemy state and relationship attributes.
    """
    excl = ('_sa_adapter', '_sa_instance_state')
    return {k: v for k, v in vars(obj).items() if not k.startswith('_') and
            not any(hasattr(v, a) for a in excl)}

class Base:

    def __repr__(self):
        params = ', '.join(f'{k}={v}' for k, v in todict(self).items())
        return f"{self.__class__.__name__}({params})"

Base = declarative_base(cls=Base)
任何继承自 Base 的模型都将拥有默认的 __repr__() 方法,如果我需要做一些不同的事情,我只需在该特定类上重写该方法。该字符串排除了以下划线开头的任何私有属性值、SQLAlchemy 实例状态对象和任何关系属性值。我排除关系属性值,因为我通常不希望 repr 引起关系的延迟加载,并且在关系是双向的情况下,包括关系属性值可能导致无限递归。结果看起来像:ClassName(attr=val, ...)。--编辑--我上面提到的 todict() 功能是一个助手,我经常使用它来构造 SQLA 对象的字典(主要用于序列化)。在这种情况下,我懒惰地使用它,但它并不是非常高效,因为它正在构造一个字典(在 todict() 中)来构造另一个字典(在 __repr__() 中)。我已经修改了模式,现在调用生成器:
def keyvalgen(obj):
    """ Generate attr name/val pairs, filtering out SQLA attrs."""
    excl = ('_sa_adapter', '_sa_instance_state')
    for k, v in vars(obj).items():
        if not k.startswith('_') and not any(hasattr(v, a) for a in excl):
            yield k, v

那么基础的底座看起来就像这样:

class Base:

    def __repr__(self):
        params = ', '.join(f'{k}={v}' for k, v in keyvalgen(self))
        return f"{self.__class__.__name__}({params})"

todict() 函数也利用了 keyvalgen() 生成器,但现在不再需要构建 repr 了。


1
优美而干净的实现。 - Intrastellar Explorer
如何只定义__str__()而不是__repl__()?这个问题似乎只是想要打印一个SQLAlchemy对象。 - pplorins

11

我在我的基础模型上定义了这个__repr__方法:

def __repr__(self):
    fmt = '{}.{}({})'
    package = self.__class__.__module__
    class_ = self.__class__.__name__
    attrs = sorted(
        (k, getattr(self, k)) for k in self.__mapper__.columns.keys()
    )
    sattrs = ', '.join('{}={!r}'.format(*x) for x in attrs)
    return fmt.format(package, class_, sattrs)

该方法按字母顺序显示表的所有列的名称(但不包括关系),以及它们的值的repr。我通常不定义__str__,除非我需要特定的形式 - 也许str(User(name='Alice'))只是Alice - 因此str(model_instance)将调用__repr__方法。

示例代码

import datetime

import sqlalchemy as sa
from sqlalchemy.ext import declarative


class BaseModel(object):

    __abstract__ = True

    def __repr__(self):
        fmt = u'{}.{}({})'
        package = self.__class__.__module__
        class_ = self.__class__.__name__
        attrs = sorted(
            (k, getattr(self, k)) for k in self.__mapper__.columns.keys()
        )
        sattrs = u', '.join('{}={!r}'.format(*x) for x in attrs)
        return fmt.format(package, class_, sattrs)


Base = declarative.declarative_base(cls=BaseModel)


class MyModel(Base):

    __tablename__ = 'mytable'

    foo = sa.Column(sa.Unicode(32))
    bar = sa.Column('bar_id', sa.Integer, primary_key=True)
    baz = sa.Column(sa.DateTime)

>>> mm = models.MyModel(foo='Foo', bar=42, baz=datetime.datetime.now())
>>> mm
models.MyModel(bar=42, baz=datetime.datetime(2019, 1, 4, 7, 37, 59, 350432), foo='Foo')

这个答案的原始版本使用模型的__table__属性来访问列名。我已经改为使用模型的__mapper__,因为SQLAlchemy允许模型属性名称(存储在映射器中)与数据库中的列名(存储在表中)不同。 MyModel.bar演示了这一点。


1
一个写得很好的答案,并且附带有一个BaseModel的示例代码。我已经实现了这个,谢谢! - Intrastellar Explorer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接