3

Consider the following example:

class Company():

    def hireEmployee():

    def fireEmployee():

    def promoteEmployee():

    etc...

class EngineeringFirm(Company):
    pass

class PaintingFirm(Company):
    pass

Suppose the Company class has a lot more methods. What if I want to rename these methods from the superclass so I can get the following:

class EngineeringFirm(Company):

    def hireEngineer():
    ...

class PaintingFirm(Company):

    def hirePainter():
    ...

...and so on. While using 'Employee' in this scenario really wouldn't hurt a bit, this is really just to illustrate the idea. How would I go about it?

My idea was to use a classFactory function that would take the type of employee as argument and generate a Company class, while a metaclass would handle the renaming by iterating through the attribute dictionary and replacing 'Employee' with said type.

class EngineeringFirm(companyFactory('Engineer'))
    ...

The only problem is this: What if the methods inside of Company make calls to one another by the default 'Employee' names? This is where I'm stumped. I had the idea that the metaclass involved in renaming the methods could also get the source of each function (via the inspect module) and search if a known method attribute is found within and, if so, replace that part and create a new function via exec and assigning it back to the right attribute key.

...But that really seems kinda of hacky. I am open to alternatives and although I realize there may be design-related issues with the question (I am open to suggestions on that front as well) I would be interested in finding out if this problem has a more elegant solution.

Thanks!

Edit: another solution

For the sake of argument, I'll assume for a moment that the code above is really what I'm working with; I figured I could address some of the concerns in the comments with another solution I had in mind, one I'd already considered and put away for reasons I'll explain.

If the Firm classes inherited from Company and I wished to maintain a identical interface (as one usually would in a case like this to allow dynamic calls to hire() or promote(), etc) I could implement a __getattribute__ that accepts HirePainter() (by accessing the original Employee method) while still allowing any other interface to use the HireEmployee() if necessary.

I wonder, supposing it's alright to extend my question, if this is something that would be considered bad practice if, say, I planned to do this because I thought that the code inside PaintingFirm would benefit in readability? Again, I realize this example is horrid in that readability here really does not seem to benefit in any way whatsoever, but suppose it did?

(The only reason I didn't suggest this idea in the first place is that my __getattribute__ already handles quite a bit, and adding extra noise to it didn't feel that appealing. Still, I could work it in, but this is a question I had to ask in case there were more magical (but not hacky) solutions out there..)

