5

I would like to limit the max number of instance of a dataclass and to know the index of the instance. This is the behaviour that I want:

Veget('tomato', 2.4, 5)
Veget('salad', 3.5, 2)
Veget('carot', 1.2, 7)

for Veget in Veget.instances:
    print(Veget)
Veget(index=0, name='tomato', price=2.4, quantity=5)
Veget(index=1, name='salad', price=3.5, quantity=2)
Veget(index=2, name='carot', price=1.2, quantity=7)

I tried the following, which does handle the creation limit:

from dataclasses import dataclass

MAX_COUNT = 3


class Limited:

    instances = []

    def __new__(cls, *_):
        if len(cls.instances) < MAX_COUNT:
            newobj = super().__new__(cls)
            cls.instances.append(newobj)
            return newobj
        else:
            raise RuntimeError('Too many instances')

@dataclass
class Veget(Limited):
    name: str
    price: float
    quantity: int

But it won't show the index when printed:

Veget(name='tomato', price=2.4, quantity=5)
Veget(name='salad', price=3.5, quantity=2)
Veget(name='carot', price=1.2, quantity=7)
Arne
  • 17,706
  • 5
  • 83
  • 99
laticoda
  • 100
  • 14
  • Inheritance isn't a great solution for this because all of the classes that inherit from `Limited` will share the same list of instances. I think you want a metaclass that sets up the various class variables and the `__new__` method of new classes. – Patrick Haugh Nov 07 '19 at 21:58
  • As a side note, when you write a generic-usage `__new__` you should pass both arguments as well as keyword arguments down to the `__init__` (e.g. def `__new__(cls, *_, **__)`). If you don't, inheriting classes won't be able to have keyword arguments any more, which is bad for dataclasses in particular. – Arne Nov 08 '19 at 08:36

2 Answers2

2

Placing implicit restrictions on dataclasses or requiring some kind of validation is usually implemented via the designated __post_init__ instead of using object inheritance. An implementation leveraging it could look like this, which would in my opinion be a little easier to maintain and understand:

from dataclasses import dataclass, field

MAX_COUNT = 3
VEGET_INDEX = []


@dataclass
class Veget:
    index: int = field(init=False)
    name: str
    price: float
    quantity: int

    def __post_init__(self):
        self.index = len(VEGET_INDEX)
        if self.index >= MAX_COUNT:
            raise RuntimeError("Too many instances")
        VEGET_INDEX.append(self)

You could also use a counter instead of the list that gets incremented in the post init routine, but a reference list seems convenient for debugging purposes. Anyway, creating the three allowed instances and trying to create a fourth would look like this:

>>> Veget('tomato', 2.4, 5)
Veget(index=0, name='tomato', price=2.4, quantity=5)
>>> Veget('salad', 3.5, 2)
Veget(index=1, name='salad', price=3.5, quantity=2)
>>> Veget('carot', 1.2, 7)
Veget(index=2, name='carot', price=1.2, quantity=7)
>>> Veget('potato', 0.7, 3)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<string>", line 5, in __init__
  File "<input>", line 17, in __post_init__
RuntimeError: Too many instances
Arne
  • 17,706
  • 5
  • 83
  • 99
  • i understand what you mean but is there anyway to implement class attribute inside a dataclass ? just to see how it could be written. – laticoda Nov 10 '19 at 11:24
  • 1
    Yeah, it's listed [here](https://docs.python.org/3/library/dataclasses.html#class-variables) in the docs how to do it, and [here](https://gist.github.com/a-recknagel/05d2a9e07de10de8132827f621a8d65d) is a gist of your example in that style. It introduces the `typing` module and a special hack `dataclass` does regarding it, which is why I didn't use it in my post. But it's pretty neat if it helps you better organize your code. – Arne Nov 11 '19 at 07:32
  • 1
    glad I could help =) – Arne Nov 12 '19 at 07:43
0

We want to modify instantiation behavior Hence we need to create essentially a constructor using a class method.

from dataclasses import dataclass 

MAX_COUNT = 3
VEGET_INDEX = []

@dataclass 
class Veget:
    name : str 
    price : float 
    quantity : int
    

    @classmethod
    def create(cls, name, price, quantity):
        index = len(VEGET_INDEX)
        if index >= MAX_COUNT:
            raise RuntimeError("Too many instances")
        print('ok')
        the_new_thing = cls(name, price, quantity)
        VEGET_INDEX.append(the_new_thing)
        return the_new_thing


Veget.create('tomato', 2.4, 5)
Veget.create('salad', 3.5, 2)
Veget.create('carot', 1.2, 7)
Veget.create('potato', 0.7, 3)
ok
ok
ok
Traceback (most recent call last):
  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 31, in <module>
    start(fakepyfile,mainpyfile)  File "/data/user/0/ru.iiec.pydroid3/files/accomp_files/iiec_run/iiec_run.py", line 30, in start
    exec(open(mainpyfile).read(),  __main__.__dict__)
  File "<string>", line 27, in <module>
  File "<string>", line 17, in create
RuntimeError: Too many instances
[Program finished]

As requested by OP to print index we can,

from dataclasses import dataclass, field

MAX_COUNT = 3
VEGET_INDEX = []


@dataclass
class Veget:
    index: int = field(init=False)
    name: str
    price: float
    quantity: int

    def __post_init__(self):
        self.index = len(VEGET_INDEX)
        if self.index >= MAX_COUNT:
            raise RuntimeError("Too many instances")
        VEGET_INDEX.append(self)

Veget('tomato', 2.4, 5)
Veget('salad', 3.5, 2)
Veget('carot', 1.2, 7)
#Veget('potato', 0.7, 3)

for Veget in VEGET_INDEX :
    print(Veget)
Veget(index=0, name='tomato', price=2.4, quantity=5)
Veget(index=1, name='salad', price=3.5, quantity=2)
Veget(index=2, name='carot', price=1.2, quantity=7)

[Program finished] 
Subham
  • 397
  • 1
  • 6
  • 14