views:

277

answers:

5

Is it pythonic to mimic method overloading as found in statically typed languages? By that I mean writing a function that checks the types of its arguments and behaves differently based on those types.

Here is an example:

class EmployeeCollection(object):
    def find(value):
        if isinstance(value, str):
            #find employee by name and return
        elif isinstance(value, int):
            #find employee by employee number and return
        else:
            raise TypeError()
+11  A: 

Not really, since you lose the ability to use types that are not-quite-that-but-close-enough. Create two separate methods (find_by_name() and find_by_number()) instead.

Ignacio Vazquez-Abrams
-1, Now you're just statically encoding types into the names of your methods -- not very dynamic! And what if you have 5 different parameters that could be either a string or number? Do you create 32 different methods?
Gabe
If you have 5 arguments that could be either a string or a number then you have larger architectural problems (and possibly a deeper underlying problem that no software development forum can help with...).
Ignacio Vazquez-Abrams
+11  A: 

Not very Pythonic, except perhaps, in 2.6 or better, if all the checks rely on the new abstract base classes, which are intended in part exactly to facilitate such use. If you ever find yourself typechecking for concrete classes, then you know you're making your code fragile and curtailing its use.

So, for example, checking if you have an instance of numbers.Integral is not too bad -- that new ABC exists in good part exactly to ease such checking. Checking if you have an instance of int is a disaster, ruling out long, gmpy.mpz, and a bazillion other kinds of integer-like numbers, to absolutely no good purpose: never check for concrete classes!

Strings are a difficult case, but the basestring abstract class (not one of the new kind of ABCs) is a possibility. A bit too restrictive, perhaps, but if you're using other ABCs around it, it might kinda, sorta work, if you really have to. Most definitely not str -- why ever rule out unicode?!

Alex Martelli
+1  A: 

I would say yes, it is 'Pythonic' And there are examples to back this up (which the other posters have not given). To answer this properly there should be examples!

From python core:

  • string.startswith() It accepts either a string or a tuple (of strings).
  • string.endswith()

In pymongo:

  • find_one() accepts either a dict object to lookup, or it will use another other object as the id.

Sorry I don't know more, but I think there are MANY examples of methods that behave differently according to the parameters given. That is part of the beauty of not enforcing types.

Amala
-1 typechecking is __never__ pythonic. Check interfaces but not type. the extent to which typechecking occurs in the standard library is to be taken as a deficiency and not as an example.
aaronasterling
@aaronasterling: I think that 'never' is a bit strong. How about in `__init__` methods? For example you can construct a `bytearray` from another `bytearray`, an integer, an iterable of integers, a `memory_view`, a string or (Python 3 only) a `bytes` object. All that from a relatively recently added built-in type!
Scott Griffiths
@Scott: The fact that you need to use type checking there does not make type checking any more Pythonic.
Ignacio Vazquez-Abrams
@Ignacio Vazquez-Abrams: I agree that type checking isn't generally Pythonic, but think that saying never is going too far. If you have a similar situation to the `bytearray` initialisation problem then type checking seems a reasonable design option. Either that or a recent addition to core Python isn't Pythonic, which would be a little strange...
Scott Griffiths
+1  A: 

A more pythonic way of getting to this kind of functionality is to just try and use it in the preferred way (whatever that might mean) and if the argument doesn't support that, try an alternative.

Here'd two ways to do that:

class EmployeeCollection(object):
    def find(value):
        try:
            #find employee by name and return
        catch:
            try:
                #find employee by employee number and return
            catch:
                raise TypeError()

but thats kind of yucky. here's how I usually do this:

class EmployeeCollection(object):
    def find(value):
        if hasattr(value, 'join'):
            #find employee by name and return
        elif hasattr(value, '__div__'):
            #find employee by employee number and return
        else:
            raise TypeError()

In reality, the actual attribute I'd check for depends on what happens in those comments, I'll prefer to check for an attribute that I actually use.

TokenMacGuy
I would actually say that your first approach is better.
detly
Pass the argument to the constructor of the type you care about and catch any exception from that.
Ignacio Vazquez-Abrams
The second example is the more 'pythonic' because it is checking the interface, not the type. So I would say that is your correct answer. I think some dogma about 'never type checking' is a bit silly.
Amala
@Ignacio Vazquez-Abrams: I don't think I understand your suggestion. Constructing a string from an int will never fail and constructing an int from a string isn't guaranteed to throw an exception. Admittedly people don't often have names that are numbers, but using that fact would be programming by coincidence :)
Scott Griffiths
The problem with the first approach is surely that finding an employee by name might not throw an exception at all if you use an int (I note you didn't specify what exception you expected). Maybe it will just find that the integer isn't in the employee name container - no exception thrown. Sure, you can code to ensure an exception, but that might not be the most natural code, and would be fragile.
Scott Griffiths
I admit my first example makes the assumption that if you use the wrong type in the commented part it makes a stink. If that doesn't happen, then you gotta code around that.
TokenMacGuy
+2  A: 

No, type checking here isn't Pythonic. Another option if you don't fancy multiple methods is to stick with one method but use multiple parameters:

def find(name=None, employee_number=None):
    if sum(x != None for x in (name, employee_number)) != 1:
        #raise exception - exactly one value should be passed in
    if name is not None:
        #find employee by name and return
    if employee_number is not None:
        #find employee by employee number and return

When used the intent is as obvious as with multiple methods:

employee1 = x.find(name="John Smith")
employee2 = x.find(employee_number=90210)
Scott Griffiths