1

Description & What I've tried:

I have seen many posts in stackoverflow about binding methods to class instances (I'm aware there are bunch of duplicates already).

However I havent found a discussion referring to binding a method to the class itself. I can think of workarounds but I'm curious if there is a simple way to achieve following:

import types
def quacks(some_class):
    def quack(self, number_of_quacks):
       self.number_of_quacks = number_of_quacks
    setattr(some_class, "quack", types.MethodType(quack, some_class))
    return some_class

@quacks
class Duck:
   pass

but above would not work:

d1 = Duck()
d2 = Duck()
d1.quack(1)
d2.quack(2)
print(d2.number_of_quacks)
# 2
print(d1.number_of_quacks)
# 2

because quack is actually modifying the class itself rather than the instance.

There are two workarounds I can think of. Either something like below:

class Duck:
    def __init__(self):
        setattr(self, "quack", types.MethodType(quack, self))

or something like

class Quacks:
    def quack(self, number_of_quacks):
        self.number_of_quacks = number_of_quacks

class Duck(Quacks):
    pass

Question: So my question is, is there a simple way to achieve the simple @quacks class decorator I described above?

Why I'm asking: I intend to create a set of functions to modularly add common methods I use to classes. If I dont quit this project, the list is likely to grow over time and I would prefer to have it look nice on code definition. And as a matter of taste, I think option 1 below looks nicer than option 2:

# option 1
@quacks
@walks
@has_wings
@is_white
@stuff
class Duck:
    pass

# option 2
class Duck(
  Quacks,
  Walks,
  HasWings,
  IsWhite,
  Stuff):
    pass
ozgeneral
  • 6,079
  • 2
  • 30
  • 45
  • 1
    The pattern proposed in option 2 has a name, they are called "mix-ins". Here's an SO Q/A about it: https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful – SethMMorton Apr 05 '20 at 16:36

2 Answers2

1

If you don't mind changing your desired syntax completely to get the functionality you want, you can dynamically construct classes with type (see second signature).

The first argument is the name of the class, the second is a tuple of superclasses, and the third is a dictionary of attributes to add.

Duck = type("Duck", (), {
    "quack", quack_function,
    "walk", walk_function,
    ...
})

So, instead of decorators that inject the appropriate functionality after creation, you are simply adding the functionality directly at the time of creation. The nice thing about this method is that you can programatically build the attribute dictionary, whereas with decorators you cannot.

SethMMorton
  • 45,752
  • 12
  • 65
  • 86
  • Thanks a lot for the alternative, I didn't know this was possible and it is really useful. It does not fit to this particular use case because I want to have a package that you can import these decorators and add to any existing class you already have working (so it would also force everyone who intend to use the class to re-organize their code completely). But either way, this is a super cool trick that definitely will help me at one point or another, so thanks again! – ozgeneral Apr 05 '20 at 16:50
  • @ozgeneral Then in that case I’d go with your Option 2 (mix-ins). In the linked answer, the example they show is exactly what you are trying to achieve. – SethMMorton Apr 05 '20 at 16:55
  • Just read the link, probably I will refactor to that. But out of curiosity, is there a practical reason Option 2 is better apart from that being the standard convention? – ozgeneral Apr 05 '20 at 17:09
  • You don’t need to write a hack to get it to work - it uses standard functionality provided by the language. This means there’s less that could go wrong. – SethMMorton Apr 05 '20 at 17:11
  • Just had an idea of combining both approaches which seem to be nice, see the edit in the answer below – ozgeneral Apr 05 '20 at 17:17
0

Found another workaround, I guess below would do it for me.

def quacks(some_class):
    def quack(self, number_of_quacks):
        self.number_of_quacks = number_of_quacks

    old__init__ = some_class.__init__
    def new__init__(self, *args, **kwargs):
        setattr(self, "quack", types.MethodType(quack, self))
        old__init__(self, *args, **kwargs)
    setattr(some_class, "__init__", new__init__)
    return some_class

Feel free to add any other alternatives, or if you see any drawbacks with this approach.

Edit: a less hacky way inspired from @SethMMorton's answer:


def quack(self, number_of_quacks):
    self.number_of_quacks = number_of_quacks

def add_mixin(some_class, some_fn):
    new_class = type(some_class.__name__, (some_class,), {
        some_fn.__name__: some_fn
    })
    return new_class

def quacks(some_class):
    return add_mixin(some_class, quack)

@quacks
class Duck:
    pass

d1 = Duck()
d2 = Duck()
d1.quack(1)
d2.quack(2)
print(d1.number_of_quacks)
print(d2.number_of_quacks)
ozgeneral
  • 6,079
  • 2
  • 30
  • 45