85

In Java, you can use the builder pattern to provide a more readable means to instantiating a class with many parameters. In the builder pattern, one constructs a configuration object with methods to set named attributes, and then uses it to construct another object.

What is the equivalent in Python? Is the best way to mimic the same implementation?

Community
  • 1
  • 1
name_masked
  • 9,544
  • 41
  • 118
  • 172
  • Maybe I'm missing something, but how is this an advantage over the standard instantiation technique: `myInstance = myClass(carbs=10, sodium=100)`? – Joel Cornett Aug 15 '12 at 21:07
  • 5
    As a rule of thumb, transplanting an implementation from Java to Python will usually result in un-idiomatic code. And vice versa. Why do you ask? –  Aug 15 '12 at 21:07
  • @JoelCornett: Are you indirectly asking me what a Builder pattern is ? – name_masked Aug 15 '12 at 21:11
  • 4
    @delnan: Well, I wanted to have a more readable "means" to instantiating a class with many parameters. Hence the question – name_masked Aug 15 '12 at 21:13
  • 2
    @delnan: Isn't asking for a Builder class implementation mean exactly what I wrote in comments !! (and that is not a question). Also, reading up the XY trap article, I doubt this question is applicable. I have posted a more broader question instead of specifying my implementation (attempted solution) or issue specific to any example. – name_masked Aug 15 '12 at 21:23
  • 1
    For classes that are essentially immutable structs, use namedtuple (imported from collections). – PaulMcG Aug 15 '12 at 21:26
  • 1
    The Builder pattern is a specific approach to change (improve) object construction by factoring out the construction into a dedicated entity (object). More or less. There is a myriad of ways to construct objects (e.g. factories, or just calling the darn constructor), the builder pattern is only one of those -- and quite possibly not the best. **You want to construct an object, so ask how to construct an object.** Nobody implements the builder pattern for the sake of implementing the builder pattern. –  Aug 15 '12 at 21:26
  • 2
    @delnan: So you would like that I should explicitly mention "how to construct an object" in my article? If that's the case, I am sorry but I see no intent to make the change (no disrespect please). Anybody with a clear knowledge of Builder pattern in Java would know what the question means. You can refer to: http://stackoverflow.com/questions/328496/when-would-you-use-the-builder-pattern to update yourself with the key feature of Builder pattern i.e. building an object with many parameters. – name_masked Aug 15 '12 at 21:35
  • @darkie15: Well, I assume that [this is a definition of a builder pattern](http://en.wikipedia.org/wiki/Builder_pattern). I just don't see it being necessary in Python, given that you can quite easily overwrite `__new__()` and `__init__()` with the necessary arguments to construct whatever it is you need. Is there something specific that you're trying to achieve? Using a builder pattern seems like a roundabout way to do something that can be done quite directly in Python. – Joel Cornett Aug 15 '12 at 21:51
  • @PaulMcGuire: Good suggestion. I added it to my answer (with attribution). – Mechanical snail Aug 15 '12 at 22:10
  • 1
    @delnan, JoelCornett: Okay, the poster probably shouldn't implement the builder pattern, but it's still a perfectly well-defined question. – Mechanical snail Aug 15 '12 at 22:18
  • @Mechanicalsnail Of course it is a well-defined question, but it appears that it's not the question OP meant to ask. –  Aug 15 '12 at 22:20
  • @delnan: I edited your question, adding in your use-case you mentioned in a comment. – Mechanical snail Aug 15 '12 at 22:29

7 Answers7

135

Design patterns can often be replaced with built-in language features.

Your use case

You say "I wanted to have a more readable "means" to instantiating a class with many parameters.". In Java's case:

[A] use case for the builder pattern is when the constructor of the object to be built must take very many parameters. In such cases, it is often more convenient to lump such configuration parameters in a builder object (setMaxTemperature(int t), setMinTemperature(int t), set.. , etc. ) than to burden the caller with a long list of arguments to pass in the class's constructor..

Builder pattern not needed

But Python supports named parameters, so this is not necessary. You can just define a class's constructor:

class SomeClass(object):
    def __init__(self, foo="default foo", bar="default bar", baz="default baz"):
        # do something

and call it using named parameters:

s = SomeClass(bar=1, foo=0)

Note that you can freely reorder and omit arguments, just as with a builder in Java you can omit or reorder calls to the set methods on the builder object.

Also worth stating is that Python's dynamic nature gives you more freedom over construction of objects (using __new__ etc.), which can replace other uses of the builder pattern.

But if you really want to use it

you can use collections.namedtuple as your config object. namedtuple() returns a new type representing a tuple, each of whose parameters has a given name, without having to write a boilerplate class. You can use objects of the resulting type in a similar way to Java builders. (Thanks to Paul McGuire for suggesting this.)

StringBuilder

A related pattern is Java's StringBuilder, which is used to efficiently construct an (immutable) String in stages. In Python, this can be replaced with str.join. For example:

final StringBuilder sb = new StringBuilder();
for(int i = 0; i < 100; i++)
    sb.append("Hello(" + i + ")");
return sb.toString();

can be replaced with

return "".join(f"Hello({i})" for i in range(100))
Lee Netherton
  • 21,347
  • 12
  • 68
  • 102
Mechanical snail
  • 29,755
  • 14
  • 88
  • 113
  • 4
    +1 for an answer that is likely to help others in the future. – Joel Cornett Aug 16 '12 at 00:12
  • 6
    @mechanical-snail This may just be my inexperience speaking, but how do I use those "easy, pythonic, don't-need-no-builders" solutions for something like a StringBuilder, whose methods can be invoked multiple times, order being important, before returning the finished immutable product? – kaay Jul 03 '13 at 07:18
  • Yah, I agree. I found this answer while looking for one of those complicated builders. – Jason S Oct 15 '13 at 22:40
  • 1
    @kaay: There's `"".join(iterable)`. Example in edit. – Mechanical snail Nov 14 '13 at 21:01
  • 2
    As a note, building up a list (or other data structure - often with list comprehensions or generators), and then passing the iterable into your constructor is the preferred way of doing any kind of equivalent to a builder that has that kind of behaviour. – Gareth Latty Oct 04 '14 at 13:24
58

The OP set themselves up for a fall by casting the Builder pattern as Java specific. It's not. It's in the Gang of Four's book and is potentially relevant to any object oriented language.

Unfortunately, even the Wikipedia article on the Builder pattern doesn't give it enough credit. It's not simply useful for code elegance. Builder patterns are a great way to create immutable objects that need to be mutable until they're used. Immutable state is especially critical in functional paradigms, making the Builder an excellent object-oriented pattern for python.

I've provided an an example Builder + ImmutableObject implementation below using the collections.namedtuple, borrowed and modified from "How to make an immutable object in python". I've kept the Builder fairly simple. However, setter functions could be provided that return the Builder itself to allow call chaining. Or @property syntax could be used in the Builder to provide attribute setters that check attribute validity prior to setting.

from collections import namedtuple

IMMUTABLE_OBJECT_FIELDS = ['required_function_result', 'required_parameter', 'default_parameter']

class ImmutableObjectBuilder(object):
    def __init__(self, required_function, required_parameter, default_parameter="foo"):
        self.required_function = required_function
        self.required_parameter = required_parameter
        self.default_parameter = default_parameter

    def build(self):
        return ImmutableObject(self.required_function(self.required_parameter),
                               self.required_parameter,
                               self.default_parameter)

class ImmutableObject(namedtuple('ImmutableObject', IMMUTABLE_OBJECT_FIELDS)):
    __slots__ = ()

    @property
    def foo_property(self):
        return self.required_function_result + self.required_parameter

    def foo_function(self):
        return self.required_function_result - self.required_parameter

    def __str__(self):
        return str(self.__dict__)

Example usage:

my_builder = ImmutableObjectBuilder(lambda x: x+1, 2)
obj1 = my_builder.build()
my_builder.default_parameter = "bar"
my_builder.required_parameter = 1
obj2 = my_builder.build()
my_builder.required_function = lambda x: x-1
obj3 = my_builder.build()

print obj1
# prints "OrderedDict([('required_function_result', 3), ('required_parameter', 2), ('default_parameter', 'foo')])"
print obj1.required_function_result
# prints 3
print obj1.foo_property
# prints 5
print obj1.foo_function()
# prints 1
print obj2
# prints "OrderedDict([('required_function_result', 2), ('required_parameter', 1), ('default_parameter', 'bar')])"
print obj3
# prints "OrderedDict([('required_function_result', 0), ('required_parameter', 1), ('default_parameter', 'bar')])"

In this example, I created three ImmutableObjects, all with different parameters. I've given the caller the ability to copy, modify, and pass around a mutable configuration in the form of the builder while still guaranteeing immutability of the built objects. Setting and deleting attributes on the ImmutableObjects will raise errors.

Bottom line: Builders are a great way to pass around something with mutable state that provides an object with immutable state when you're ready to use it. Or, stated differently, Builders are a great way to provide attribute setters while still ensuring immutable state. This is especially valuable in functional paradigms.

Community
  • 1
  • 1
Malina Kirn
  • 2,073
  • 1
  • 21
  • 19
  • 3
    It's worth noting that there is no such thing as an immutable object in Python (outside of some built-in types) - you can do so by convention, but not enforce it. Setters are a convention that isn't used in Python (as properties exist), and anyone could just edit the attribute directly. There isn't really any benefit to this all over setting the attributes on the 'immutable' object. – Gareth Latty Oct 04 '14 at 13:27
  • 9
    Yes, immutable objects don't really exist in Python (actually, they don't exist in Java either: you can always grab onto private members and change them via reflection). But at least the convention lets you know that you should expect Bad Things(TM) if you do change the state. Regarding the setter syntax: it's less pythonic, but more consistent with the formal Builder pattern. By returning the builder, you get call-chaining (callers don't have to keep typing my_builder.), which becomes convenient when you have many parameters to set. I think either style is reasonable. – Malina Kirn Oct 04 '14 at 14:48
  • 3
    I get what you are saying, but there isn't any value in following the builder pattern in Python - there are other language features that do the same job in a better (more readable) way. I also don't really see how the pattern makes it any less likely I just directly assign something to one of the internal values (unlike in Java, where you would have to work through reflection to access it, and it would be obvious that you are circumventing the intent of the original programmer). – Gareth Latty Oct 21 '14 at 15:25
  • 5
    I modified the implementation to an actually immutable object (using tuple). I simplified the Builder while pointing out other options available for implementing the Builder. My original implementation muddied the point: Builders are a nice way to work with something mutable that turns into something immutable. Hopefully this implementation is more clear and clean. – Malina Kirn Nov 07 '14 at 00:13
  • 15
    For everyone discounting the value of a builder in Python... look into the best examples before assuming from other answer & comments that the builder pattern is just Java fluff. It isn't about Java. The builder pattern is about separating (1) the complex construction of an object which usually includes enforcing constraints from (2) the later usage of the object. This way the object you want to build isn't later polluted with the methods needed to build. Also, the resulting object doesn't have to be immutable. – Core May 14 '16 at 17:46
