7

I implemented a metaclass that tears down the class attributes for classes created with it and builds methods from the data from those arguments, then attaches those dynamically created methods directly to the class object (the class in question allows for easy definition of web form objects for use in a web testing framework). It has been working just fine, but now I have a need to add a more complex type of method, which, to try to keep things clean, I implemented as a callable class. Unfortunately, when I try to call the callable class on an instance, it is treated as a class attribute instead of an instance method, and when called, only receives its own self. I can see why this happens, but I was hoping someone might have a better solution than the ones I've come up with. Simplified illustration of the problem:

class Foo(object):
    def __init__(self, name, val):
        self.name = name
        self.val = val
        self.__name__ = name + '_foo'
        self.name = name
    # This doesn't work as I'd wish
    def __call__(self, instance):
        return self.name + str(self.val + instance.val)

def get_methods(name, foo_val):
    foo = Foo(name, foo_val)
    def bar(self):
        return name + str(self.val + 2)
    bar.__name__ = name + '_bar'
    return foo, bar

class Baz(object):
    def __init__(self, val):
        self.val = val

for method in get_methods('biff', 1):
    setattr(Baz, method.__name__, method)
baz = Baz(10)
# baz.val == 10
# baz.biff_foo() == 'biff11'
# baz.biff_bar() == 'biff12'

I've thought of:

  1. Using a descriptor, but that seems way more complex than is necessary here
  2. Using a closure inside of a factory for foo, but nested closures are ugly and messy replacements for objects most of the time, imo
  3. Wrapping the Foo instance in a method that passes its self down to the Foo instance as instance, basically a decorator, that is what I actually add to Baz, but that seems superfluous and basically just a more complicated way of doing the same thing as (2)

Is there a better way then any of these to try to accomplish what I want, or should I just bite the bullet and use some closure factory type pattern?

ShadowRanger
  • 143,180
  • 12
  • 188
  • 271
Silas Ray
  • 25,682
  • 5
  • 48
  • 63

2 Answers2

6

One way to do this is to attach the callable objects to the class as unbound methods. The method constructor will work with arbitrary callables (i.e. instances of classes with a __call__() method)—not just functions.

from types import MethodType

class Foo(object):
    def __init__(self, name, val):
        self.val = val
        self.__name__ = name + '_foo'
        self.name = name
    def __call__(self, instance):
        return self.name + str(self.val + instance.val)

class Baz(object):
    def __init__(self, val):
        self.val = val

Baz.biff = MethodType(Foo("biff", 42), None, Baz)

b = Baz(13)
print b.biff()
>>> biff55

In Python 3, there's no such thing as an unbound instance method (classes just have regular functions attached) so you might instead make your Foo class a descriptor that returns a bound instance method by giving it a __get__() method. (Actually, that approach will work in Python 2.x as well, but the above will perform a little better.)

from types import MethodType

class Foo(object):
    def __init__(self, name, val):
        self.name = name
        self.val = val
        self.__name__ = name + '_foo'
        self.name = name
    def __call__(self, instance):
        return self.name + str(self.val + instance.val)
    def __get__(self, instance, owner):
        return MethodType(self, instance) if instance else self
        # Python 2: MethodType(self, instance, owner)

class Baz(object):
    def __init__(self, val):
        self.val = val

Baz.biff = Foo("biff", 42)

b = Baz(13)
print b.biff()
>>> biff55
kindall
  • 178,883
  • 35
  • 278
  • 309
  • This doesn't work in Python 3, since unbound methods no longer exist (`MethodType` only takes two arguments, and the second must not be None). – Blckknght Jan 25 '13 at 17:52
  • I think I was updating that while you wrote that comment. :-) – kindall Jan 25 '13 at 18:00
  • I've written descriptors before, but I guess the descriptor isn't that messy in this case, really... I've poked at `MethodType` before, but to be honest, it always kind of scares me, since it seems so easy to break, what with needing to basically initialize its namespace and whatnot. I ended up implementing another closure, but I might go back and look at replacing it with this. Thanks. – Silas Ray Jan 25 '13 at 18:24
  • `MethodType` is not that complicated, really. You need a function (or other callable), an instance (if you're making an instance method; in Python 3 that's the only kind you can make), and a type (in Python 2.x; in Python 3 it gets the type from the instance). That's it. – kindall Jan 30 '13 at 23:07
  • Why are there two assignments of `self.name = name` in `__init__`? Is that just a mistake or is it some bizarre black magic? It's so distracting I feel like reaching in and deleting the extra line. When I see things like that I find it hard to trust the rest of the code. – NeilG Mar 02 '23 at 02:29
  • It's indeed an error. Doesn't hurt anything, but as you say, raises confusing questions about why it's there. Baleeted! – kindall Mar 02 '23 at 13:26
1

The trouble you're running into is that your object is not being bound as a method of the Baz class you're putting it in. This is because it is not a descriptor, which regular functions are!

You can fix this by adding a simple __get__ method to your Foo class that makes it into a method when it's accessed as a descriptor:

import types

class Foo(object):
    # your other stuff here

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self # unbound
        else:
            return types.MethodType(self, obj) # bound to obj
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • I don't think you need the `if/else` in that since `__get__()` won't be called when a `Foo` instance is accessed through a class anyway. – kindall Jan 25 '13 at 18:25
  • @kindall: If I don't have the `if` block, I get an exception when I access the `Foo` instance via the class: `TypeError: self must not be None`. That's "class binding" which still exists in Python 3, even though functions don't use it to make unbound methods any more. – Blckknght Jan 25 '13 at 19:26
  • Python docs: "The only difference [of methods] from regular functions is that the first argument is reserved for the object instance". Also Python docs, the very next paragraph: "functions include the __get__() method for binding methods during attribute access". Python docs are so loose (or just plain missing, or in this case just plain contradictory and therefore *wrong*) on how to manage class construction that it becomes very hard to customise. What *else* do "regular functions" have that is different from methods that the docs are pretending doesn't exist? – NeilG Mar 02 '23 at 02:24
  • @NeilG: I think you're misunderstanding that documentation. Methods defined in classes *are* functions. They only get turned into a `MethodType` object when they're looked up on an instance. That's because functions always have a `__get__` method of their own, so when they're used as descriptors, they make methods. – Blckknght Mar 02 '23 at 17:15
  • I'm beginning to understand that I've opened a Pandora's box with this, @Blckknght. I'm clearly not understanding the docs so I guess I should stop criticising. There are functions, unbound methods, bound methods, binding on call instead of assignment so the `__get__` method is just the tip of the iceberg. All I want to do is add a function to a `class` as a `classmethod` at run time, but it seems a lot harder than I thought it would be. https://stackoverflow.com/a/75601761 – NeilG Mar 03 '23 at 02:14
  • `classmethod` is a callable. If you have a function `__modify_schema__` that you want to be a class method on a class `Factor`, use `Factor.__modify_schema__ = classmethod(__modify_schema__)`. There's no reason to use pre-bound methods, that way leads to madness (and won't work for subclasses, if you ever needed it to). The `@classmethod` syntax that is normally used to apply decorators like that to functions is just a fancy way of doing `somename = classmethod(somename)`. And to tie back to this answer, `classmethod` returns a descriptor object. It binds to the class, rather than the instance – Blckknght Mar 03 '23 at 02:58