[Tutor] method, type?

Steven D'Aprano steve at pearwood.info
Wed Jan 6 05:49:29 EST 2016


On Tue, Jan 05, 2016 at 08:58:42PM -0800, Alex Kleider wrote:

> class JournalLineItem(object):
>     def __init__(self, account, debit_or_credit, amount):
>         self.account = account
>         self.debit_or_credit = debit_or_credit
>         self.amount = float(amount)
>     def show(self):
>         return ("ACNT: {}  ${:0.2f} {}".
>             format(self.account, self.amount, self.debit_or_credit))
>     def get_line_item(text):
>         return JournalLineItem(*text.split())
> 
> def test():
>     print(
>     JournalLineItem.get_line_item("2435 Dr 25.33").show())


> What kind of a method/function is get_line_item?

In Python 3, it's a regular function. In Python 2, it's a broken method 
that won't work.

Some background information: methods and functions are different 
kinds of things, but methods are constructed from functions as 
needed. So when you define a method inside a class:

class Spam:
    def method(self, arg): ...

you're actually defining an ordinary function object. All the magic 
takes place when you go to use it. The rules changed slightly in Python 
3, so I'll start with Python 2.

Normal use is to create an instance, then call the method:

instance = Spam()
instance.method(x)

At this point, calling instance.method extracts the function object out 
of the class, converts it into a "bound method", and calls that method. 

("Bound" in this context only means that the method knows what instance 
it will get as self.)

If you extract the method from the class instead, you get an "unbound 
method" which means it doesn't have an instance applied to it, so you 
have to provide one yourself:

Spam.method(instance, x)


We can see that the class actually stores a regular function object, 
which is then automatically converted into methods as required:

py> Spam.__dict__['method']  
<function method at 0xb7c0f79c>
py> Spam().method
<bound method Spam.method of <__main__.Spam instance at 0xb7c0e22c>>
py> Spam.method
<unbound method Spam.method>

The magic that makes this work is called the Descriptor protocol, and it 
is responsible for all sorts of goodies, like properties, staticmethods, 
classmethods, and more.

So, that was the situation in Python 2. When Python 3 came about, people 
realised that there actually isn't anything special about unbound 
methods. They're just functions, and so in Python 3 extracting a method 
off the class just returns the regular function, with no changes made:

py> Spam.__dict__['method']
<function Spam.method at 0xb7ade14c>
py> Spam().method
<bound method Spam.method of <__main__.Spam object at 0xb7b5a86c>>
py> Spam.method
<function Spam.method at 0xb7ade14c>


Your JournalLineItem class takes advantage of that fact. When you go to 
use the get_line_item "method", you extract it from the class:

    JournalLineItem.get_line_item

which returns an ordinary function, as if it were defined outside of a 
class. You then provide an argument:

    JournalLineItem.get_line_item("2435 Dr 25.33")

which gets assigned to the parameter "text" and various things happen.

Although this is legal code, it should be avoided because:

(1) It is confusing and weird.

(2) If you try calling the method from an instance, bad things happen:

py> x = JournalLineItem("account", True, 100)
py> x.get_line_item("2435 Dr 25.33")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: get_line_item() takes 1 positional argument but 2 were given


This is how get_line_item should be written:

    @classmethod
    def get_line_item(cls, text):
        return cls(*text.split())

which also has the advantage that if you subclass JournalLineItem, the 
method will continue to work correctly.


> From what I've read (and not fully understood)
> static methods and class methods must have
> @staticmethod and @classmethod on the line above them.

In simple terms, correct.

To be technical, not quite, there are other, less convenient but 
sometimes useful, ways to create static and classmethods, but you don't 
need to worry about those if you don't want. (I'll explain if you ask.)


> get_line_item works as I wanted but it's clearly not the
> usual type of method and I don't know how to categorize it.
> It's an instance creator- is there a better term?
> Is this 'Pythonic' code?

The usual term for this is "alternate constructor", or just 
"constructor". It constructs an instance of the class, you see, but it 
is not the standard one, __init__.

(To be precise, __init__ is the initiator, __new__ is the actual 
constructor, but most of the time you don't write __new__, and it calls 
__init__ by default.)

Is it Pythonic? As written, hell no! It's a mess! (No offense intended.) 
It confused me, for a while, I really thought it wouldn't work at all 
and was somewhat surprised to see that it did actually work. (Once I saw 
that it worked, in hindsight it was obvious why it worked.)

But the principle is sound. For example, dicts have an alternate 
constructor method:

    dict.fromkeys

which creates a new dict from a collection of keys.

So I recommend you re-write the method to the version I suggested, and 
then you can happily use it secure in the knowledge that not only does 
it work, but it works in a Pythonic way.




-- 
Steve


More information about the Tutor mailing list