2

I want to use an object's properties for recursive pattern replacement. Since the object's properties are not known at time of class definition I cannot use @property decorators

Example

class Test():
    def __init__(self):
        self.date = '{year}-{month}'
        self.year = '2020'
        self.month = '06'

        self.path = '/dev/null'
        self.file_name = 'ABC-{date}.mp4'
        self.file = '{path}/{file_name}'

    def print(self):
        print(__class__)
        print ("Filename is {file_name}".format(**vars(self)))
        print ("File {file} will be written to {path}".format(**vars(self)))

t = Test()
t.print()

Actual output

<class '__main__.Test'>
Filename is ABC-{date}.mp4
File {path}/{file_name} will be written to /dev/null

Desired output

<class '__main__.Test'>
Filename is ABC-2020-06.mp4
File /dev/null/ABC-2020-06.mp4 will be written to /dev/null
Chris
  • 23
  • 3
  • It'd be interesting to see if there's a solution to this, but I doubt that there is. Nested format strings feels like it's impossible because it's just an [expression that's evaluated at runtime](https://stackoverflow.com/a/41227080/929999). But perhaps there's some voodoo I haven't seen before. Your best bet is to create a parser or something that detects `{...}` and keeps attempting to format/parse the string for as long as there's something to parse. Altho that seems hacky. – Torxed Jun 17 '20 at 15:42
  • What do you mean by *"the object's properties are not known at time of class definition"*? In the example you gave, all the instance attributes are defined in `__init__`, and `Test.print` only uses attributes defined in `__init__`. – wjandrea Jun 17 '20 at 15:53
  • This seems like a bit of an [XY-problem](https://meta.stackexchange.com/q/66377/343832), but thankfully you've included the problem you're trying to solve. – wjandrea Jun 17 '20 at 15:54
  • BTW welcome to SO! Check out the [tour], and [ask] if you want advice. This is a great first question even though it's a bit unclear. – wjandrea Jun 17 '20 at 15:57
  • Should have clarified: This is just a simplified example, the properties in my actual code are only passed to __init__ at run time. – Chris Jun 17 '20 at 16:23
  • @Chris Oh, you mean the *values* of the attributes aren't known? That's pretty standard, and still doesn't prevent you from using properties. – wjandrea Jun 17 '20 at 16:37

4 Answers4

1

You mentioned recursion but did not actually use any recursion in your code.

You can achieve the desired behavior by implementing a recursive method that formats a given template string until the rendered result is the same as the input:

def render(self, template):
    rendered = template.format(**vars(self))
    return rendered if rendered == template else self.render(rendered)

def print(self):
    print(self.render("Filename is {file_name}"))
    print(self.render("File {file} will be written to {path}"))

With the above changes, your code would output:

Filename is ABC-2020-06.mp4
File /dev/null/ABC-2020-06.mp4 will be written to /dev/null
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • 1
    Wow, so simple! I had tried other approaches with recursion before but ended up in endless loops – Chris Jun 17 '20 at 16:27
1

You can use properties. You just need to use more of them than you expected.

class Test:
    def __init__(self):
        self.year = '2020'
        self.month = '06'

        self.path = '/dev/null'

    @property
    def file(self):
        return '{0.path}/{0.file_name}'.format(self)

    @property
    def file_name(self):
        return 'ABC-{0.date}.mp4'.format(self)

    @property
    def date(self):
        return '{0.year}-{0.month}'.format(self)

    def print(self):
        print(__class__)
        print ("Filename is {0.file_name}".format(self))
        print ("File {0.file} will be written to {0.path}".format(self))

t = Test()
t.print()

path, year, and month are the only "hard-coded" attributes; the rest can be derived. The recursion (such as it is) is implicit in that each attribute access may be a property which itself invokes another property. This is the key benefit of properties: their interface is indistinguishable from an instance attribute.

chepner
  • 497,756
  • 71
  • 530
  • 681
0

It’s possible, by overriding __dict__ and __getattribute__:

class Test:
    def __init__(self):
        self.date = '{year}-{month}'
        self.year = '2020'
        self.month = '06'

        self.path = '/dev/null'
        self.file_name = 'ABC-{date}.mp4'
        self.file = '{path}/{file_name}'
        self.__dict__ = {x: getattr(self, x) for x in vars(self)}

    def __getattribute__(self, a):
        real_attribute = super().__getattribute__(a)
        if isinstance(real_attribute, str) and "{" in real_attribute:
            return real_attribute.format(
                **{
                    x: getattr(self, x)
                    for x in vars(self)
                    if "{" + x + "}" in real_attribute
                }
            )
        return real_attribute
    def print(self):
        print(__class__)
        print ("Filename is {file_name}".format(**vars(self)))
        print ("File {file} will be written to {path}".format(**vars(self)))

t = Test()
t.print()
LeopardShark
  • 3,820
  • 2
  • 19
  • 33
  • `self.__dict__` is set automatically, so why do you set it manually? – wjandrea Jun 17 '20 at 16:42
  • 1
    @wjandrea `self.__dict__` doesn’t seem to use the defined `__getattribute__`. If I run it without that line, I just get ` Filename is ABC-{date}.mp4 File {path}/{file_name} will be written to /dev/null`. – LeopardShark Jun 17 '20 at 16:44
0

You can do it by using a little helper class (named FL below) along with f-strings literals instead of the str.format() method. Here's what I mean:

class FL:
    """ Lazy literal f-string interpolation: Postpones evaluation of string
        until instance is converted to a string.
    """
    def __init__(self, func):
        self.func = func
    def __str__(self):
        return self.func()


class Test():
    def __init__(self):
        self.date = FL(lambda: f'{self.year}-{self.month}')
        self.year = '2020'
        self.month = '06'

        self.path = '/dev/null'
        self.file_name = FL(lambda: f'ABC-{self.date}.mp4')
        self.file = FL(lambda: f'{self.path}/{self.file_name}')

    def print(self):
        print(__class__)
        print(FL(lambda: f"Filename is {self.file_name}"))
        print(FL(lambda: f"File {self.file} will be written to {self.path}"))

t = Test()
t.print()
martineau
  • 119,623
  • 25
  • 170
  • 301