Search code examples
pythonstringelementtreeduck-typing

Duck Typing in Python (in order to mimic a String)


Duck Typing in general is explained here: https://stackoverflow.com/a/4205163/19446851.

What does Duck Typing mean in Python? Is it really possible to make one type look like another type. Can I have an own class that "looks and quacks" like a string?

See the following example:

from dataclasses import dataclass

@dataclass
class ColoredObject:
    color : ... 
    name : ...
    def __str__(self):
        return self.color + " " + self.name

x = ColoredObject("red", "circle")
print("I have a " + x +  ".")

That code does not work because strings and objects of the type ColoredObject cannot be concatenated. If in Python it would actually be possible to make ColoredObject "look and quack" like a string, there should be a way to concatenate both without the explicit conversion.

The following is a more practical example. I try to make the class MutableText "looking and quacking" like a string so that I can use it in an XML Element Tree.

import xml.etree.cElementTree as ET

root = ET.Element("root_node")

class MutableText:
    def __init__(self, init_text):
        self.text = init_text
    
mutable_contents = MutableText("ZigZag")

ET.SubElement(root, "child_node").text = mutable_contents

tree = ET.ElementTree(root)
tree.write("filename.xml")

The goal is that line ET.SubElement(root, "child_node").text = mutable_contents works. What can I do to achieve this?

The error message, that I get with the code is TypeError: cannot serialize <__main__.MutableText object at 0x7fafc0099e20> (type MutableText)

I already got the advice to inherit from str class. But this is not Duck Typing. This is static typing like in C++ or Java.

Another advice to use ET.SubElement(root, "child_node").text = mutable_contents.text is good. But that is also not Duck Typing. And that means, I always have to update the ElementTree whenever mutable_contents changes. (This is actually my motivation, why I ask this academic question. I am trying to find a solution for not having to always do this update.)

I also got the comment that ElementTree actually expects a string and not a MutableString. But why do people then say, Python uses Duck Typing? And why don't I get the error Message that a string is expected where a MutableString is provided?

Obviously there is something missing in my code in order to make MutableText like a string? But what is missing? And shouldn't Python give me an error message when it tries to call something from MutableText, which is missing?


Solution

  • Is it really possible to make one type look like another type?

    This is quite typical of people who come from a statically typed language to interpret duck typing but it misses a significant aspect of the whole deal: it isn't that you are faking another type it is that your code relies on behaviour instead of types.

    say we have this function:

    def example_math_equation(a,b):
        return a + 4*b
    

    This doesn't dictate anything about what types a or b have to be, just that it should be valid to multiply b by an integer and that a can be added to the result. as such this code would be applicable to not just numbers but also sequences:

    >>> example_math_equation("foo", "bar")
    'foobarbarbarbar'
    

    This is the idea of duck typing, that you avoid checking for types of data as much as possible and just assume they support the operations you need and if they don't you get an error. Then if someone wants to make a new data type - not with the intent to mimic another well defined data type but just to behave differently - then it could be used instead.

    If you don't want to do duck typing, you just want to cheat and mimic the str class there is a route that exists:

    
    class MockStr:
        def __init__(self, initial_text):
            self._TEXT = initial_text
    
    def make_wrapper_method(methodname):
        "makes a method that will forward to ._TEXT field"
        def wrapper_method(self, *args, **kw):
            #print("CALLED WRAPPER", methodname)
            return getattr(self._TEXT, methodname)(*args, **kw)
        return wrapper_method
    for methodname, underlying_method in vars(str).items():
        if not callable(underlying_method) or methodname in dir(MockStr):
            continue
        setattr(MockStr, methodname, make_wrapper_method(methodname))
    
    x = MockStr("hi there")
    
    print(x + " got added to a string")
    
    

    But don't go down this route, the first issue you will come across is that because str can't be added to any other built in type it doesn't bother defining a __radd__ so "a" + x will fail unless you do that yourself, but more specifically if your goal is to make a mutable string you truely shouldn't do this because your object won't be immutable

    If a class defines mutable objects and implements an __eq__() method, it should not implement __hash__(), since the implementation of hashable collections requires that a key’s hash value is immutable

    and if the library you are using expects the strings to be immutable and makes certain optimisations based on that then the whole journey of trying to accomplish that will just be a wild goose chase, you are much better off to learn what behaviours (methods) the library is expecting and see if you can reasonably provide those with a data type that also has the behaviour you want.