33

I disagree with @MechanicalSnail. I think a builder implementation similar to one referenced by the poster is still very useful in some cases. Named parameters will only allow you to simply set member variables. If you want to do something slightly more complicated, you're out of luck. In my example I use the classic builder pattern to create an array.

class Row_Builder(object):
  def __init__(self):
    self.row = ['' for i in range(170)]

  def with_fy(self, fiscal_year):
    self.row[FISCAL_YEAR] = fiscal_year
    return self

  def with_id(self, batch_id):
    self.row[BATCH_ID] = batch_id
    return self

  def build(self):
    return self.row

Using it:

row_FY13_888 = Row_Builder().with_fy('FY13').with_id('888').build()
CowboyBebop
  • 701
  • 7
  • 10
  • 4
    That's simply not true - named parameters are just passed into the constructor - you can do whatever you want with them inside the constructor, not just set attributes. – Gareth Latty Oct 04 '14 at 13:20
  • 2
    That's right. I should have written, "named parameters will only allow you to pass parameters into the constructor. – CowboyBebop Oct 21 '14 at 15:16
  • 4
    Right, except you can do anything you want in the constructor, so your assertion that *'If you want to do something slightly more complicated, you're out of luck'* isn't valid. You can do whatever you want with those named arguments, just as much as you can with the builder, except more pythonically. – Gareth Latty Oct 21 '14 at 15:21
  • 12
    This is long past, but for future people reading this - the arguments are actually passed to `__init__` which is the initializer, not the constructor. The constructor in python is the magic classmethod `__new__`, which you much less commonly customize. In any case, yes a builder can be helpful and I am mostly commenting on semantics. – nerdwaller Dec 15 '15 at 19:48
  • 1
    Putting heavy business logic in the constructor is an anti-pattern – Fermi-4 Jun 26 '23 at 15:53
