What would be the python equivalent of type-wise o

2019-09-06 16:56发布

问题:

To my knowledge, there are two types of overloading, one based on number of arguments, and one based on argument types

While overloading based on number of arguments has been covered here, I can't seem to find guidelines on function overloading by argument type.

So since type checking with type() seems to be generally frowned upon, how do I do this in a pythonic way?

This seems less elegant than I would expect...

def overloaded_func(arg):
    try:
        do_list_action(arg)
    except:
        try:
            do_dict_action(arg)
        except:
            pass

回答1:

Type checking with type() (or, better, isinstance()) isn't frowned upon in general in Python. What's frowned upon is using type as a proxy for behavior when you can get away with only checking the behavior. In other words, when you need an object to have some certain functionality, whereas in some other languages you would have to explicitly check whether its type supports that functionality, in Python you just assume the object does what you need it to do, and trust that an exception will be raised if that's not the case. But if you're choosing between different types of functionality, any of which will do the job for you, that's a different story.

For example, suppose you have some code that can use either lists or dicts to implement integer-indexed arrays.

class Array:
    def __init__(self, keys_and_values):
        self.store = ... # either a list or a dict

Perhaps it uses a dict if only a few, very large indices have values assigned, and a list otherwise. If you want to access the element at an index, if there is one, then you just write self.store[index].

    def __getitem__(self, index):
        return self.store[index]

You don't bother to check whether it's a list or a dict first, because the behavior you want - the ability to be indexed by an integer - exists either way.

But if you want to set the element at an index, if it's a list, you need to extend it to the proper length first. Now, proper duck typing would probably suggest you do this:

    def __setitem__(self, index, value):
        if index >= len(self.store):
            try:
                self.store.extend([None] * (index - len(self.store) + 1))
            except AttributeError:
                pass
        self.store[index] = value

But I think most Python programmers would say isinstance() is better in this case. (No, really. It's okay.)

    def __setitem__(self, index, value):
        if isinstance(self.store, list) and index >= len(self.store):
            self.store.extend([None] * (index - len(self.store) + 1))
        self.store[index] = value

I would generally recommend this route when you've only got a few types to test.

If you have many more types to test, it's more practical to use a dispatcher pattern, which is a functional approach. You build a mapping of types to functions that handle that type, and choose which one to call based on the type of the object you get. In this example, that would play out like this:

    def __setitem__dict(self, index, value):
        self.store[index] = value
    def __setitem__list(self, index, value):
        if index >= len(self.store):
            self.store.extend([None] * (index - len(self.store) + 1))
        self.store[index] = value
    __setitem__dispatch = {list: __setitem__list, dict: __setitem__dict}
    def __setitem__(self, index, value):
        self.__setitem__dispatch[type(self.store)](index, value)

It's pretty silly to do this in this simple example, but in more complicated scenarios it can come in very handy. The pattern in general is

dispatch = {list: handle_list, dict: handle_dict, ...}
def function(arg):
    return dispatch[type(arg)](arg)

It even lets you dynamically add handlers for new types later on. This is basically what functools.singledispatch does (as another answer mentioned). That way just looks complicated because it hides the dispatch dictionary as an attribute of the original function itself.

It's impossible to say in general whether to use duck typing, type checking, dispatching, or something else, because it's somewhat subjective and depends on the details of your situation: how different is the code you need to handle different types? How many types are you dealing with? Do you need to be able to handle new types easily? And so on. You haven't given enough information in the question to allow anyone else to tell you which way seems best, but they all have their uses.



回答2:

You could use functools.singledispatch. It will dispatch based on the type of first parameter and uses the default implementation if type doesn't match with any of the registered functions:

from functools import singledispatch

@singledispatch
def overloaded_func(arg):
    print('Default impl', arg)

@overloaded_func.register(list)
def do_list_action(lst):
    print('List action', lst)

@overloaded_func.register(dict)
def do_list_action(dct):
    print('Dict action', dct)

overloaded_func(['foobar'])
overloaded_func({'foo':'bar'})
overloaded_func('foobar')

Output:

List action ['foobar']
Dict action {'foo': 'bar'}
Default impl foobar


回答3:

Use isinstance:

def overloaded_func(arg):
    if isinstance(arg, list):
        do_list_action(arg)
    elif isinstance(arg, dict):
        do_dict_action(arg)
    else:
        do_default_action(arg)

Alternatively, you might consider checking other ways such as for the presence of __getitem__ or __iter__ etc. This depends on the details of why you're overloading, which you haven't shared with us.