I would like to display / print my sqlalchemy classes nice and clean.
In Is there a way to auto generate a __str__()
implementation in python?
the answer You can iterate instance attributes using vars, dir, ...:... helps in the case of simple classes.
When I try to apply it to a Sqlalchemy
class (like the one from
Introductory Tutorial of Python’s SQLAlchemy - see below), I get - apart from the member variables also the following entry as a member variable:
_sa_instance_state=<sqlalchemy.orm.state.InstanceState object at 0x000000004CEBCC0>
How can I avoid that this entry appears in the __str__
representation?
For the sake of completeness, I put the solution of the linked stackoverflow question below, too.
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)
As mentioned, this is the solution from Is there a way to auto generate a __str__
() implementation in python?:
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
Applied:
>>> str(Foo('bar', 'ping'))
'Foo(attribute_2=ping, attribute_1=bar)'
The __repr__ function is defined by the designer of a type, in order to provide a means for users of the type to represent values of that type unambiguously, with a string.
all() method. The Query object, when asked to return full entities, will deduplicate entries based on primary key, meaning if the same primary key value would appear in the results more than once, only one object of that primary key would be present.
ORM, which stands for Object Relational Mapper, is the specialization of the Data Mapper design pattern that addresses relational databases like MySQL, Oracle, and PostgreSQL.
This is what I use:
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)
Any models that inherit from Base
will have the default __repr__()
method defined and if I need to do something different I can just override the method on that particular class.
It excludes the value of any private attributes denoted with a leading underscore, the SQLAlchemy instance state object, and any relationship attributes from the string. I exclude the relationship attributes as I most often don't want the repr to cause a relationship to lazy load, and where the relationship is bi-directional, including relationship attribs can cause infinite recursion.
The result looks like: ClassName(attr=val, ...)
.
--EDIT--
The todict()
func that I mention above is a helper that I often call upon to construct a dict
out of a SQLA object, mostly for serialisation. I was lazily using it in this context but it isn't very efficient as it's constructing a dict
(in todict()
) to construct a dict
(in __repr__()
). I've since modified the pattern to call upon a generator:
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
Then the base Base looks like this:
class Base:
def __repr__(self):
params = ', '.join(f'{k}={v}' for k, v in keyvalgen(self))
return f"{self.__class__.__name__}({params})"
The todict()
func leverages off of the keyvalgen()
generator as well but isn't needed to construct the repr anymore.
I define this __repr__
method on my base model:
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)
The method displays the names of all of a table's columns (but not relationships), and the repr
of their values, in alphabetical order. I don't usually define a __str__
unless I need a particular form - perhaps str(User(name='Alice'))
would just be Alice
- so str(model_instance)
will call the __repr__
method.
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')
The original version of this answer used the model's __table__
attribute to access the column names. I've changed it to use the model's __mapper__
instead because SQLAlchemy allows the model attribute name (stored in the mapper) to be different from the column name in the database (stored in the table). MyModel.bar
demonstrates this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With