Friday Finking: Poly more thick

DL Neil PythonList at DancesWithMice.info
Fri Feb 28 19:34:25 EST 2020


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


More information about the Python-list mailing list