Michael Bianconi
Michael Bianconi

Reputation: 5242

Including common property decorators

I'm looking for a shorthand to add common property decorators to classes.

class Animal:
    def __init__(self):
        self._attributes = {}
    

class Dog(Animal):

    @property
    def color(self):
        return super()._attributes.get('color', None)

    @color.setter
    def color(self, value):
        if value is not None:
            super()._attributes['color'] = value
        else:
            super()._attributes.pop('color', None)


class Cat(Animal):

    @property
    def color(self):
        return super()._attributes.get('color', None)

    @color.setter
    def color(self, value):
        if value is not None:
            super()._attributes['color'] = value
        else:
            super()._attributes.pop('color', None)


class InvisibleMan(Animal):
    pass

I'm looking for the easiest way to "package" the color property so I can assign it to Dog and Cat, but not InvisibleMan. Something like this (although in actuality there will be ~8 such properties and ~15 such classes)

    class Dog(Animal):

        def __init__(self):
            super().__init__()
            includeColorProperty(self)

Upvotes: 0

Views: 47

Answers (2)

chris
chris

Reputation: 2063

Have you considered descriptors, instead of a decorator?

In a nutshell, descriptors give you fine-grained control over attribute storage. (In fact, the property decorator builds a descriptor under the hood!) Here are some Python docs that may be helpful.

Anyway, sticking with your pattern, a descriptor that manipulates _attributes would look something like this:

class Color:

    def __get__(self, obj, objtype=None):
        return obj._attributes.get('color')
        
    def __set__(self, obj, value):
        if value is None:
            obj._attributes.pop('color', None)
        else:
            obj._attributes['color'] = value

where obj is a reference to the Dog instance, et al.

(Note the __get__ and __set__ methods match your getter and setter, respectively.)

Then, plug the descriptor into your classes like this:

class Animal:
    def __init__(self):
        self._attributes = {}
    

class Dog(Animal):

    color = Color()


class Cat(Animal):

    color = Color()


class InvisibleMan(Animal):
    pass

You can see in this example the behaviors you're looking for are preserved: instances maintain their own _attributes, and InvisibleMan has no color:

>>> d1, d2 = Dog(), Dog()
>>> d1.color = 'blue'
>>> d1.color, d2.color
('blue', None)
>>>
>>>
>>> x = InvisibleMan()
>>> x.color
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'InvisibleMan' object has no attribute 'color'

Personally, I also find this a bit easier to read when many properties are involved, as you mentioned is true in your case. Want to know what properties are available for a given type? They're listed out right at the top, no surprises.

Upvotes: 1

Sheena
Sheena

Reputation: 16242

You have about options.

Firstly, multiple inheritance:

# this is the best way to do things if lots of stuff is invisible 

class HasColor:
   # getter and setter go here

class Dog(Animal,HasColor):
   ...

OR

# This is probably the best one way to do things, if not many things are invisible 

class Invisible:
    @property 
    def color(self):
        raise AttributeError("very meaningful message")


class InvisibleMan(Invisible,Animal):  # THE ORDER HERE MATTERS!!
    etc

Option 2 would be to override the getter and setter in invisible man:

class Dog(Animal):
   ...

class InvisibleMan(Animal): 
    @property 
    def color(self):
        raise AttributeError("very meaningful message")

Bonus option:

If you want to turn invisibility on and off on an instance then you want to do something else. I'm not sure if you want this but:

class Animal:
   cloaking_on = False 
   
   @property 
   def color(self):
      if self.cloaking_on:
          raise AttributeError(etc)
      etc 

Then you can have a way to set cloaking on and off and make all Cats invisible by default.

Upvotes: 0

Related Questions