Hybrid Attributes

Hybrid attributes encapsulate functionality that operates at both the Pythonand SQL levels. The idea for hybrid attributes comes from a feature of thesame name in SQLAlchemy.Consider the following example:

  1. class Interval(Model):
  2. start = IntegerField()
  3. end = IntegerField()
  4.  
  5. @hybrid_property
  6. def length(self):
  7. return self.end - self.start
  8.  
  9. @hybrid_method
  10. def contains(self, point):
  11. return (self.start <= point) & (point < self.end)

The hybrid attribute gets its name from the fact that the lengthattribute will behave differently depending on whether it is accessed via theInterval class or an Interval instance.

If accessed via an instance, then it behaves just as you would expect.

If accessed via the Interval.length class attribute, however, the lengthcalculation will be expressed as a SQL expression. For example:

  1. query = Interval.select().where(Interval.length > 5)

This query will be equivalent to the following SQL:

  1. SELECT "t1"."id", "t1"."start", "t1"."end"
  2. FROM "interval" AS t1
  3. WHERE (("t1"."end" - "t1"."start") > 5)

The playhouse.hybrid module also contains a decorator for implementinghybrid methods which can accept parameters. As with hybrid properties, whenaccessed via a model instance, then the function executes normally as-written.When the hybrid method is called on the class, however, it will generate a SQLexpression.

Example:

  1. query = Interval.select().where(Interval.contains(2))

This query is equivalent to the following SQL:

  1. SELECT "t1"."id", "t1"."start", "t1"."end"
  2. FROM "interval" AS t1
  3. WHERE (("t1"."start" <= 2) AND (2 < "t1"."end"))

There is an additional API for situations where the python implementation differs slightly from the SQL implementation. Let’s add a radius method to the Interval model. Because this method calculates an absolute value, we will use the Python abs() function for the instance portion and the fn.ABS() SQL function for the class portion.

  1. class Interval(Model):
  2. start = IntegerField()
  3. end = IntegerField()
  4.  
  5. @hybrid_property
  6. def length(self):
  7. return self.end - self.start
  8.  
  9. @hybrid_property
  10. def radius(self):
  11. return abs(self.length) / 2
  12.  
  13. @radius.expression
  14. def radius(cls):
  15. return fn.ABS(cls.length) / 2

What is neat is that both the radius implementations refer to thelength hybrid attribute! When accessed via an Interval instance, theradius calculation will be executed in Python. When invoked via an Intervalclass, we will get the appropriate SQL.

Example:

  1. query = Interval.select().where(Interval.radius < 3)

This query is equivalent to the following SQL:

  1. SELECT "t1"."id", "t1"."start", "t1"."end"
  2. FROM "interval" AS t1
  3. WHERE ((abs("t1"."end" - "t1"."start") / 2) < 3)

Pretty neat, right? Thanks for the cool idea, SQLAlchemy!

Hybrid API

  • class hybridmethod(_func[, expr=None])
  • Method decorator that allows the definition of a Python object method withboth instance-level and class-level behavior.

Example:

  1. class Interval(Model):
  2. start = IntegerField()
  3. end = IntegerField()
  4.  
  5. @hybrid_method
  6. def contains(self, point):
  7. return (self.start <= point) & (point < self.end)

When called with an Interval instance, the contains method willbehave as you would expect. When called as a classmethod, though, a SQLexpression will be generated:

  1. query = Interval.select().where(Interval.contains(2))

Would generate the following SQL:

  1. SELECT "t1"."id", "t1"."start", "t1"."end"
  2. FROM "interval" AS t1
  3. WHERE (("t1"."start" <= 2) AND (2 < "t1"."end"))
  • expression(expr)
  • Method decorator for specifying the SQL-expression producing method.
  • class hybridproperty(_fget[, fset=None[, fdel=None[, expr=None]]])
  • Method decorator that allows the definition of a Python object propertywith both instance-level and class-level behavior.

Examples:

  1. class Interval(Model):
  2. start = IntegerField()
  3. end = IntegerField()
  4.  
  5. @hybrid_property
  6. def length(self):
  7. return self.end - self.start
  8.  
  9. @hybrid_property
  10. def radius(self):
  11. return abs(self.length) / 2
  12.  
  13. @radius.expression
  14. def radius(cls):
  15. return fn.ABS(cls.length) / 2

When accessed on an Interval instance, the length and radiusproperties will behave as you would expect. When accessed as classattributes, though, a SQL expression will be generated instead:

  1. query = (Interval
  2. .select()
  3. .where(
  4. (Interval.length > 6) &
  5. (Interval.radius >= 3)))

Would generate the following SQL:

  1. SELECT "t1"."id", "t1"."start", "t1"."end"
  2. FROM "interval" AS t1
  3. WHERE (
  4. (("t1"."end" - "t1"."start") > 6) AND
  5. ((abs("t1"."end" - "t1"."start") / 2) >= 3)
  6. )