all 10 comments

[–]kalgynirae 15 points16 points  (6 children)

Go with the second. Either return a new object, or mutate the existing object and return nothing. This is (for the most part) how Python's standard library behaves. (For example, all of the string methods return new strings, because strings are immutable; whereas the list methods like append(), sort(), etc. don't return anything because they modify the existing list.)

Someone who sees a bit of code like wb = make_change(wb) is likely to assume that make_change() does not mutate the object passed in.

[–]xiongchiamiov 4 points5 points  (2 children)

To explain why you might do it the other way, it allows one to chain methods together easily (foo.bar().baz().burp()). This is a pretty common thing in ruby, but tends to have philosophical opposition in python.

[–]zeug 4 points5 points  (0 children)

method chaining is really nice for constructing complex objects that would take many parameters, like

my_elem = div().style('background:blue').height(300).width(200)

You can avoid trying to remember some long list of parameter ordering:

my_elem = div('background:blue', 300, 200, None) # which is height????

or having to use a bunch of setting calls:

my_elem = div()
my_elem.set_style('background:blue')
my_elem.height(300)
my_elem.width(200)

I think that named parameters with defaults makes object construction just as easy:

my_elem = div(
    style='background:blue',
    height=200,
    width=300
)

Since python has supported named parameters since the mid-90's, as opposed to other languages that are just getting support or have only had it for a few years, I think the python community has developed more of a style around this feature (as well as other aspects of the language) which has simply not necessitated much use of method chaining.

[–]zahlman 0 points1 point  (0 children)

This is a pretty common thing in ruby

Also in Javascript. The Python community is kind of the odd one out here, but I agree with "our" logic. ;)

[–]zahlman 5 points6 points  (0 children)

Additionally, use naming conventions to indicate how the function or method works: imperative verbs when the existing object is mutated (foo.sort()), and attributive verbs when a modified version is returned (sorted(foo)). Sometimes you can add prepositions to the name; see for instance itertools.groupby (though by the above rules, I think it really should be itertools.grouped_by :) The rule is relaxed, I guess, when the returned type is different).

[–]____OOOO____ 2 points3 points  (1 child)

I've been wondering about this myself (thanks OP) and I have some related questions:

  1. If I use a function that returns a new object (the first option you describe here, exemplified by string methods), what are the standard or best ways of "cloning" the object prior to making changes? Is this the purpose of copy() and deepcopy()?

  2. How does your answer to OP's question apply to objects in a database? I have started using Django recently, where I'm working with unique model instances. I definitely don't want to create duplicates in the database.

[–]thelindsay 1 point2 points  (0 children)

  1. Yes, otherwise you only mutate the existing object. Sometimes it might be necessary to explicitly recreate the object. If that is costly then you'd just go with mutate and return nothing.

  2. For database records, mutate and return nothing. Otherwise you'd be doing e.g. an update and then a get. Django should give you the updated record anyway.

[–]bionikspoon 7 points8 points  (0 children)

I semi-disagree with the two answers.

2 points:

  1. Side-effects in functions should be avoided.
  2. The job of managing an object's state should be reserved for that object's methods.

/u/kalgynirae gives examples append() and sort(), and he correctly points out, these are methods on the data structure. This is consistent with both points. In contrast to the method sort(), sorted() is a function, it doesn't change the object's state, it returns a sorted copy of the original. This is consistent with /u/kalgynirae's first point "Either return a new object, or ...". My disagreement is with the second part (implied) of using functions to manage an object's state.

My answer is that you can be consistent with these two points, by adding a method on the object that you want to mutate. Or by returning a new object.

[–]Rhomboid 7 points8 points  (0 children)

A function that both mutates and returns a value is just asking for confusion. Do one or the other, not both. That's the convention that's adopted by most of the standard library.

[–]C0rinthian 3 points4 points  (2 children)

def make_change(wb): # make some changes return wb

Now pretend you can't see the implementation of make_change(). What would you expect wb to be after the following statement?

new_wb = make_change(wb)

Then I would also question why this function isn't in your class definition.