16

I have just come across the need for building this pattern and stumbled across this question. I realize how old this question is but might as well add my version of the builder pattern in case it is useful to other people.

I believe using a decorator to specify builder classes is the most ergonomic way to implement the builder pattern in python.

def buildermethod(func):
  def wrapper(self, *args, **kwargs):
    func(self, *args, **kwargs)
    return self
  return wrapper

class A:
  def __init__(self):
    self.x = 0
    self.y = 0

  @buildermethod
  def set_x(self, x):
    self.x = x

  @buildermethod
  def set_y(self, y):
    self.y = y

a = A().set_x(1).set_y(2)
M Burn
  • 161
  • 2
  • 2
5

builder and constructor are not the same thing, builder is a concept, constructor is a programming syntax. There is no point to compare the two.

So sure you can implement the builder pattern with a constructor, a class method or a specialized class, there is no conflict, use whichever one suit your case.

Conceptually, builder pattern decouple the building process from the final object. Take a real world example of building a house. A builder may use a lot of tools and materials to build a house, but the final house need not have those tools and excess materials lying around after it is build.

Example:

woodboards = Stores.buy(100)
bricks = Stores.buy(200)
drills = BuilderOffice.borrow(4)

house = HouseBuilder.drills(drills).woodboards(woodboards).bricks(bricks).build()
4

