Can I have two sets of arguments

73 Views Asked by At

I am trying to make a dice roller in python. the code is:

class die:
    def __init__(self, num:int, die:int | str, mod:int=0, keep:int=None):
        keep = keep if keep != None else num
        if keep > num:  keep = num
        self.num, self.die, self.mod, self.keep = num, die, mod, keep
    def __repr__(self) -> str:
        mod = f"+{self.mod}" if self.mod > 0 else f"-{abs(self.mod)}" if self.mod < 0 else ''
        keep = f"{f"{self.keep}kh" if self.keep > 0 else f"{abs(self.keep)}kl" if self.keep < 0 else ''}"
        return f"{keep}{self.num}d{self.die}{mod}"
    def roll(self) -> int:
        log = []
        die = range(0, self.num)
        for _ in die:
            log += [rand.randint(0, (10 if self.die == '%' else int(self.die)) * (10 if self.die == '%' else 1))]
        if self.keep > 0:
            log = [log] + [heapq.nlargest(self.keep, log)]
        elif self.keep < 0:
            log = [log] + [heapq.nsmallest(abs(self.keep), log)]
        final = sum(log[1], self.mod)
        return final
#Sorry if my code is hard to read.

I want to add the ability to pass a dice expression (example: 4d6+3) as well as individual options. is there any way I can pass a separate argument if there is only one argument provided. For example:

def __init__(self, num:int, die:int | str, mod:int=0, keep:int=None OR expression:str)
2

There are 2 best solutions below

2
chepner On

__init__ is fine the way it is. The job of parsing a string into suitable arguments should be handled by a class method.

Something like

class die:
    def __init__(self, num:int, die:int | str, mod:int=0, keep:int=None):
        ...

    @classmethod
    def from_description(cls, descr: str) -> die:
        num, die, mod = re.match("\d+d\d+\+\d+").groups()
        return cls(num, die, mod)

d = die.from_description("4d6+3")
0
blhsing On

Declaring functions with the same name but different parameters is called overloading in other programming languages such as C++ and Java, but Python has only very limited support for the concept through functools.singledispatch and typing.overload, neither of which allows functions with distinctly different signatures and separate implementations to share the same name, which is what you're trying to do here.

With type annotations introduced in Python 3.5, it is possible to implement true overloading with a metaclass, as demonstrated in the MultiMeta recipe included in the Python Cookbook written by David Beazley, where he uses the __prepare__ method of a metaclass to allow methods of the same name to be stored in a MultiMethod instance in a dict subclass that detects name collisions.

David's implementation uses simple type equivalence in matching arguments against method signatures, however, and does not support composite type annotations such as int | str as a result, so I refactored his code with the runtime type checker typeguard by decorating methods with typeguard.typechecked to validate arguments against signatures of each overloaded method:

import types
from typeguard import typechecked

class MultiMethod:
    def __init__(self, name):
        self._methods = []
        self.__name__ = name

    def register(self, method):
        self._methods.append(typechecked(method))

    def __call__(self, *args, **kwargs):
        for method in self._methods:
            try:
                return method(*args, **kwargs)
            except TypeError:
                pass
        raise TypeError('No matching method')

    def __get__(self, instance, cls):
        if instance is not None:
            return types.MethodType(self, instance)
        return self

class MultiDict(dict):
    def __setitem__(self, key, value):
        if key in self:
            current_value = self[key]
            if isinstance(current_value, MultiMethod):
                current_value.register(value)
            else:
                mvalue = MultiMethod(key)
                mvalue.register(current_value)
                mvalue.register(value)
                super().__setitem__(key, mvalue)
        else:
            super().__setitem__(key, value)

class Overloadable(type):    
    @classmethod
    def __prepare__(cls, clsname, bases):
        return MultiDict()

so that:

import re

class die(metaclass=Overloadable):
    def __init__(self, num: int, die: int | str, mod: int = 0):
        self.num = num
        self.die = die
        self.mod = mod

    def __init__(self, expr: str):
        self.__init__(*map(int, re.match(r'(\d+)d(\d+)\+(\d+)', expr).groups()))

print(vars(die('4d6+3')))

outputs:

{'num': 4, 'die': 6, 'mod': 3}

Demo: https://replit.com/@blhsing1/SnoopyArcticMicrostation