Adding a method/property to an existing instance dynamically in Python.


There are a number of way to add a new method to an existing instance dynamically in Python without needing to change its class and without affecting other existing objects.

A pre-defined class called Cat

Instantiate two objects called Tuxedo and Calico respectively.
class Cat(object):
    def __init__(self, name):
        self.name = '[inner] ' + name

tuxedo = Cat('Tuxedo')
calico = Cat('Calico')

# normal functions
def meow(self):
    print('%s is meowing.' % self.name)

def sleep(self):
    print('%s is sleeping.' % self.name)
    
def catch(self, stuff):
    print('%s is catching %s' % (self.name, stuff))

Use functools.partial

We can see there is no defined function in the class. Let's add a meow() function to an instance. Use functools.partial to pre-bind the function and store it in the object dictionary so it can be accessed like a method.
from functools import partial

tuxedo.meow = partial(meow, tuxedo)  # pre-bound and stored in the instance
tuxedo.meow()  # access like a normal method

>> [inner] Tuxedo is meowing.
Use a helper function in a more elegant way.
def bind(obj, func):
    'Bind a function and store it in an object'
    setattr(obj, func.__name__, partial(func, obj))

bind(calico, meow)
calico.meow()

>> [inner] Calico is meowing.

Use types

The argument signature for types.MethodType is (function, instance, class):
import types

def bind(obj, func):
    setattr(obj, func.__name__, types.MethodType(func, obj))

bind(tuxedo, sleep)
tuxedo.sleep()

>> [inner] Tuxedo is sleeping.

Use the descriptor method, __get__

Dotted lookups on functions call the __get__ method of the function with the instance, binding the object to the method and thus creating a "bound method."
calico.catch = catch.__get__(calico)
calico.catch('a mouse')

>> [inner] Calico catches a mouse

Use lexical binding

def bind(instance, method):
    def binding_scope_fn(*args, **kwargs):
        return method(instance, *args, **kwargs)

    return binding_scope_fn

tuxedo.catch = bind(tuxedo, catch)
tuxedo.catch('two mice')

>> [inner] Tuxedo catches two mice

Unbound function as an object attribute - why this doesn't work:

tuxedo.catch = catch
tuxedo.catch('three mice')

>> tuxedo.catch('three mice')
TypeError: catch() missing 1 required positional argument: 'stuff'
We can make the unbound function work by explicitly passing the instance (or anything, since this method doesn't actually use the self argument variable), but it would not be consistent with the expected signature of other instances (if we're monkey-patching this instance):
tuxedo.catch(tuxedo, 'three mice')

>> [inner] Tuxedo catches three mice

Use namedtuple

from collections import namedtuple

Cat = namedtuple('Cat', ['age', 'weight'])

cat = Cat(age=5, weight=13)
print('My cat is %d years old, Her weight is %d pounds.' % (cat.age, cat.weight))

>> My cat is 5 years old, Her weight is 13 pounds.

cat2 = Cat()  # error

>> TypeError: __new__() missing 2 required positional arguments: 'age' and 'weight'

Use property

tabby = Cat('Tabby')
tabby.kittens = 0

print('Tabby has %d newborn kittens so far.' % tabby.kittens)

>> Tabby has 0 newborn kittens so far.

Cat.give_birth = property(lambda self: self.kittens + 1)
tabby.kittens = tabby.give_birth
print('Tabby gives birth to %d kitten(s).' % tabby.kittens)

>> Tabby gives birth to 1 kitten(s).
A property is actually a simple implementation of a thing called a descriptor. It's an object that provides custom handling for a given attribute, on a given class. Kinda like a way to factor a huge if tree out of __getattribute__.
When I ask for tabby.give_birth in the example above, Python sees that the give_birth defined on the class implements the descriptor protocol—which just means it's an object with a __get__, __set__, or __delete__ method. The descriptor claims responsibility for handling that attribute, so Python calls Cat.give_birth.__get__(tabby, Cat), and the return value is passed back to you as the value of the attribute. In the case of property, each of these methods just calls the fget, fset, or fdel you passed to the property constructor.

For more information about descriptors


Concerns of binding a new method

According to Aaron Hall's answer in Stackoverflow. Here's a couple of reasons:
  •  You'll add a bound object to every instance you do this to. If you do this a lot, you'll probably waste a lot of memory. Bound methods are typically only created for the short duration of their call, and they then cease to exist when automatically garbage collected. If you do this manually, you'll have a name binding referencing the bound method - which will prevent its garbage collection on usage. 
  • Object instances of a given type generally have its methods on all objects of that type. If you add methods elsewhere, some instances will have those methods and others will not. Programmers will not expect this, and you risk violating the rule of least surprise. 
  • Since there are other really good reasons not to do this, you'll additionally give yourself a poor reputation if you do it.
Thus, he suggests that you not do this unless you have a really good reason. It is far better to define the correct method in the class definition or less preferably to monkey-patch the class directly.

References




Share:

5 則留言: