How does one code a function/method signature so that it will accept either a set of key-value pairs, or the same data enclosed as a dict, as part of a general-case and polymorphic solution?

Wikipedia: polymorphism is the provision of a single interface to entities of different types.
( https://en.wikipedia.org/wiki/Polymorphism_(computer_science) )


<tldr;>
At the end of a code-sprint, one of 'my' teams was presenting. Everything went well. Fatefully, the client-manager then remembered that the latest work would extend a previous sprint's work. Despite it not being a deliverable (?), he asked to be shown the two working together. The team, flushed with pride at their 'success' (foolishly) agreed, and the attempt immediately crashed. (as such live-demos are wont to do!) Wisely, a 'coffee/tea break' was suggested, to give the team time to 'fix' things.

Hence a panic-call to me, working from home. It turned-out that a new-ish and less-experienced coder had been given this job, and she had been shown the interface as key-value pairs (her interpretation) - and even on the white-board there were no braces to show it as a dict! Whereas the more experienced programmer who originally assembled the class/signature/interface, had used a dict. Unhappiness was evident, argument threatened...

I quickly mocked-up the situation. (code below). The fastest solution, which didn't require altering any calling-code, seemed to be to change the class's signature to use *args and **kwargs, which thereafter required only a single 'normalisation' step to turn any/the kwargs into a dict - which in-turn enabled the existing code, unchanged.

Quick and dirty to be sure! However, it allowed the demo to go ahead and recovered everyone's feelings of satisfaction/success!

Aside from 'repairing' team spirit, (as regular readers will recognise) I wasn't going to let this pass without some thought and discussion (and perhaps I might learn something about Python interfacing)!
</tldr;>


Python's powerful polymorphic capabilities [insert expression of thanks here!] allow us to substitute collections - with care. So, a simple function, such as:)

def f( colln ):
        for element in colln:
                print( element )

will work quite happily when (separately) fed either a tuple or a list.

It will also work with a string (a collection of characters) - as long as we are happy with single characters being output.

What about dicts as collections? It will work with dicts, as long as we consider the dict's keys to be 'the dict' (cf its collection of values).

Well that's very flexible!

However, whilst a set of key-value arguments 'quack' somewhat like a dict, or even a tuple (of k-v pairs), they will not be accepted as parameters. Crash!

Yes it can be solved, with a 'normalisation' function, as described above, perhaps as a working-theory:

def f( *colln_as_tuple, **key_value_pairs_as_dict ):
        my_dict = key_value_pairs_as_dict if key_value_pairs_as_dict \
                                                else colln_as_tuple[ 0 ]
        # no change to existing code using my_dict
        ...


However, this seems like a situation that "quacks like a duck", yet the built-in polymorphism doesn't auto-magically extend to cover it.

So:
- is that the way 'polymorphism' really works/should work?
- is expecting the dict to work as a "collection", already 'taking things too far'?
- is expecting the key-values to work, really 'taking things too far'?

Remember that I am not an OOP-native! The discussion around-the-office diverged into two topics: (1) theoretical: the limits and capability of polymorphism, and (2) practical: the understanding and limits of a Python "collection".


How do you see it? How would you solve the Python coding problem - without re-writing both sprints-worth of code? Did we leave polymorphism 'behind' and expect Python to perform magic?


### investigative and prototyping code ###

def print_dict( dictionary ):
    # helper function only
    for key, value in dictionary.items():
           print( "\t", key, value )


def f( **kwargs ):
    print( kwargs, type( kwargs ) )
    print_dict( kwargs )


f( a=1, b=2 )                           # arguments as key-value pairs
### {'a': 1, 'b': 2} <class 'dict'>
###      a 1
###      b 2

d={ 'a':1, 'b':2 }                      # arguments as a dictionary
try:
        f( d )
except TypeError:
        print( "N/A" )
### N/A

### Traceback (most recent call last):
###   File "<stdin>", line 1, in <module>
### TypeError: f() takes 0 positional arguments but 1 was given


f( **d )                        # yes, easy to edit arguments, but...
### {'a': 1, 'b': 2} <class 'dict'>
###      a 1
###      b 2

try:
        f( { 'a':1, 'b':2 } )   # or modify API to accept single dict
except TypeError:
        print( "N/A" )
### N/A


print( "\nwith g()\n" )
### with g()

def g( *args, **kwargs ):
    print( args, type( args ), kwargs, type( kwargs ) )
    for arg in args:                    # handle single-objects, eg dict
        print( arg, type( arg ) )
        print_dict( arg )
    for k, v in kwargs.items():         # handle key-value pairs
        print( "\t\t", k, v )


g( a=1, b=2 )                           # arguments as key-value pairs
### () <class 'tuple'> {'a': 1, 'b': 2} <class 'dict'>
###              a 1
###              b 2

g( d )                                  # argument as a dictionary
### {'a': 1, 'b': 2},) <class 'tuple'> {} <class 'dict'>
### {'a': 1, 'b': 2} <class 'dict'>
###      a 1
###      b 2


--
Regards,
=dn
--
https://mail.python.org/mailman/listinfo/python-list

Reply via email to