object.enable() anti-pattern

Discussion in 'Python' started by Steven D'Aprano, May 8, 2013.

  1. I'm looking for some help in finding a term, it's not Python-specific but
    does apply to some Python code.

    This is an anti-pattern to avoid. The idea is that creating a resource
    ought to be the same as "turning it on", or enabling it, or similar. For
    example, we don't do this in Python:


    f = file("some_file.txt")
    f.open()
    data = f.read()


    because reading the file can fail if you forget to call open first.
    Instead, Python uses a factory function that creates the file object and
    opens it:

    f = open("some_file.txt") # if this succeeds, f is ready to use
    data = f.read()


    Basically, any time you have two steps required for using an object, e.g.
    like this:

    obj = someobject(arg1, arg2)
    obj.enable()

    you should move the make-it-work functionality out of the enable method
    and into __init__ so that creating the object creates it in a state ready
    to work.

    I read a blog some time ago (a year ago?) that discusses this anti-
    pattern, and I'm pretty sure gave it a specific name, but I cannot find
    it again. Can anyone find any references to this anti-pattern? My google-
    fu is letting me down.



    (For what it's worth, I'm refactoring some of my own code that falls into
    this anti-pattern. Die, enable method, die die die!)
     
    Steven D'Aprano, May 8, 2013
    #1
    1. Advertisements

  2. Steven D'Aprano

    Robert Kern Guest

    I don't think the anti-pattern has a name, but it's opposite pattern is named:

    http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

    --
    Robert Kern

    "I have come to believe that the whole world is an enigma, a harmless enigma
    that is made terrible by our own mad attempt to interpret it as though it had
    an underlying truth."
    -- Umberto Eco
     
    Robert Kern, May 8, 2013
    #2
    1. Advertisements

  3. That sounds like it should be related, but it actually isn't since RAII
    actually has little to do with *acquiring* the resource and everything to
    do with *releasing* it. See, for example:

    http://stackoverflow.com/questions/2321511/what-is-meant-by-resource-
    acquisition-is-initialization-raii

    where it is pointed out that the resource may be acquired outside of the
    object constructor and passed in as an argument. RAII is actually about
    deterministic destruction of objects and the release of their resources.

    But thanks for the attempt :)
     
    Steven D'Aprano, May 8, 2013
    #3
  4. Steven D'Aprano

    Roy Smith Guest

    I've worked with C++ code that did this. At one point in the evolution
    of OOP group consciousness, there was a feeling that constructors must
    never fail. I don't remember if it was a general language-agnostic
    pattern, or a specific C++ reaction to poor exception handling code in
    early compilers. What came out of that was the pattern you describe.
    All the code that could fail was factored out of the constructor into an
    "enable" method.

    That being said, sometimes there are good reasons for doing this. One
    example might be something like:

    frobnicator = Frobnicator()
    for file in my_file_list:
    frobnicator.munch(file)
    for line in frobnicator:
    process(line)

    If creating a Frobnicator instance is very expensive, it might pay to
    create an instance once and keep reusing it on multiple files. Here,
    munch() is your enable() method. But, that's not quite what you were
    talking about.
     
    Roy Smith, May 8, 2013
    #4
  5. Steven D'Aprano

    Mark Janssen Guest

    I'm looking for some help in finding a term, it's not Python-specific but
    I would call it "user-mediated resource allocation" as distinct from
    "object-mediated" resource allocation.
     
    Mark Janssen, May 9, 2013
    #5
  6. Steven D'Aprano

    Mark Janssen Guest

    This is an anti-pattern to avoid. The idea is that creating a resource
    I should point out, though, it's not really an anti-pattern (coming
    form the C community).
     
    Mark Janssen, May 9, 2013
    #6


  7. Thanks for the link. It's obviously not the blog post I was looking for,
    but it is interesting.
     
    Steven D'Aprano, May 9, 2013
    #7
  8. Because there is a lot more that you might want to do to a file than just
    read from it.

    py> f = open('/tmp/x')
    py> dir(f)
    ['_CHUNK_SIZE', '__class__', '__delattr__', '__dict__', '__dir__',
    '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__',
    '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__',
    '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__',
    '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
    '__str__', '__subclasshook__', '_checkClosed', '_checkReadable',
    '_checkSeekable', '_checkWritable', 'buffer', 'close', 'closed',
    'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty',
    'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable',
    'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate',
    'writable', 'write', 'writelines']


    That's 24 public methods and attributes, excluding private and dunder
    attributes.

    There is no sensible use-case for creating a file without opening it.
    What would be the point? Any subsequent calls to just about any method
    will fail. Since you have to open the file after creating the file object
    anyway, why make them two different calls?

    Besides, this is not to denigrate the idea of a read() function that
    takes a filename and returns its contents. But that is not an object
    constructor. It may construct a file object internally, but it doesn't
    return the file object, so it is completely unrelated to the scenario I
    described.


    Well duh. That's like saying that for loops, while loops and co-routines
    are just GOTO in a socially acceptable and traditional wrapper. Of course
    they are. That's the whole point.
     
    Steven D'Aprano, May 9, 2013
    #8
  9. Whether it is synchronous or asynchronous is irrelevant.

    I can see use-cases for separating "make it go" from initialisation. It
    all depends on what you might want to do to the object before making it
    go. If the answer is "Nothing", then there is no reason not to have the
    constructor make it go. If the answer is, "Well, sometimes we need to do
    things to the object before making it go", then it makes sense to
    separate the two:

    blob = MyBlob(arg1, arg2, agr3)
    blob.poke("with a stick")
    blob.append(data)
    blob.foo = "spam"
    blob.make_it_go()


    I'm not talking about this case. I'm talking about the case where there's
    nothing you can do with the blob before making it go.

    "And that is why we always write code like this:

    n = int(int(len(something)) + int(1))

    just to be sure that the result is explicitly an int and not just
    implicitly an int. Suppose some Javascript programmer was reading the
    code, and they thought that 1 was a floating point value. That would be
    bad!"
     
    Steven D'Aprano, May 9, 2013
    #9
  10. Example use-case: Most GUI frameworks. You create a window, then
    populate it, then show it. When you create the window object in
    Python, you expect an actual window to exist, it should have its
    handle etc. So it's still the same thing; the object is fully created
    in its constructor.

    ChrisA
     
    Chris Angelico, May 9, 2013
    #10
  11. At least a few stdlib modules that define classes *also* have such
    functions. They create an instance of the class, call one or more
    methods, and return the result of the method, discarding the instance.
    For instance, see the subprocess module, its POpen class, and module
    functions; or the timeit module, its Timer class, and functions.
     
    Terry Jan Reedy, May 9, 2013
    #11
  12. Early unix systems often used this as a form of locking.
     
    Gregory Ewing, May 9, 2013
    #12
  13. | Steven D'Aprano wrote:
    | >There is no sensible use-case for creating a file without opening
    | >it. What would be the point?
    |
    | Early unix systems often used this as a form of locking.

    Not just early systems: it's a nice lightweight method of making a
    lockfile even today if you expect to work over NFS, where not that
    many things are synchronous. You open a file with "0" modes, so
    that it is _immediately_ not writable. Other attempts to make the
    lock file thus fail because of the lack of write, even over NFS.

    Cheers,
     
    Cameron Simpson, May 9, 2013
    #13
  14. Steven D'Aprano

    Wayne Werner Guest

    I'm not entirely sure what the name of it is, but the basic concept is
    that you should never partially create, or create a class that can be in
    an unstable state. Which isn't to say you should prevent invalid input,
    only that with every valid input or single operation (including
    construction) your class should be valid.


    Ah, that's it - the problem is that it introduces /Temporal Coupling/ to
    one's code: http://blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling/

    You don't ever want a class that has functions that need to be called in a
    certain order to *not* crash. That's fine if you have to call them in a
    certain sequence in order to get the correct data - that's what
    programming is all about, after all. But if you provide me a class with a
    constructor you better make sure that when I do this:

    thing = YourSuperAwesomeClass()
    thing.do_stuff()

    that I don't get some horrid stack trace ending with

    InvalidStateError: initialize() needs to be called before do_stuff()

    Or something worse.


    HTH,
    Wayne

    p.s. I'm interested in reading whatever is evenually written on the topic
     
    Wayne Werner, May 9, 2013
    #14
  15. [emphasis added]

    This is all very well and good, but for the life of me, I cannot see how
    opening a file is a good example of not opening a file. Perhaps it is a
    Zen thing, like the sound no spoon makes when you don't tap it against a
    glass that isn't there.
     
    Steven D'Aprano, May 9, 2013
    #15
  16. Good catch!

    That's not the blog post I read, but that's the same concept. "Temporal
    Coupling" -- yes, that is an excellent description of the problem.

    Exactly.


    Thank you Wayne, that's a great help.
     
    Steven D'Aprano, May 9, 2013
    #16
  17. Steven D'Aprano

    Roy Smith Guest

    Sure there is. Sometimes just creating the name in the file system is
    all you want to do. That's why, for example, the unix "touch" command
    exists.
     
    Roy Smith, May 9, 2013
    #17
  18. Wouldn't the code that implements the touch command just look
    something like this:

    f = open(filename)
    f.close()

    Or is there some other way of creating the file that doesn't open it
    (I mean in general not just in Python)?


    Oscar
     
    Oscar Benjamin, May 9, 2013
    #18
  19. Since I neglected to make it clear above that I was still talking about
    file objects, rather than files on disk, I take responsibility for this
    misunderstanding. I thought that since I kept talking about file
    *objects* and *constructors*, people would understand that I was talking
    about in-memory objects rather than on-disk files. Mea culpa.

    So, let me rephrase that sentence, and hopefully clear up any further
    misunderstandings.

    There is no sensible use-case for creating a file OBJECT unless it
    initially wraps an open file pointer.

    This principle doesn't just apply to OOP languages. The standard C I/O
    library doesn't support creating a file descriptor unless it is a file
    descriptor to an open file. open() has the semantics:

    "It shall create an open file description that refers to a file and a
    file descriptor that refers to that open file description."

    http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html

    and there is no corresponding function to create a *closed* file
    description. (Because such a thing would be pointless.)

    Of course language designers are free to design their language to work
    under whatever anti-patterns they desire. I quote from the Pascal
    Language Reference:

    "open associates the permanent file file [sic] with a file variable for
    reading or writing. open does not actually open the file; you must call
    reset or rewrite before reading or writing to that file."

    http://www.amath.unc.edu/sysadmin/DOC4.0/pascal/lang_ref/
    ref_builtin.doc.html


    but since that's not a part of standard Pascal, other Pascals may behave
    differently.
     
    Steven D'Aprano, May 9, 2013
    #19
  20. Steven D'Aprano

    MRAB Guest

    You might want to do this:

    f = File(path)
    if f.exists():
    ...

    This would be an alternative to:

    if os.path.exists(path):
    ...
    [snip]
     
    MRAB, May 9, 2013
    #20
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.