2

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?

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • If you are using safe data (not user entered), have you considered using `exec` or `eval` instead? This seems overengineered to me. – jarcobi889 Aug 18 '20 at 22:15
  • I don't see why simplifying this code would require `exec` or `eval`. Anyhow, it's an interesting question regardless of the framing. – Charles Duffy Aug 18 '20 at 23:25
  • (...that said, why not pass the names in explicitly?) – Charles Duffy Aug 18 '20 at 23:32
  • @CharlesDuffy. I appreciate you pointing that out. I've updated my answer to actually solve the problem – Mad Physicist Aug 18 '20 at 23:51
  • @CharlesDuffy: Thanks for your solution. That is exactly how I envisaged it, if I had known how to implement it. Now that I see your solution (and also Mad Physicist's) I wonder if I should edit the title of my question, since neither of the acceptable solutions uses some "name function" – Patrick Herfurth Aug 19 '20 at 15:00
  • As long as an edit doesn't invalidate existing answers, it's fair game. I'd go ahead, if you think you can clarify. – Charles Duffy Aug 19 '20 at 15:01
  • @MadPhysicist: Thanks for understanding what I actually want. Your proposed solution had not even crossed my mind, although I might have to tweak it slightly to match the real file – Patrick Herfurth Aug 19 '20 at 15:06

2 Answers2

3

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'
Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • I really like the `__getattr__` approach you're using. With the code you have here, changes to `fn_names` will immediately be reflected in behavior rather than needing to rerun setup code after updates and worry about state carried over from old values. – Charles Duffy Aug 19 '20 at 01:02
2

Consider closing over your keys and values -- note that you can see the below code running at https://ideone.com/qmoZCJ:

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 getter(key, val):
    def _get_line(self):
        return self.file[val][0].split('=')[1]
    return _get_line
    
for key, val in fn_names.items():
    setattr(Filer, key, getter(key, val))

f = Filer(test_file)
print("Author: ", f.get_author())
print("Date: ", f.get_date())
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Neat. I did the same, but through `__getattr__` so you don't need the extra loop or explicitly creating methods or closures except on the fly. – Mad Physicist Aug 18 '20 at 23:52