0

New to Python here - coming from Perl, so have mercy If I'm asking perl-ish things.

I am trying to extend the functionality of an existing Python class (svgpathtools) via an additional module.

In other words, I would like to have in one file somewhere (AKA the module providing the extension):

def foo(path_a, path_b):
    d=0
    for t in range(0, 101, 1):
        d = d + complex_distance(path_a.point(t / 100), path_b.point(t / 100))
    return d / 100

setattr(svgpathtools.Path, 'foo', foo)

and then in the file using the extension

be able to do something like this:

import svgpathtools.foo

some_path.foo(some_other_path)

I'd like to know:

  • how to name/where to save the file extending the svpathtools module (i.e.: what's the mapping between module name and file name)
  • what should it look like in order to monkey-patch the svgtools methods

My biggest problem is finding out what this kind of stuff is called - so I can do my own googling. Full explanations are appreciated, but pointers for further research are enough for me to get going.

simone
  • 4,667
  • 4
  • 25
  • 47
  • Why not use conventional inheritance? – quamrana Sep 17 '21 at 13:05
  • 1
    Don't worry, it takes time to detox from Perl. :) – chepner Sep 17 '21 at 13:06
  • I'm not sure if patching stuff outside tests is appropriate. IMHO It's better to inherit from base class, or simply declare this function as standalone and call it directly, w/o referring to svgpathtools. setattr on import is not... explicit to the end user, if you still need to patch, please, wrap it in some sort of `install` function – Adam Bright Sep 17 '21 at 13:08
  • 1
    More seriously, Perl and Python have *very* different ideas about how classes are implemented. In Perl, a class is just a package. In Python, a class is quite distinct from a module (and a package is just a module that contains other modules). `svgpathtools` itself is not a class at all; it's a module. `svgtools.Path` is an attribute of that module whose value is a class. While you *can* add a new method to an existing class, it's more likely you want to use inheritance or composition to define a *new* class to use instead. – chepner Sep 17 '21 at 13:12
  • As far as I can recall, Perl doesn't even support inheritance in the language; there's a separate library for defining an inheritance relationship between two packages. Perl 6 / Raku might have changed that, but I never had any need (or much desire) to learn that language. – chepner Sep 17 '21 at 13:15
  • @chepner - I'm actually enjoying switching back and forth, like being able to appreciate English and Italian - different, both good. No detox planned, or needed. I do miss things such as roles (https://metacpan.org/pod/Role::Tiny) and method modifiers (https://metacpan.org/pod/Class::Method::Modifiers) which make extending classes much less complex – simone Sep 17 '21 at 13:18
  • @chepner - inheritance is handled via ```base``` https://perldoc.perl.org/base – simone Sep 17 '21 at 13:20
  • @simone Thanks; somehow, I overlooked the existence of that even when I *was* trying to write OO Perl. – chepner Sep 17 '21 at 13:23

1 Answers1

1

In Python, a method is merely an attribute of a class that happens to be a function having the object (by convention called self) as its first argument. And a class is just an object. So patching a class to add new methods or replace existing (it is called monkey patching) is trivial. But inheritance is a much cleaner way to obtain the same result: the new behaviour is encapsulated in a distinct class having a distinct name, which can save later maintenance headaches...

Here is a trivial example with a base class that can compute the double of its original value, and an extension that can compute the quad (* 4):

class A:
    def __init__(self, a):
        self._val = a
    def double(self):
        return 2 * self._val

Example usage:

>>> a = A(2)
>>> a.double()
4

Let us derive it:

class B(A):
    def quad(self):
        return 4 * self._val

which can be used that way:

>>> b = B(2)
>>> b.double()
4
>>> b.quad()
8

Here is a monkey patched version:

>>> A.quad = lambda self: 4 * self._val

which also gives

>>> a = A(2)
>>> a.quad()
8

But that way has globally modified the A class which no longer respects its initial (and hopefully documented API). Unless it is commented in red flashing font a future maintener will certainly blame you for that...

That being said, monkey patching can make sense to fix a bug in a module that you cannot (or do not want to) patch at source level...

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Thanks - I'm missing a detail though: where (i.e.: in what file) should the mokey patching happen, for it to be re-usable (via ```import```) – simone Sep 17 '21 at 13:47
  • If you want it to be importable in a standard way, you **really** should considere inheritance. You can only monkey patch a class after the module in which it is declared has been loaded. And as the patch is global, you must be cautious for side effect in other modules using the same class... – Serge Ballesta Sep 17 '21 at 14:01