7

Coming to Python from Java, I've been told that factories are not Pythonic. Thus, I'm looking for a the Python way to do something like the following. (I'm oversimplifying my goal so that I don't have to describe my entire program, which is very complicated).

My script will read in names of people (along with some information about them) and, from this, construct objects of type Person. The names may be repeated, and I only want one Person instance per name. These People may also belong to subclasses Man and Woman.

One way to do this would be to create a PersonFactory which would either return a newly instantiated Man or Woman or a reference to the previously instantiated Man/Woman with the same name. The other would be to create a set of all Person objects and check each time for the presence of a Person with the given name before instantiating a new object. Neither approach strikes me as Pythonic, though. The first seems a bit too cumbersome for Python (creating a whole class just to handle creation of another object? Really?) and the second will get expensive quickly, as I have a lot of names to process.

chimeracoder
  • 20,648
  • 21
  • 60
  • 60
  • Checking for the existence of a key in a dictionary is not really expensive, it's O(1). – Amber Aug 26 '10 at 03:18
  • Right, I overlooked that. Thanks, Amber! – chimeracoder Aug 26 '10 at 03:49
  • 3
    I cringe when I see the term "pythonic" in this sort of context. Python is just a tool. Use the tool to solve your problem the best way you know how. If that involves factories, so be it. Your customers don't care if you're "pythonic" or not. – Bryan Oakley Aug 26 '10 at 11:41
  • 2
    why cringe? as with human language, the concept of a given 'language' in the wider sense also comprises the many cultural aspects of its living usage, its community and so on. though i get to see many OO-inspired follies written in python, this community prefers to do things in a simpler, less artificially-convoluted way than the java folks. also, your customers may never come around to appreciate the beauty, simplicity, and understandability of your solution. but the next guy who gets assigned to do an upgrade of that code certainly will. – flow Aug 26 '10 at 12:17
  • 3
    @Bryan I think you have a valid point, but ultimately one must learn to use the language the way intended, as it's more powerful, concise, and "standard" that way. – Humphrey Bogart Aug 26 '10 at 15:19

6 Answers6

13

I don't think factories are un-Pythonic. You don't need a whole class, though. One big difference between Java and Python is that in Python you can have code outside of classes. So you might want to create a factory function. Or you can make the factory be a class method on the Person class:

class Person:

    name_map = {}

    @classmethod
    def person_from_name(cls, name):
        if name not in cls.name_map:
            cls.name_map[name] = cls(name)
        return cls.name_map[name]

    def __init__(self, name):
        etc...

Often the same patterns are at work in Python code as in Java, but we don't make as big a deal of it. In Java, you'd have a whole new class, which would imply a whole new .java file, and you'd need to make it a singleton, etc, etc. Java seems to breed this sort of complexity. A simple class method will do, so just use it.

Ned Batchelder
  • 364,293
  • 75
  • 561
  • 662
2
class Person(object):
    # ...

class Man(Person):
    # ...

class Woman(Person):
    # ...

constructors = {
    'male': Man,
    'female': Woman,
    None: Person,
}

people = {}

# in processing loop
if person.name not in people:
    people[person.name] = constructors[person.gender]()
person_object = people[person.name]

Since Python allows you to do things like calling storing class types in a dict, you don't need a factory; you can just look up a type and instantiate it.

Amber
  • 507,862
  • 82
  • 626
  • 550
  • This calls the constructor every time and then throws away most of the results -- what an utter, total waste of resources (`setdefault` often encourages just this kind of wanton wastefulness). – Alex Martelli Aug 26 '10 at 04:39
  • True, alex; it'd be nice if setdefault could be passed a callable instead and only evaluate it if the value isn't set. – Amber Aug 26 '10 at 05:56
  • Use a collections.defaultdict instead. This does what you are asking for - takes a function that is only evaluated if the key is missing. – Dave Kirby Aug 26 '10 at 07:44
  • @Dave: the problem with that is that it doesn't allow you to specify arguments to that function. So in this case, where we're trying to be able to check a value to determine what type to add, we don't have a way to pass in that value. – Amber Aug 26 '10 at 07:54
  • good point. It would not be difficult to write a variant of defaultdict that took the key as a parameter - just override the `__missing__` method. I was quite surprised that they did not include that in the standard lib when they added defaultdict. – Dave Kirby Aug 27 '10 at 07:52
  • **Note to readers: all of the above comments refer to an earlier edit of this answer, which used `setdefault`.** – Evgeni Sergeev Jul 27 '15 at 12:09
