3

The title might not be the best description of what I'm trying to do but I'm not sure what to call this. I came across various seemingly related concepts with names like "decorators", "descriptors", and "metaclasses" but I don't know which of those approaches (if any) I should investigate further!

Given the following classes:

class AnimalGreeter(object):

    def __init__(self, greeting):
        self.greeting = greeting

    def greet(self, animal):
        print(self.greeting + ", " + animal + "!")

class MyGreeter(AnimalGreeter):

    animals = ['dog', 'parrot']

I'd like to be able to use an instance of MyGreeter like this:

greeter = MyGreeter("What's up")
greeter.parrot
# "What's up, parrot!"

Effectively, I would like it to function as if I had defined MyGreeter like this instead:

class MyGreeter(AnimalGreeter):

    @property
    def dog(self):
        self.greet('dog')

    @property
    def parrot(self):
        self.greet('parrot')

In other words, I want to dynamically define methods (dog(), etc.) with names derived from an attribute (animals) of the class (MyGreeter).

What is the best way to do this? Thanks a lot!

tino
  • 4,780
  • 5
  • 24
  • 30
  • 2
    Just implement `__getattr__(self, animal)`. – AChampion Sep 13 '17 at 15:55
  • Cant help here, but I'm interested in your use case. Why do you need this behavior? – FabienP Sep 13 '17 at 15:56
  • 1
    @FabienP, In my case `AnimalGreeter` is a Django model and `MyGreeter` is a custom mixin that handles images with configurable filenames. I want to have access to the image urls in Django's templates but Django's templating system does not permit passing arguments to instance methods. So I can't do the equivalent of `model.image_url('large')` in the template. But I can access instance properties in the templates, e.g., `model.large_image_url`. – tino Sep 13 '17 at 16:25
  • @tino: nice to know, thanks for the explanation! – FabienP Sep 13 '17 at 17:40

1 Answers1

4

An easy way to do this would just be to implement __getattr__(), e.g.:

class MyGreeter(AnimalGreeter):
    animals = {'dog', 'parrot'}

    def __getattr__(self, animal):
        if animal in self.animals:
            return self.greet(animal)
        raise AttributeError("'{}' object unknown animal '{}'".format(type(self).__name__, animal))

>>> greeter = MyGreeter("What's up")
>>> greeter.parrot
What's up, parrot!
>>> greeter.cat
...
AttributeError: 'MyGreeter' object unknown animal 'cat'
AChampion
  • 29,683
  • 4
  • 59
  • 75
  • Thanks! This was one of the suggestions I came across but was unsure if I should be concerned about overhead. `AnimalGreeter` has a lot of other attributes that are accessed frequently. Does this get executed every time I access any instance attribute? – tino Sep 13 '17 at 16:28
  • 2
    @tino No, `__getattr__` is only called if you access an attribute that "doesn't exist", i.e. it's called when an AttributeError would be thrown. [Read the docs.](https://docs.python.org/3/reference/datamodel.html#object.__getattr__) – Aran-Fey Sep 13 '17 at 17:21
  • @Rawing, Ah, that makes perfect sense. Thanks for the link and the explanation! – tino Sep 13 '17 at 20:02