Eithos
  • 2,421
  • 13
  • 13
  • 4
    [EEEEEEEEEEWWWWWWWWWWWWWW](http://en.wikipedia.org/wiki/Liskov_substitution_principle) – Ignacio Vazquez-Abrams Jan 01 '15 at 09:44
  • 2
    @Eithos Well, the point is, your firms should have a similiar, if not identical, interface so that you can tell any firm object to hire someone, and let it decide whom it hires. So you can do `firm.hire()` without knowing what `firm` is exactly. – glglgl Jan 01 '15 at 10:57
  • @glglgl Interesting. It makes me feel silly because in most other situations this is probably the way I'd have handled it. Makes perfect sense. I realize the problem is complicated by the fact I haven't really posted my real code, and there's very little, if any, chance that any other class would/should have access to `firm.hire()`. In other words, while using `firm.hire` would facilitate dynamic calls to hire without knowing the firm beforehand, this dynamic handling really wouldn't be necessary. In fact, these calls are activated by UI Events that are... – Eithos Jan 01 '15 at 18:07
  • ...specific to one class only; therefore, they wouldn't be calling different kinds of 'hires', ever. I suppose this is something I should have mentioned... – Eithos Jan 01 '15 at 18:10
  • I think this is a great question. I'm trying to make a `FormsetView` generic class in Django. It should do everything that `FormView` does but with the word `formset` instead. In that case, I don't really need `FormsetView` to be a subclass of `FormView`. I just want it to be a copy with minor changes to the attribute names. – Bobort Oct 06 '17 at 13:21

3 Answers3

1

For posterity's sake, I'm posting a solution of my own that I believe is a decent alternative. I don't suggest this as the answer because the truth is I did not mention in my question that I preferred not adding extra names, or to retain the ability to call these attributes as self.hireEngineer rather than ClassDict['HireEngineer']. Given that, I can't really say any of these answers don't answer the question.

Solution:

In hindsight, the problem was a lot simpler than I made it out to be. I guess I got hooked on the metaclassery just for the sake of it. If it's not already obvious, I'm really only just learning about metaclasses and for a moment it seemed like a good opportunity to try them out. Alas.

I believe the following solution respects the spirit of Liskov's principle (thank you, Ignacio) while giving the derived class the ability to reference the derived methods in its own way. The class namespace stays the same and other objects can call upon these methods with their real names if necessary.

# superclass...

def __getattribute__(self, attr):

    # Early exit (AttributeError) if attribute not found.
    obj = object.__getattribute__(self, attr)

    # All the extra code...

def __getattr__(self, attr):

    # Ex. self.type == 'Engineer'
    # Replacing titled-cased and lower-cased 
    # versions just to be safe (ex. self.employeeNames)

    attr = (attr
        .replace(self.type, 'Employee')
        .replace(self.type.lower(), 'employee')
    )

    if attr in self.attributes:
        return self.__getattribute__(attr)
    else:
        raise AttributeError

I'll try to do a better job next time around when outlining the requirements. Thanks, guys.

Eithos
  • 2,421
  • 13
  • 13
0

You could try adding in a dictionary for each class.

class EngineeringFirm(Company):
  ClassDict = {'HireEngineer':self.HireEmployee,
               ...
               };

Whenever you want to call the function you would use

<EngineeringFirmInstanc>.ClassDict['HireEngineer'](<arguments>)

It's not particularly elegant, but it might get you close to what you are asking.

JRogerC
  • 598
  • 6
  • 17
  • Thanks! I had another solution in mind somewhat close to this, but this is better and doesn't pollute the namespace like mine would have. I'll wait juste a little before I decide on an answer, but I'm glad you pitched in. – Eithos Jan 01 '15 at 18:12
0

I tend to agree with the comments on the question: I suspect that what you're asking would add unnecessary complication to the code, making it harder to read & maintain just to implement a minor "cosmetic" feature of dubious benefit.

However, if you really want to do this, perhaps you could create methods that are synonyms of the existing methods, so you can call a method with its original name or with a "customized" name when it seems appropriate.

Here's one fairly straight-forward way to do that. I guess there's some sleek way to do it with class decorators, but I don't know how to use those. :)

#! /usr/bin/env python

''' Class synonym demo

From http://stackoverflow.com/q/27729681/4014959

Written by PM 2Ring 2015.01.01
'''

class Foo(object):
    def __init__(self, data):
        self.foo_set(data)

    def foo_set(self, data):
        self.data = data

    def foo_add(self, n):
        self.data += n
        return self.data

    def foo_mul(self, n):
        self.data *= n
        return self.data

    def foo_mul_add(self, n, m):
        self.foo_mul(n)
        return self.foo_add(m)


def make_synonyms(cls, old, new):
    class newclass(cls):
        pass

    d = cls.__dict__
    for k in d:
        if k.startswith(old):
            newname = k.replace(old, new)
            #print k, d[k], newname
            setattr(newclass, newname, d[k])
    return newclass

#--------------------------------------

Bar = make_synonyms(Foo, 'foo', 'bar')

a = Foo(5)
print a.data
print a.foo_add(10)
print a.foo_mul(4)
print a.foo_mul_add(2, 1)

print '-' * 20

a = Bar(6)
print a.data
print a.foo_add(10)
print a.foo_mul(4)
print a.foo_mul_add(2, 1)

print '-' * 20

a.bar_set(5)
print a.data
print a.bar_add(10)
print a.bar_mul(4)
print a.bar_mul_add(2, 1)

output

5
15
60
121
--------------------
6
16
64
129
--------------------
5
15
60
121
PM 2Ring
  • 54,345
  • 6
  • 82
  • 182
  • Yeah.. This was probably the first thing that occured to me. The only issue is that the namespace gets polluted with extra methods, which in my situation probably wouldn't matter; however, I've tried hard to avoid going this way for that very reason. – Eithos Jan 01 '15 at 18:40
  • Fair enough, but IMHO that's not a big deal since it's only affecting the class's namespace. The new names shouldn't clash with the old names, so I wouldn't class it as namespace pollution myself, but YMMV. No extra methods are being created, it's just that (some of) the existing methods get an extra name. – PM 2Ring Jan 02 '15 at 06:23
  • Extra methods: Oops.. you're perfectly right. _That_ is certainly not what I meant to say, although it is exactly what I wrote. Good catch. – Eithos Jan 02 '15 at 06:52
  • @Eithos: No worries. I guess I ought to have mentioned in my answer that the point of the synonyms is that you don't have to worry about the "renamed" methods calling the original methods, since the new methods still exist. But I figured you'd realise that was the case. :) FWIW, I just read an excellent Q&A on metaclasses: http://stackoverflow.com/q/100003/4014959 – PM 2Ring Jan 02 '15 at 07:15
  • Yep. I read through that one a couple weeks back. I love these sorts of Q&A. Who doesn't? – Eithos Jan 02 '15 at 22:48