Python: How can I initialize class of NamedTuple

1.3k Views Asked by At

I want to create a class, and I don’t want its attributes to be modified after new, so I choose NamedTuple,

But I hope that It can do something immediately after initialize,

So I hope to I can override the __init__ method,

but if I do this, I will encounter AttributeError: Cannot overwrite NamedTuple attribute __init__.

Is there any elegant code that can do it?

My actual case is to initialize the style of ttk, as follows.

import tkinter as tk
from tkinter import ttk
from typing import NamedTuple


class TTKStyle(NamedTuple):
    LF_NORMAL = f'Normal.TLabelframe'

    def init_style(self):
        style = ttk.Style()
        style.configure(self.LF_NORMAL, background='#FFFF00')
        style.configure(f'{self.LF_NORMAL}.Label', foreground='red', background='blue', font=('courier', 15, 'bold'))


root = tk.Tk()
ttk_style = TTKStyle()
ttk_style.init_style()  # <-- I don't want to write this line.
lf_exif = ttk.LabelFrame(root, text='EXIF Tag', style=ttk_style.LF_NORMAL)
lf_exif.pack()
tk.Label(lf_exif, text='ExifVersion').pack()
root.mainloop()

1

There are 1 best solutions below

0
On BEST ANSWER

Decorator

You can use the decorator to help you.

from typing import NamedTuple, Type


def init_namedtuple(init_func_name):
    def wrap(class_obj: Type[NamedTuple]):
        def new_instance(*args, **kwargs):
            instance_obj = class_obj(*args, **kwargs)
            init_func = getattr(instance_obj, init_func_name)
            if init_func:
                init_func()
            return instance_obj

        return new_instance

    return wrap


@init_namedtuple('init_style')
class TTKStyle(NamedTuple):
    ...

__slots__

or you can use normal class and add __slots__ and put your init function on __init__ directly (see class Person3), for example:

from typing import NamedTuple
class Person(NamedTuple):
    NAME: str
    SCIENTIFIC_NAME = 'Homo sapiens'

class Person2:
    __slots__ = ()
    NAME: str
    SCIENTIFIC_NAME = 'Homo sapiens'


class Person3:
    __slots__ = ()

    NAME: str
    SCIENTIFIC_NAME = 'Homo sapiens'

    def __init__(self, name: str):
        self.__class__.NAME = name
        ...
class Person4:
    __slots__ = ('_name', '_scientific_name')

    def __init__(self, name: str):
        self._name = name  # Technically, the way is not really read-only, but a conventional is so. 
        self._scientific_name = 'Homo sapiens'

    @property
    def name(self):
        return self._name

    @property
    def scientific_name(self):
        return self._scientific_name
for person in (Person('Carson'), Person('Carson2')):
    print(person.NAME)  # output: Carson, Carson2


for person in (Person4('Carson3'), Person4('Carson4')):
    print(person.name)  # output: Carson3, Carson4

if "it's ok but weird":
    unknown_person = Person2()
    unknown_person.__class__.NAME = '???'
    print(unknown_person.NAME)

for person in [Person3('Marry'), Person3('Marry2')]:
    # BE CAREFUL! IF YOU USE THE CLASS ATTRIBUTE, THEN ALL THE INSTANCES IS SHARED THIS VARIABLE.
    print(person.NAME)  # both output is: Marry2

The class of all the above attributes is read-only (for 2 and 3, you can change it from the class but not change from instance) and not accept other new attributes.