Same name functions in same class – is there an elegant way to determine which to call?

Question:

I am trying to do product version control in Python scripts for a specific reason, but I couldn’t figure out how to do it in an elegant way.

Currently, I am doing something like the below. However, the scripts are hard to maintain when version content is changed.

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    def function():
        if self.version == '1.0':
            print('for version 1.0')
        elif self.version == '2.0':
            print('for version 2.0')
        else:
            print(f'function not support {self.version}')

Therefore, I want to do something like the below to separate the functions with the same name.

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    def function():
        print('for version 1.0')

    def function():
        print('for version 2.0')

I was thinking about to use decorator to achieve this:

class Product(object):

    def __init__(client):
        self.version = client.version  # Get client version from another module

    @version(1.0)
    def function():
        print('for version 1.0')

    @version(2.0)
    def function():
        print('for version 2.0')

However, I failed to figure out how… it seems like a decorator cannot do this kind operation or I just don’t understand how to.

Is there a elegant way to do this?

Asked By: Timmy Lin

||

Answers:

Could you put your Product class into two modules, v1 and v2, then import them conditionally?

For example:

Productv1.py

class Product(object):
    def function():
        print('for version 1.0')

Productv2.py

class Product(object):
    def function():
        print('for version 2.0')

Then in your main file:

main.py

if client.version == '1.0':
    from Productv1 import Product
elif client.version == '2.0':
    from Productv2 import Product
else:
    print(f'function not support {self.version}')

p = Product
p.function()
Answered By: Loocid

You can use decorators for this

def v_decorate(func):
   def func_wrapper(name):
       return func(name)
   return func_wrapper

And

@v_decorate
def get_version(name):
   return "for version {0} ".format(name)

You can call it

get_version(1.0)

   'for version 1.0 '

get_version(2.0)
'for version 2.0 '
Answered By: Richard Rublev

Inheritance is probably the best way to do this, but since you asked specifically about decorators, I wanted to show you could do this using decorators.

You’ll need to use a dictionary to store your functions by version, and then look up which version to use at runtime. Here’s an example.

version_store = {}

def version(v):
    def dec(f):
        name = f.__qualname__
        version_store[(name, v)] = f
        def method(self, *args, **kwargs):
            f = version_store[(name, self.version)]
            return f(self, *args, **kwargs)
        return method
    return dec

class Product(object):
    def __init__(self, version):
        self.version = version

    @version("1.0")
    def function(self):
        print("1.0")

    @version("2.0")
    def function(self):
        print("2.0")

Product("1.0").function()
Product("2.0").function()
Answered By: Bi Rico

As another option, you could go for some factory to create your class.

Create your versioned functions (note the self parameter). This can be done in a different module. Also, add some collection to fetch the function based on the version number.

def func_10(self):
    print('for version 1.0')

def func_20(self):
    print('for version 2.0')

funcs = {"1.0": func_10,
         "2.0": func_20}

Add a base class that contains the static parts of your implementation and a utility class to create your instances in:

class Product:
    def __init__(self, version):
        self.version = version

class ProductFactory(type):
    @classmethod
    def get_product_class(mcs, version):
        # this will return an instance right away, due to the (version) in the end
        return type.__new__(mcs, "Product_{}".format(version.replace(".","")), (Product,), {"function": funcs.get(version)})(version)
        # if you want to return a class object to instantiate in your code omit the (version) in the end

Using this:

p1 = ProductFactory.get_product_class("1.0")
p2 = ProductFactory.get_product_class("2.0")
print(p1.__class__.__name__) # Product_10
p1.function() # for version 1.0
print(p1.function) # <bound method func_10 of <__main__.Product_10 object at 0x0000000002A157F0>>
print(p2.__class__.__name__) # Product_20
p2.function() # for version 2.0 
print(p2.function) # <bound method func_20 of <__main__.Product_20 object at 0x0000000002A15860>>
Answered By: shmee

In general, don’t. Method overloading is discouraged in Python. If you have to differentiate on class level, read Loocid’s answer. I won’t repeat his excellent post.

If you want in on a method level because the difference is small enough for that, try something like this:

class Product:

    def method(self):
        if self.version == "1.0":
            return self._methodv1()
        elif self.version == "2.0":
            return self._methodv2()
        else:
            raise ValueError("No appropriate method for version {}".format(self.version))

    def _methodv1(self):
        # implementation

    def _methodv2(self):
        # implementation

Note here:

  1. Use of methods starting with an underscore, to indicate those are
    the implementation.
  2. Keeping the methods nice and tidy without
    contamination between the versions
  3. Raising an error for unexpected versions (crash early and hard).
  4. In my not so humble opinion, this will be a lot clearer for other people reading your post than using decorators.

Or:

class Product:

    def method_old(self):
        # transform arguments to v2 method:
        return self.method()

    def method(self):
        # implementation
  1. In case you want to totally deprecate previous usage, and want to drop version 1.0 support in the future.
  2. Be sure to give deprecation warnings, so to not surprise users of a library with sudden changes.
  3. It is arguably the better solution if nobody else uses your code.

I get the vibe my first method would be more suitable to your problem at hand, but I wanted to include the second for posterity. If you edit your code 10 years from now, that one will make you happier. If you edit code using the current code tomorrow, the first method will make you happier.

Answered By: Gloweye

I’m not a Python developer, but I can’t help but wonder why you don’t just do something like this:

class Product(object):
    def __init__(self, version):
        self.version = version
    def function(self):
        print('for version ' + self.version)
Answered By: Jack

Or you can do, dict.get to call a function and do print in lambda if nothing is right:

def func_1(self):
        print('for version 1.0')

    def func_2(self):
        print('for version 2.0')
    def function(self):
       funcs = {"1.0": self.func_1,
                "2.0": self.func_2}
       funcs.get(self.version,lambda: print(f'function not support {self.version}'))()

Example:

class Product(object):

    def __init__(self,version):
        self.version = version

    def func_1(self):
        print('for version 1.0')

    def func_2(self):
        print('for version 2.0')
    def function(self):
       funcs = {"1.0": self.func_1,
                "2.0": self.func_2}
       funcs.get(self.version,lambda: print(f'function not support {self.version}'))()
Product('1.0').function()
Product('2.0').function()
Product('3.0').function()

Output:

for version 1.0
for version 2.0
function not support 3.0
Answered By: U12-Forward