Using class or static method as default_factory in dataclasses
Asked Answered
C

5

11

I want to populate an attribute of a dataclass using the default_factory method. However, since the factory method is only meaningful in the context of this specific class, I want to keep it inside the class (e.g. as a static or class method). For example:

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:
    cards: List[str] = field(default_factory=self.create_cards)

    @staticmethod
    def create_cards():
        return ['King', 'Queen']

However, I get this error (as expected) on line 6:

NameError: name 'self' is not defined

How can I overcome this issue? I don't want to move the create_cards() method out of the class.

Castra answered 23/7, 2020 at 9:38 Comment(5)
where is self in code? and on which line error is coming.Joviality
@Joviality Thank you, I fixed the code (I had omitted the self). The error comes from line 6.Castra
Does this answer your question? NameError within class definitionKeirakeiser
@KurtBourbaki did I answer your question?Feme
@Feme Your answer helped me solving the issue, yes. I'll add a comment below.Castra
F
8

One possible solution is to move it to __post_init__(self). For example:

@dataclass
class Deck:
    cards: List[str] = field(default_factory=list)

    def __post_init__(self):
        if not self.cards:
            self.cards = self.create_cards()

    def create_cards(self):
        return ['King', 'Queen']

Output:

d1 = Deck()
print(d1) # prints Deck(cards=['King', 'Queen'])
d2 = Deck(["Captain"])
print(d2) # prints Deck(cards=['Captain'])
Feme answered 23/7, 2020 at 11:5 Comment(2)
Thank you for your answer. I'm not very convinced about the __post_init__ method though: it seems to me like a sort of workaround that makes the default_factory parameter less intuitive to use (since it's basically overriding it). Why don't we completely omit the field(...) part then?Castra
The reason for using field(...) is to provide a default value. If we omit this part, d1 = Deck() is no longer valid and must be called with an empty list, i.e. d1 = Deck([]) (which is IHMO less elegant). With the suggested approach, your code becomes very flexible (e.g. you might allow different types cards: Union[List[str], str]) and can always correctly initialize it to a list self.cards = [self.cards]). This might also be useful if you develop a library for other developers because you know, that your method will always behave the same way and allows crappy input (Deck(None))Feme
N
4

One option is to wait until after you define the field object to make create_cards a static method. Make it a regular function, use it as such to define the cards field, then replace it with a static method that wraps the function.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Deck:

    # Define a regular function first (we'll replace it later,
    # so it's not going to be an instance method)
    def create_cards():
        return ['King', 'Queen']

    # Use create_cards as a regular function
    cards: List[str] = field(default_factory=create_cards)

    # *Now* make it it a static method
    create_cards = staticmethod(create_cards)

This works because the field object is created while the class is being defined, so it doesn't need to be a static method yet.

Nga answered 5/10, 2022 at 0:24 Comment(3)
did not know that staticmethod is also a functionLalittah
staticmethod is a type whose instances wrap functions; it's the descriptor protocol that causes both Deck.create_cards and Deck().create_cards to evaluate to the underlying function.Nga
(Fixed a typo; create_cards, not cards, should have been the argument to staticmethod.)Nga
L
2

Can you do something like this?

from dataclasses import dataclass, field

@dataclass
class Deck:
    cards: list[str] = field(
             default_factory=lambda: ["King", "Queen"])

ps. in python 3.9+ you can use list instead of typing.List.

Limiter answered 3/5, 2023 at 12:34 Comment(0)
B
1

I adapted momo's answer to be self contained in a class and without thread-safety (since I was using this in asyncio.PriorityQueue context):

from dataclasses import dataclass, field
from typing import Any, ClassVar

@dataclass(order=True)
class FifoPriorityQueueItem:
    data: Any=field(default=None, compare=False)
    priority: int=10
    sequence: int=field(default_factory=lambda: {0})
    counter: ClassVar[int] = 0

    def get_data(self):
        return self.data

    def __post_init__(self):
        self.sequence = FifoPriorityQueueItem.next_seq()

    @staticmethod
    def next_seq():
        FifoPriorityQueueItem.counter += 1
        return FifoPriorityQueueItem.counter

def main():
    import asyncio
    print('with FifoPriorityQueueItem is FIFO')
    q = asyncio.PriorityQueue()
    q.put_nowait(FifoPriorityQueueItem('z'))
    q.put_nowait(FifoPriorityQueueItem('y'))
    q.put_nowait(FifoPriorityQueueItem('b', priority=1))
    q.put_nowait(FifoPriorityQueueItem('x'))
    q.put_nowait(FifoPriorityQueueItem('a', priority=1))
    while not q.empty():
        print(q.get_nowait().get_data())

    print('without FifoPriorityQueueItem is no longer FIFO')
    q.put_nowait((10, 'z'))
    q.put_nowait((10, 'y'))
    q.put_nowait((1, 'b'))
    q.put_nowait((10, 'x'))
    q.put_nowait((1, 'a'))
    while not q.empty():
        print(q.get_nowait()[1])

if __name__ == '__main__':
    main()

Results in:

with FifoPriorityQueueItem is FIFO
b
a
z
y
x
without FifoPriorityQueueItem is no longer FIFO
a
b
x
y
z
Bascio answered 4/10, 2022 at 23:30 Comment(0)
O
0

Just reorder the definition of the attribute so it comes after the staticmethod.

from dataclasses import dataclass, field

@dataclass
class Deck:

    @staticmethod
    def create_cards():
        return ['King', 'Queen']

    cards: list[str] = field(default_factory=create_cards)

This is not ideal (those attributes should go first, ideally), but it is cleaner than the other answers in my opinion.

Outbreed answered 13/5 at 16:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.