How to use an equivalent to __post_init__ method with normal class?

Question:

I would like to store entity used in my code and avoid multiple occurrences. Thus, my idea was to use an __init__ method for collecting the main data for my class, and then use a kind of __post_init__ method for computing an id from my class object. Here is the code:

class Worker(Base):
    __tablename__='worker'
    id = Column(Integer,primary_key=True)
    profile=Column(String(100),nullable=False)
    useragent=Column(String(100),nullable=False)
    def __init__(self,useragent,profile):
        """ specify the main information"""
        print('init')
        self.profile= profile
        self.useragent=useragent
    def __post_init__(self):
        """ compute an id based on self, the worker"""
        self.id=id(self)
        print('dans post init')

With this example, the __init__ method can be used, but it doesn’t run the __post_init__ method like we could expect with dataclass, for example.

How could I run this method just after the execution of the __init__ method ?

Asked By: HappyCloudNinja

||

Answers:

The __post_init__ method is specific to the dataclasses library, because the __init__ method on dataclass classes is generated and overriding it would entirely defeat the purpose of generating it in the first place.

SQLAlchemy, on the other hand, provides an __init__ implementation on the base model class (generated for you with declarative_base()). You can safely re-use that method yourself after setting up default values, via super().__init__(). Take into account that the SQLAlchemy-provided __init__ method only takes keyword arguments:

def __init__(self, useragent, profile):
    """specify the main information"""
    id = generate_new_id(self)
    super().__init__(id=id, useragent=useragent, profile=profile)

If you need to wait for the other columns to be given updated values first (because perhaps they define Python functions as a default), then you can also run functions after calling super().__init__(), and just assign to self:

def __init__(self, useragent, profile):
    """specify the main information"""
    super().__init__(useragent=useragent, profile=profile)
    self.id = generate_new_id(self)

Note: you do not want to use the built-in id() function to generate ids for SQL-inserted data, the values that the function returns are not guaranteed to be unique. They are only unique for the set of all active Python objects only, and only in the current process. The next time you run Python, or when objects are deleted from memory, values can and will be reused, and you can’t control what values it’ll generate next time, or in a different process altogether.

If you were looking to only ever create rows with unique combinations of the useragent and profile columns, then you need to define a UniqueConstraint in the table arguments. Don’t try to detect uniqueness at the Python level, as you can’t guarantee that another process will not make the same check at the same time. The database is in a much better position to determine if you have duplicate values, without risking race conditions:

class Worker(Base):
    __tablename__='worker'
    id = Column(Integer, primary_key=True, autoincrement=True)
    profile = Column(String(100), nullable=False)
    useragent = Column(String(100), nullable=False)

    __table_args__ = (
        UniqueConstraint("profile", "useragent"),
    )

or you could use a composite primary key based on the two columns; primary keys (composite or otherwise) must always be unique:

class Worker(Base):
    __tablename__='worker'
    profile = Column(String(100), primary_key=True, nullable=False)
    useragent = Column(String(100), primary_key=True, nullable=False)
Answered By: Martijn Pieters

I implemented the similar behavior using the __init_subclass__ method:

class Parent:
    def __init_subclass__(cls, **kwargs):
        def init_decorator(previous_init):
            def new_init(self, *args, **kwargs):
                previous_init(self, *args, **kwargs)
                if type(self) == cls:
                    self.__post_init__()
            return new_init

        cls.__init__ = init_decorator(cls.__init__)

    def __post_init__(self):
        pass


class Child(Parent):
    def __init__(self):
        print('Child __init__')

    def __post_init__(self):
        print('Child __post_init__')


class GrandChild(Child):
    def __init__(self):
        print('Before calling Child __init__')
        Child.__init__(self)
        print('After calling Child __init__')

    def __post_init__(self):
        print('GrandChild __post_init__')


child = Child()
# output:
#  Child __init__
#  Child __post_init__


grand_child = GrandChild()
# output:
#  Before calling Child __init__
#  Child __init__
#  After calling Child __init__
#  GrandChild __post_init__
Answered By: iman azari
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.