2

A free-standing function def PersonFactory(name, gender): is fine, though packaging it up as a classmethod, as @Ned suggests, shouldn't hurt (in this particular case it won't help much either, since the exact class of person to instantiate must vary). I think the cleanest implementation is in fact as a free-standing function, just because I prefer a classmethod to return instances of the class it's called on (rather than, of some other class) -- but this is a stylistic point that cannot be said to be sharply defined either way.

I'd code it (with some assumptions which I hope as clear, e.g. the gender is coded as M or F and if not specified is heuristically inferred from the name, &c):

def gender_from_name(name): ...

person_by_name = {}

class_by_gender = {'M': Man, 'F': Woman}

def person_factory(name, gender=None):
  p = person_by_name.get(name)
  if p is None:
    if gender is None:
      gender = gender_from_name(name)
    p = person_by_name[name] = class_by_gender[gender](name)
  return p
Alex Martelli
  • 854,459
  • 170
  • 1,222
  • 1,395
2

The place to put a "no two objects with same key" registration is in __new__, like this:

class Person(object):
    person_registry = {}
    mens_names = set('Tom Dick Harry'.split())
    womens_names = set('Mary Linda Susan'.split())
    gender = "?"
    def __new__(cls, *args):
        if cls is Person:
            fname,lname = args[0].split()
            key = (lname, fname)
            if key in Person.person_registry:
                return Person.person_registry[key]

            if fname in Person.mens_names:
                return Man(*args)
            if fname in Person.womens_names:
                return Woman(*args)
        else:
            return object.__new__(cls, *args)

    def __init__(self, name):
        fname,lname = name.split()
        Person.person_registry[(lname, fname)] = self

class Man(Person):
    gender = "M"

class Woman(Person):
    gender = "W"

p1 = Person("Harry Turtledove")
print p1.__class__.__name__, p1.gender

p2 = Person("Harry Turtledove")

print p1 is p2

prints:

Man M
True

I also took a stab at your Man/Woman distinction, but I'm not thrilled with it.

PaulMcG
  • 62,419
  • 16
  • 94
  • 130
  • 1
    Note that this runs `__init__` again even when `__new__` returns a pre-existing object. In this case it doesn't do any harm, but it easily could. Generally I find it cleanest to use either `__new__` *or* `__init__`, not both. – Ben Jan 12 '12 at 09:04
  • 1
    And in fact, when you call `Woman(*args)` that causes `__init__` to be called on the object, then you return it from `__new__` and `__init__` will be called *again*. Generally it's pretty unintuitive to have both `__new__` and `__init__` in the same class hierarchy. It's actually easier to do this sort of thing by overriding `__call__` in a *metaclass* than it is by using `__new__`, I've found. – Ben Jan 12 '12 at 09:18
2

Flyweight pattern in Python? (appears to be broken, Internet Archive version)

Edit: Another flyweight example in Python.

ShinNoNoir
  • 2,274
  • 1
  • 18
  • 24
0

The easiest way is probably to use builtin lru_cache from functools on __new__.

import functools

class Person:
    gender = 'unknown'
    @functools.lru_cache(maxsize=None)
    def __new__(cls, full_name):
        names = first, last = full_name.split()
        for subclass in cls.__subclasses__():
            if first in subclass.names:
                cls = subclass
        self = super().__new__(cls)
        self.first, self.last = names
        return self

class Woman(Person):
    gender = 'female'
    names = {*'Mary Linda Susan'.split()}

class Man(Person):
    gender = 'male'
    names = {*'Tom Dick Harry'.split()}
Veky
  • 2,646
  • 1
  • 21
  • 30