[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