_yet another easy kata!_

_Bored of usual python katas? me too;_



## Overview

     As you have guessed from the title of the kata you are going to implement a class that supports ***function overloading***. You might be thinking python doesn't support that thing... Of course python doesn't support that! So you have to implement that missing functionality.

     To achieve that You have to implement the `Meta` class which will be the metaclass of `Overload` class (see sample tests) and class that use `Meta` should work as usual at other things.



## The Goal

```python
    
class Overload(metaclass=Meta):
    
    CLS_VAR = 42
    
    def __init__(self):
        self.a = 1
        self.no = 'This is "No parameter" function.'
        self.single = 'This is "Single parameter" function'
        self.two = 'This is "Two parameter" function'
        self.three = 'This is "Three parameter" function'

    def foo(self):
        return self.no

    def foo(self, x):
        return self.single + ':' + str(x)

    def foo(self, x, y):
        return self.two + ':' + str(x) + ',' + str(y)

    def foo(self, x, y, z):
        return self.three + ':' + str(x) + ',' + str(y) + ',' + str(z)
    
    def extra(self):
        return 'This is extra method.'
            
  

  obj = Overload()

  Overload.foo=lambda self,a,b,c,d: 'from outside!'

  obj.foo()            # 'This is "No parameter" function.'
  obj.foo(1, 2)        # 'This is "Two parameter" function:1,2'
  obj.foo(1, 2, 3)     # 'This is "Three parameter" function:1,2,3'
  obj.foo(1, 2, 3, 4)  # 'from outside!'
```



## Specifications
   
   * The Overload base class will always be the same as above. It will be regenerated different times in the tests, for testing purpose.
   * All the other methods will be added and tested **after instanciation** the class like shown in the example above (***Focus on this point; you will need this***).
   * Only instance methods will be tested, no static or class level methods.
   * There is no testing for either `*varargs` or `**kwargs`.
   * Aside from overloading, the class should behave just like usual. Talking here about variable assginment or reassignment, at class or instance level, reassigning a method to a var or the opposite, ...
   * If there is neither a method (overloaded or not) which satisfies the expected number of arguments nor a property or class level property that cn be found when calling for an attribute, raise an exception of type `AttributeError` ("just like usual", as said above...)
   * Last but not least, different classes must not share overloaded method. Hence:
   
```python
Cls1 = generate_Overload()
obj = Cls1()
Cls1.foo=lambda self,a,b,c,d: 'from Cls1'

Cls2 = generate_Overload()
obj2 = Cls2()
Cls2.foo=lambda self,a,b,c,d: 'from Cls2'

obj.foo(1,2,3,4)    # -> 'from Cls1'
obj2.foo(1,2,3,4)   # -> 'from Cls2'

Cls2.foo=lambda self: 'updated'

obj.foo()           # -> 'This is "No parameter" function.'
obj2.foo()          # -> 'updated'
```
   
## Notes

   * If there are any flaw in testing then report me.
   * If you need some hints then ping me in discourse.

_Enjoy!_

_Thanks to B4B for his contribution._

_I can assure you that this can be done without using any library/modules._

Difficulty: interview

Starter code:

def setter(prep,k,v,supSetter):
	

Solutions:
from collections import defaultdict


def setter(prep,k,v,supSetter):
    if callable(v): 
        def wrap(*args):
            f = prep.d[k][len(args)]
            if isinstance(f,int): raise AttributeError()
            return f(*args)
        prep.d[k][v.__code__.co_argcount] = v
        v = wrap
    supSetter(k,v)
        
        
class Prep(dict):
    def __init__(self):         self.d = defaultdict(lambda: defaultdict(int))
    def __setitem__(self,k,v):  setter(self, k, v, super().__setitem__)


class Meta(type):
    @classmethod
    def __prepare__(cls,*args, **kwds): return Prep()
    
    def __new__(metacls, name, bases, prep, **kwargs):
        prep['_Meta__DCT'] = prep
        return super().__new__(metacls, name, bases, prep, **kwargs)
    
    def __setattr__(self,k,v): setter(self.__DCT, k, v, super().__setattr__)

from collections import defaultdict
def setter(prep, k, v, sup):
    if callable(v):
        def wrap(*args):
            a = prep.d[k][len(args)]
            if isinstance(a, int): raise AttributeError()
            return a(*args)
        prep.d[k][v.__code__.co_argcount] = v
        v = wrap
    sup(k, v)
    
class Prep(dict):
    def __init__(self): self.d = defaultdict(lambda: defaultdict(int))
    def __setitem__(self,k,v): setter(self, k, v, super().__setitem__)

class Meta(type):
    @classmethod
    def __prepare__(cls, *args, **kwargs): return Prep()
    
    def __new__(metacls, name, bases, prep, **kwargs):
        prep['_Meta__DCT'] = prep
        return super().__new__(metacls, name, bases, prep, **kwargs)
    
    def __setattr__(self, k, v): setter(self.__DCT, k, v, super().__setattr__)