48

I want to create a class that behaves like collections.defaultdict, without having the usage code specify the factory. EG: instead of

class Config(collections.defaultdict):
    pass

this:

Config = functools.partial(collections.defaultdict, list)

This almost works, but

isinstance(Config(), Config)

fails. I am betting this clue means there are more devious problems deeper in also. So is there a way to actually achieve this?

I also tried:

class Config(Object):
    __init__ = functools.partial(collections.defaultdict, list)
Neil G
  • 32,138
  • 39
  • 156
  • 257
Evan Benn
  • 1,571
  • 2
  • 14
  • 20
  • 1
    Wouldn't a more reasonable check be: `isinstance(Config(), collections.defaultdict)`? As long as `Config` is not a class, `isinstance` will of course fail. As explicit type checks aren't common / recommended in python you might as well keep using `Config` as above - it should work as intented in many cases. – sebastian Aug 12 '16 at 06:27

5 Answers5

37

I don't think there's a standard method to do it, but if you need it often, you can just put together your own small function:

import functools
import collections


def partialclass(cls, *args, **kwds):

    class NewCls(cls):
        __init__ = functools.partialmethod(cls.__init__, *args, **kwds)

    return NewCls


if __name__ == '__main__':
    Config = partialclass(collections.defaultdict, list)
    assert isinstance(Config(), Config)
fjarri
  • 9,546
  • 39
  • 49
  • 7
    FYI: I submitted this code to Python https://bugs.python.org/issue33419 – Neil G May 03 '18 at 18:52
  • Drats - I was hoping to see this implemented, rather than deferred (probably forever) and closed! – snoopyjc Mar 27 '22 at 06:15
  • Is there a way to make `Config.__repr__` display something more reasonable besides `partialclass..NewCls`? I think this would be similar to `functools.wraps` but the only way to know what the new class is called is from its declaration – Addison Klinke Jun 14 '22 at 03:35
17

At least in Python 3.8.5 it just works with functools.partial:

import functools

class Test:
    def __init__(self, foo):
        self.foo = foo
    
PartialClass = functools.partial(Test, 1)

instance = PartialClass()
instance.foo
Dieter Weber
  • 403
  • 3
  • 7
  • 1
    Bear in mind that you can't subclass this or nest the partial call. Which I needed. – seb May 03 '22 at 02:13
12

I had a similar problem but also required instances of my partially applied class to be pickle-able. I thought I would share what I ended up with.

I adapted fjarri's answer by peeking at Python's own collections.namedtuple. The below function creates a named subclass that can be pickled.

from functools import partialmethod
import sys

def partialclass(name, cls, *args, **kwds):
    new_cls = type(name, (cls,), {
        '__init__': partialmethod(cls.__init__, *args, **kwds)
    })

    # The following is copied nearly ad verbatim from `namedtuple's` source.
    """
    # For pickling to work, the __module__ variable needs to be set to the frame
    # where the named tuple is created.  Bypass this step in enviroments where
    # sys._getframe is not defined (Jython for example) or sys._getframe is not
    # defined for arguments greater than 0 (IronPython).
    """
    try:
        new_cls.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    return new_cls
Ruben
  • 541
  • 6
  • 14
10

If you actually need working explicit type checks via isinstance, you can simply create a not too trivial subclass:

class Config(collections.defaultdict):

    def __init__(self): # no arguments here
        # call the defaultdict init with the list factory
        super(Config, self).__init__(list)

You'll have no-argument construction with the list factory and

isinstance(Config(), Config)

will work as well.

sebastian
  • 9,526
  • 26
  • 54
2

Could use *args and **kwargs:

class Foo:
    def __init__(self, a, b):
        self.a = a
        self.b = b

    def printy(self):
        print("a:", self.a, ", b:", self.b)

class Bar(Foo):
    def __init__(self, *args, **kwargs):
        return super().__init__(*args, b=123, **kwargs)

if __name__=="__main__":
    bar = Bar(1)
    bar.printy()  # Prints: "a: 1 , b: 123"
run_the_race
  • 1,344
  • 2
  • 36
  • 62