Should a plugin adding new instance-methods monkey-patch or subclass/mixin and replace the parent?

元气小坏坏 提交于 2019-12-12 14:21:15

问题


As a simple example take a class Polynomial

class Polynomial(object):
     def __init__(self, coefficients):
         self.coefficients = coefficients

for polynomials of the form p(x) = a_0 + a_1*x + a_2*x^2 + ... + a_n*x^n where the list coefficients = (a_0, a_1, ..., a_n) stores those coefficients.

One plugin-module horner could then provide a function horner.evaluate_polynomial(p, x) to evaluate a Polynomial instance p at value x, i.e. return the value of p(x). But instead of calling the function that way, a call to p.evaluate(x) (or more intuitively p(x) via __call__) would be better. But how should it be done?

a) Monkey-patching, i.e.

Polynomial.evaluate = horner.evaluate_polynomial
# or Polynomial.__call__ = horner.evaluate_polynomial

b) Subclassing and replacing the class, i.e.

orgPolynomial = Polynomial
class EvaluatablePolynomial(Polynomial):
    def evaluate(self, x):
        return horner.evaluate_polynomial(self, x)
Polynomial = EvaluatablePolynomial

c) Mixin + Replacing, i.e.

orgPolynomial = Polynomial
class Evaluatable(object):
    def evaluate(self, x):
        return horner.evaluate_polynomial(self, x)
class EvaluatablePolynomial(Polynomial, Evaluatable):
    pass
Polynomial = EvaluatablePolynomial

Sure enough, monkey-patching is the shortest one (especially since I didn't include any check à la hasattr(Polynomial, 'evaluate'), but similarly a subclass should call super() then...), but is it the most Pythonic? Or are there other better alternatives?

Especially considering the possibility for multiple plugins providing the same function, e.g. zeros either using numpy or a self-made bisection, where of course only one implementing plugin should be used, which choice might be less error-prone?


回答1:


The one and probably most important property of monkey-patching the function directly to the original class instead of replacing it is that references to / instances of the original class before the plugin was loaded will now also have the new attribute. In the given example, this is most likely desired behaviour and should therefore be used.

There may however be other situations where a monkey-patch modifies the behaviour of an existing method in a way that is not compatible with its original implementation and previous instances of the modified class should use the original implementation. Granted, this is not only rare but also bad design, but you should keep this possibility in mind. For some convoluted reasons code might even rely on the absence of a monkey-patch-added method, though it seems hard to come up with a non-artificial example here.

In summary, with few exceptions monkey-patching the methods into the original class (preferably with a hasattr(...) check before patching) should be the preferred way.


edit My current go: create a subclass (for simpler code completion and patching) and then use the following patch(patching_class, unpatched_class) method:

import logging
from types import FunctionType, MethodType


logger = logging.getLogger(__name__)
applied_patches = []


class PatchingError(Exception):
    pass


def patch(subcls, cls):
    if not subcls in applied_patches:
        logger.info("Monkeypatching %s methods into %s", subcls, cls)
        for methodname in subcls.__dict__:
            if methodname.startswith('_'):
                logger.debug('Skipping methodname %s', methodname)
                continue
            # TODO treat modified init
            elif hasattr(cls, methodname):
                raise PatchingError(
                    "%s alrady has methodname %s, cannot overwrite!",
                    cls, methodname)
            else:
                method = getattr(subcls, methodname)
                logger.debug("Adding %s %s", type(method), methodname)
                method = get_raw_method(methodname, method)
                setattr(cls, methodname, method)
        applied_patches.append(subcls)


def get_raw_method(methodname, method):
    # The following wouldn't be necessary in Python3...
    # http://stackoverflow.com/q/18701102/321973
    if type(method) == FunctionType:
        logger.debug("Making %s static", methodname)
        method = staticmethod(method)
    else:
        assert type(method) == MethodType
        logger.debug("Un-bounding %s", methodname)
        method = method.__func__
    return method

The open question is whether the respective subclass' module should directly call patch on import or that should be done manually. I'm also considering writing a decorator or metaclass for such a patching subclass...



来源:https://stackoverflow.com/questions/18466214/should-a-plugin-adding-new-instance-methods-monkey-patch-or-subclass-mixin-and-r

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!