3

I want to keep the end-user (often myself) from being presented with inapplicable methods when coding directly in an interpreter, but I also don't want the user to have to inspect the incoming data and then determine which class to instantiate. This is probably the XY problem, but here's a working example, in which I set the class dynamically:

class _BaseNumber(object):
    def __init__(self, x):
        self.x = x

class _Even(_BaseNumber):
    def cool_method_just_for_evens(self):
        pass

class Number(object):
    def __new__(self, x):
        if x % 2 == 0:
            return _Even(x)
        else:
            return _BaseNumber(x)

print(type(Number(3)))
print(type(Number(4)))

This works, with a result of

<class '__main__._BaseNumber'>
<class '__main__._Even'>

Is there a cleaner/more efficient way to conditionally enable certain methods? Or is this sort of conditional functionality discouraged for some reason that will come back to bite me later on?

I'm in Python 2.7, but apparently that's not a tag here.

  • Why not just have an EvenNumber class and an OddNumber class? – Bryan Oakley Aug 27 '16 at 3:31
  • @BryanOakley, mostly because I don't want to have to figure out each time which class to use. Plus, this was just a generic example; the real data would have much more complex/longer if statements to determine which type (of 5) it should be. I'd have to go through those if statements every time. An alternative would be to have a function that returns the proper class, but that seems effectively identical to what I have, but requires a potentially less intuitive workflow for the end-user. – Tom Aug 27 '16 at 3:38
2

What you are seeing here, Tom, is the Strategy design pattern, which may or may not be a variation of Facade or Decorator, and uses the Adapter design pattern.

The strategy method itself is a Factory method. A simple factory method produces an object with parameters you supply, but you have provided a conditional returning a specific subtype (or the supertype itself), you provided a strategy.

When you are using the strategy pattern you usually provide a common (adapter) interface for all underlying implementations and use that one. By doing so you are abstracting the inner parts which you do not care about.

Consider your example in a compiled language C++:

#include <exception>

class Number
{
protected:
    int _number;

public:
    explicit Number(const int number)
        : _number(number)
    {       
    }

    int getNumber() const
    {
        return _number;
    }
};

class EvenNumber : public Number
{
public:
    explicit EvenNumber(const int number)
        : Number(number)
    {
    }

    bool isNumberReallyEven() const
    {
        return _number % 2 == 0;
    }
};

namespace NumberStrategy
{
    Number createNumber(const int number)
    {
        switch (number % 2)
        {
        case 0:
            return EvenNumber(number);
        case 1:
            return Number(number);
        }
        throw std::exception("Should never happen.");
    }
}

int main(int agrc, char* argv[])
{
    auto number = NumberStrategy::createNumber(2);
    auto isItEven = number.isNumberReallyEven();

    return EXIT_SUCCESS;
}

By compiling this short snippert, you will get a compilation error on the line 52:

'isNumberReallyEven': is not a member of 'Number'

But why, you may ask? By passing the number 2 to the NumberStrategy::createNumber function, I should have the EvenNumber instance which has the isNumberReallyEven method.

You actually do have the instance but compiler does not know that, because the return type of the NumberStrategy::createNumber function is Number. If you want to access the underlying methods, you need to tell the compiler which type you really have by casting it.

C++11 has a very nice concept for this called reinterpret_cast. It is completely unsafe, so when you use it you must be completely sure you know what you are doing. Obviously, in this example you know that passing an even number to the NumberStrategy::createNumber function will return the EvenNumber type.

You change the program to this:

int main(int agrc, char* argv[])
{
    auto number = NumberStrategy::createNumber(2);

    // I know I recieved a number but I am 100% sure it has to be even,
    // thus I am casting it manually without any worry.
    auto evenNumber = reinterpret_cast<EvenNumber&>(number);
    auto isItEven = evenNumber.isNumberReallyEven();

    return EXIT_SUCCESS;
}

And it compiles.

You may ask why I decided to show you a C++ example instead of a Python one? I am not a Python programmer but I work with PHP (a dynamic language as well). In dynamic languages the program would probably still work even without the casting, due to the nature of the language - it will simply inspect the type during the runtime and see whether the method is available or not and in the case of passing a 2 to the method it would be.

By using for example C++, the compiler will not allow you to get past certain restriction you wouldn't have to deal with in an interpreted language and that is good. When another programmer reads the code he will instantly know that you are doing that on purpose.

The same applies to Python. If you are calling a method which you know is available in a subtype but the contract of the method states a supertype is returned instead, you should probably comment that you are exactly sure the method will be available when you call it.


Then there's the other thing: wrong abstraction. If you encouter a situation where you are forced to typecast a variable to access its methods, should the supertype<->subtype relationship even exist?

In the C++ snippet this problem is easily fixed by placing the isNumberReallyEven method in the Number class. If you cannot do something like that, your design may be wrong.

  • Thanks for the thorough answer, David. It sent me down a path of reading about design patterns--something that a self-taught hack like me has had no real exposure to. From my reading, it sounds like what I'm looking for is actually a Factory Pattern, as described here. I may have worded my question poorly, because this seems spot-on. With that in mind, from your perspective, does the factory pattern seem fit my description/code? – Tom Aug 27 '16 at 22:21
  • That said, I'm also not clear on how the strategy pattern is different from just regular method overriding. – Tom Aug 28 '16 at 1:02
  • @Tom The patterns are interchangeable in a sort of way. If you want to use strategy pattern, you will need adapter. There are various ways of implementing the strategy: it may only be chosing the correct implementation from a private set of attributes of a class, lazy loading them or creating them on each use. Depends on the use case. How strategy differs from a method overriding? Method overriding defines what a method should do, strategy patterns decides when. Common usage of strategy pattern is sorting, where for a small set a different sorting algorithm is used than for a larger one. – Andy Aug 28 '16 at 8:30
0

This is polymorphism. You're just doing it by delegation and deciding what to delegate to later than is necessary. When x is set the delegation should be set. Really no need to go ask later. In addition to being clunky it also invites branch prediction problems.

Now sure there are Object Oriented ways to do polymorphism dynamically, the state pattern or strategy pattern to name a few. But this is python. You don't have to do this the OO way here.

In python there is this nifty trick called monkey patching. Since functions are first class in python you can treat them like data. So rather than pass the value of X around you can pass around the function you want called.

Either approach avoids the branch prediction problems. Choosing which one to do is more about readability and maintainability. OO still looks good if you want a set of functions changing together. But monkey patching is better if you want to swap em out independently.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.