The builder pattern in Java can easily be achieved in python by using a variant of:

MyClass(self, required=True, someNumber=<default>, *args, **kwargs)

where required and someNumber are an example to show required params with a default value and then reading for variable arguments while handling the case where there might be None

In case you have not used variable arguments before, refer this

Community
  • 1
  • 1
Pratik Mandrekar
  • 9,362
  • 4
  • 45
  • 65
0

I implement the Builder pattern by adding a Method chaining, and in most cases I get easy-reading code like that:

pizza = (PizzaBuilder("Margherita")
            # add ingredients
            .set_dough("cross")
            .set_sauce("tomato")
            .set_topping("mozzarella")
            # cook
            .build())

To do this, I use the following template:

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Pizza:
    """Pizza class."""

    name: str
    dough: str
    sauce: str
    topping: str


class BasePizzaBuilder(ABC):
    """
    Base class for Pizza Builder. 
    Need for more accurate typing.
    """

    @abstractmethod
    def set_dough(self, dough: str) -> Any:
        raise NotImplementedError()

    @abstractmethod
    def set_sauce(self, sauce: str) -> Any:
        raise NotImplementedError()

    @abstractmethod
    def set_topping(self, topping: str) -> Any:
        raise NotImplementedError()


class PizzaBuilder(BasePizzaBuilder):

    def __init__(self, pizza_name: str) -> None:
        self.pizza_name = pizza_name

    def set_dough(self, dough: str) -> BasePizzaBuilder:
        """Set dough."""
        self._dough = dough
        return self
    
    def set_sauce(self, sauce: str) -> BasePizzaBuilder:
        """Set sauce."""
        self._sauce = sauce
        return self
    
    def set_topping(self, topping: str) -> BasePizzaBuilder:
        """Set topping."""
        self._topping = topping
        return self
    
    def build(self) -> Pizza:
        """Cook pizza."""
        return Pizza(
            name=self.pizza_name, 
            dough=self._dough, 
            sauce=self._sauce, 
            topping=self._topping
        )
codez0mb1e
  • 706
  • 6
  • 17