Search code examples
pythonpython-3.xmonkeypatchingsetattr

Can dynamically created class methods know their 'created' name at runtime?


I have a class which I want to use to extract data from a text file (already parsed) and I want do so using dynamically created class methods, because otherwise there would be a lot of repetitive code. Each created class method shall be asociated with a specific line of the text file, e.g. '.get_name()' --> read a part of 0th line of text file. My idea was to use a dictionary for the 'to-be-created' method names and corresponding line.

import sys
import inspect

test_file = [['Name=Jon Hancock'], 
     ['Date=16.08.2020'], 
     ['Author=Donald Duck']]

# intented method names
fn_names = {'get_name': 0, 'get_date': 1, 'get_author': 2}

class Filer():
    def __init__(self, file):
        self.file = file

def __get_line(cls):
    name = sys._getframe().f_code.co_name
    line = fn_names[name]        # <-- causes error because __get_line is not in fn_names
    print(sys._getframe().f_code.co_name)    # <-- '__get_line' 
    print(inspect.currentframe().f_code.co_name)    # <-- '__get_line'
    return print(cls.file[line][0].split('=')[1])

for key, val in fn_names.items():
    setattr(Filer, key, __get_line)

f = Filer(test_file)
f.get_author()
f.get_date()

When I try to access the method name to link the method to the designated line in the text file, I do get an error because the method name is always '__get_line' instead of e.g. 'get_author' (what I had hoped for). Another way how I thought to solve this was to make '__get_line' accepting an additional argument (line) and set it by passing the val during 'the setattr()' as shown below:

def __get_line(cls, line):
    return print(cls.file[line][0].split('=')[1])

and

 for key, val in fn_names.items():
     setattr(Filer, key, __get_line(val))

however, then Python complains that 1 argument (line) is missing.

Any ideas how to solve that?


Solution

  • I would propose a much simpler solution, based on some assumptions. Your file appears to consist of key-value pairs. You are choosing to map the line number to a function that processes the right hand side of the line past the = symbol. Python does not conventionally use getters. Attributes are much nicer and easier to use. You can have getter-like functionality by using property objects, but you really don't need that here.

    class Filer():
        def __init__(self, file):
            self.file = file
            for line in file:
                name, value = line[0].split('=', 1)
                setattr(self, name.lower(), value)
    

    That's all you need. Now you can use the result:

    >>> f = Filer(test_file)
    >>> f.author
    'Donald Duck'
    

    If you want to have callable methods exactly like the one you propose for each attribute, I would one-up your proposal and not even have a method to begin with. You can actually generate the methods on the fly in __getattr__:

    class Filer():
        def __init__(self, file):
            self.file = file
    
        def __getattr__(self, name):
            if name in fn_names:
                index = fn_names[name]
                def func(self):
                    print(self.file[index][0].split('=', 1)[1])
                func.__name__ = func.__qualname__ = name
                return func.__get__(self, type(self))
            return super().__getattr__(name)
    

    Calling __get__ is an extra step that makes the function behave as if it were a method of the class all along. It binds the function object to the instance, even through the function is not part of the class.

    For example:

    >>> f = Filer(test_file)
    >>> f.get_author
    <bound method get_author of <__main__.Filer object at 0x0000023E7A247748>>
    >>> f.get_author()
    'Donald Duck'