36

Dead Simple Python: Classes

 5 years ago
source link: https://www.tuicool.com/articles/hit/u2iYj2u
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Classes and objects: the bread-and-butter of many a developer. Object-oriented programming is one of the mainstays of modern programming, so it shouldn't come as a surprise that Python is capable of it.

But if you've done object-oriented programming in any other language before coming to Python, I can almost guarantee you're doing it wrong.

Hang on to your paradigms, folks, it's going to be a bumpy ride.

Class Is In Session

Let's make a class, just to get our feet wet. Most of this won't come as a surprise to anyone.

class Starship(object):

    sound = "Vrrrrrrrrrrrrrrrrrrrrr"

    def __init__(self):
        self.engines = False
        self.engine_speed = 0
        self.shields = True

    def engage(self):
        self.engines = True

    def warp(self, factor):
        self.engine_speed = 2
        self.engine_speed *= factor

    @staticmethod
    def make_sound(cls):
        print(cls.sound)

Once that's declared, we can create a new instance , or object , from this class. All of the member functions and variables are accessible using dot notation .

uss_enterprise = Starship()
uss_enterprise.warp()
uss_enterprise.engage(4)
uss_enterprise.engines
>>> True
uss_enterprise.engine_speed
>>> 8
Wow, Jason, I knew this was supposed to be 'dead simple', but I think you just put me to sleep.

No surprises there, right? But look again, not at what is there, but what isn't there.

You can't see it? Okay, let's break this down. See if you can spot the surprises before I get to them.

Declaration

We start with the definition of the class itself:

class Starship(object):

Python might be considered one of the more truly object-oriented languages, on account of its design principle of "everything is an object." All other classes inherit from that object class.

Of course, most Pythonistas really hate boilerplate, so as of Python 3, we can also just say this and call it good:

class Starship:

Personally, considering The Zen of Python's line about "Explicit is better than implicit," I like the first way. We could debate it until the cows come home, really, so let's just make it clear that both approaches do the same thing in Python 3, and move on.

Legacy Note:If you intend your code to work on Python 2, you must say (object) .

Methods

I'm going to jump down to this line...

def engage(self, factor):

Obviously, that's a member function or method . In Python, we pass self as the first parameters to every single method. After that, we can have as many parameters as we want, the same as with any other function.

We actually don't have to call that first argument self ; it'll work the same regardless. But, we always use the name self there anyway, as a matter of style. There exists no valid reason to break that rule.

"But, but...you literally just broke the rule yourself! See that next function?"

@staticmethod
def make_sound(cls):

You may remember that in object-oriented programming, a static method is one that is shared between all instances of the class (objects). A static method never touches member variables or regular methods.

If you haven't already noticed, we always access member variables in a class via the dot operator: self. . So, to make it extra-super-clear we can't do that in a static method, we call the first argument cls . In fact, when a static method is called, Python passes the class to that argument, instead of the object .

As before, we can call cls anything we want, but that doesn't mean we should .

For a static method, we also MUST put the decorator @staticmethod on the line just above our function declaration. This tells the Python language that you're making a static method, and that you didn't just get creative with the name of the self argument.

Those methods above would get called something like this...

uss_enterprise = Starship() # Create our object from the starship class

# Note, we aren't passing anything to 'self'. Python does that implicitly.
uss_enterprise.warp(4)

# We can call static functions on the object, or directly on the class.
uss_enterprise.make_sound()
Starship.make_sound()

Those last two lines will both print out "Vrrrrrrrrrrrrrrrrrrrrr" the exact same way. (Note that I referred to cls.sound in that function.)

...

What?

Come on, you know you made sound effects for your imaginary spaceships when you were a kid. Don't judge me.

Initializers and Constructors

Every Python class needs to have one, and only one, __init__(self) function. This is called the initializer .

def __init__(self):
    self.engine_speed = 1
    self.shields = True
    self.engines = False

If you really don't need an initializer, it is technically valid to skip defining it, but that's pretty universally considered bad form. In the very least, define an empty one...

def __init__(self):
    pass

While we tend to use it the same way as we would a constructor in C++ and Java, __init__(self) is not a constructor! The initializer is responsible for initializing the instance variables , which we'll talk more about in a moment.

We rarely need to actually to define our own constructor . If you really know what you're doing, you can redefine the __new__(cls) function...

def __new__(cls):
    return object.__new__(cls)

By the way, if you're looking for the destructor , that's the __del__(self) function.

Variables

In Python, our classes can have instance variables , which are unique to our object (instance), and static variables , which belong to the class, and are shared between all instances.

I have a confession to make: I spent the first few years of Python development doing this absolutely and completely wrong! Coming from other object-oriented languages, I actually thought I was supposed to do this:

class Starship(object):

    engines = False
    engine_speed = 0
    shields = True

    def __init__(self):
        self.engines = False
        self.engine_speed = 0
        self.shields = True

    def engage(self):
        self.engines = True

    def warp(self, factor):
        self.engine_speed = 2
        self.engine_speed *= factor

The code works , so what's wrong with this picture? Read it again, and see if you can figure out what's happening.

Final Jeopardy music plays

Maybe this will make it obvious.

uss_enterprise = Starship()
uss_enterprise.warp(4)

print(uss_enterprise.engine_speed)
>>> 8
print(Starship.engine_speed)
>>> 0

Did you spot it?

Static variables are declared outside of all functions, usually at the top. Instance variables, on the other hand, are declared in the __init__(self) function: for example, self.engine_speed = 0 .

So, in our little example, we've declared a set of static variables, and a set of instance variables, with the same names. When accessing a variable on the object, the instance variables shadow (hide) the static variables, making it behave as we might expect. However, we can see by printing Starship.engine_speed that we have a separate static variable sitting in the class, just taking up space. Talk about redundant.

Anyone get that right? Sloan did, and wagered...ten thousand cecropia leaves. Looks like the sloth is in the lead. Amazingly enough.

By the way, you can declare instance variables for the first time from within any instance method, instead of the initializer. However...you guessed it: don't . The convention is to ALWAYS declare all your instance variables in the initializer, just to prevent something weird from happening, like a function attempting to access a variable that doesn't yet exist.

Scope: Private and Public

If you come from another object-oriented language, such as Java and C++, you're also probably in the habit of thinking about scope (private, protected, public) and its traditional assumptions: variables should be private, and functions should (usually) be public. Getters and setters rule the day!

I'm also an expert in C++ object-oriented programming, and I have to say that I consider Python's approach to the issue of scope to be vastly superior to the typical object-oriented scope rules. Once you grasp how to design classes in Python, the principles will probably leak into your standard practice in other languages...and I firmly believe that's a good thing.

Ready for this? Your variables don't actually need to be private.

Yes, I just heard the gurgling scream of the Java nerd in the back. "But...but...how will I keep developers from just tampering with any of the object's instance variables?"

Often, that concern is built on three flawed assumptions. Let's set those right first:

  • The developer using your class almost certainly isn't in the habit of modifying member variables directly, any more than they're in the habit of sticking a fork in a toaster.

  • If they do stick a fork in the toaster, proverbially speaking, the consequences are on them for being idiots, not on you.

  • As my Freenode #python friend grym once said, "if you know why you aren't supposed to remove stuck toast from the toaster with a metal object, you're allowed to do so."

In other words, the developer who is using your class probably knows better than you do about whether they should twiddle the instance variables or not.

Now, with that out of the way, we approach an important premise in Python: there is no actual 'private' scope . We can't just stick a fancy little keyword in front of a variable to make it private.

What we can do is stick an underscore at the front of the name, like this: self._engine .

That underscore isn't magical. It's just a warning label to anyone using your class: "I recommend you don't mess with this. I'm doing something special with it."

Now, before you go sticking _ at the start of all your instance variable names, think about what the variable actually is , and how you use it. Will directly tweaking it really cause problems? In the case of our example class, as it's written right now, no. This actually would be perfectly acceptable:

uss_enterprise.engine_speed = 6
uss_enterprise.engage()

Also, notice something beautiful about that? We didn't write a single getter or setter! In any language, if a getter or setter are functionally identical to modifying the variable directly, they're an absolute waste. That philosophy is one of the reasons Python is such a clean language.

You can also use this naming convention with methods you don't intend to be used outside of the class.

Side Note:Before you run off and go eschew private and protected from your Java and C++ code, please understand that there's a time and a place for scope. The underscore convention is a social contract among Python developers, and most languages don't have anything like that. So, if you're in a language with scope, use private or protected on any variable you would have put an underscore in front of in Python.

Private...Sort Of

Now, on a very rare occasion, you may have an instance variable which absolutely, positively, never, ever should be directly modified outside of the class. In that case, you may precede the name of the variable with two underscores ( __ ), instead of one.

This doesn't actually make it private; rather, it performs something called name mangling : it changes the name of the variable, adding a single underscore and the name of the class on the front.

In the case of class Starship , if we were to change self.shields to self.__shields , it would be name mangled to self._Starship__shields .

So, if you know how that name mangling works, you can still access it...

uss_enterprise = Starship()
uss_enterprise._Starship__shields
>>> True

It's important to note, you also cannot have more than one trailing underscore if this is to work. ( __foo and __foo_ will be mangled, but __foo__ will not). But then, PEP 8 generally discourages trailing underscores , so it's kinda a moot point.

By the way, the purpose of the double underscore ( __ ) name mangling actually has nothing to do with private scope; it's all about preventing name conflicts with some technical scenarios. In fact, you'll probably get a few serious frowns from Python ninjas for employing __ at all, so use it sparingly.

Properties

As I said earlier, getters and setters are usually pointless. On occasion, however, they have a purpose. In Python, we can use properties in this manner, as well as to pull off some pretty nifty tricks!

Properties are defined simply by preceding a method with @property .

My favorite trick with properties is to make a method look like an instance variable...

class Starship(object):

    def __init__(self):
        self.engines = True
        self.engine_speed = 0
        self.shields = True

    @property
    def engine_strain(self):
        if not self.engines:
            return 0
        else if self.shields:
            # Imagine shields double the engine strain
            return self.engine_speed * 2
        # Otherwise, the engine strain is the same as the speed
        return self.engine_speed

When we're using this class, we can treat engine_strain as an instance variable of the object.

uss_enterprise = Starship()
uss_enterprise.engine_strain
>>> 0

Beautiful, isn't it?

(Un)fortunately, we cannot modify engine_strain in the same manner.

uss_enterprise.engine_strain = 10
>>> Traceback (most recent call last):
>>>   File "<stdin>", line 1, in <module>
>>> AttributeError: can't set attribute

In this case, that actually does make sense, but it might not be what you're wanting other times. Just for fun, let's define a setter for our property too; at least one with nicer output than that scary error.

@engine_strain.setter
def engine_strain(self, value):
    print("I'm giving her all she's got, Captain!")

We precede our method with the decorator @NAME_OF_PROPERTY.setter . We also have to accept a single value argument (after self , of course), and positively nothing beyond that. You'll notice we're not actually doing anything with the value argument in this case, and that's fine for our example.

uss_enterprise.engine_strain = 10
>>> I'm giving her all she's got, Captain!

That's much better.

As I mentioned earlier, we can use these as getters and setters for our instance variables. Here's a quick example of how:

class Starship:
    def __init__(self):
        # snip
        self._captain = "Jean-Luc Picard"

    @property
    def captain(self):
        return self._captain

    @captain.setter
    def captain(self, value):
        print("What do you think this is, " + value + ", the USS Pegasus? Back to work!")

We simply preceded the variable these functions concern with an underscore, to indicate to others that we intend to manage the variable ourselves. The getter is pretty dull and obvious, and is only needed to provide expected behavior. The setter is where things are interesting: we knock down any attempted mutinies. There will be no changing this captain!

uss_enterprise = Starship()
uss_enterprise.captain
>>> 'Jean-Luc Picard'
uss_enterprise.captain = "Wesley"
>>> What do you think this is, Wesley, the USS Pegasus? Back to work!

Technical rabbit trail:if you want to create static properties, that requires some hacking on your part. There are several solutions floating around the net, so if you need this, go research it!

A few of the Python nerds will be on me if I didn't point out, there is another way to create a property without the use of decorators. So, just for the record, this works too...

class Starship:
    def __init__(self):
        # snip
        self._captain = "Jean-Luc Picard"

    def get_captain(self):
        return self._captain

    def set_captain(self, value):
        print("What do you think this is, " + value + ", the USS Pegasus? Back to work!")

    captain = property(get_captain, set_captain)

(Yes, that last line exists outside of any function.)

As usual, the documentation on properties has additional information, and some more nifty tricks with properties.

Inheritance

Finally, we come back to that first line for another look.

class Starship(object):

Remember why that (object) is there? We're inheriting from Python's object class. Ahh, inheritance! That's where it belongs.

class USSDiscovery(Starship):

    def __init__(self):
        super().__init__()
        self.spore_drive = True
        self._captain = "Jason Isaacs"

The only real mystery here is that super().__init__() line. In short, super() refers to the class we inherited from (in this case, Starship ), and calls its initializer. We need to call this, so USSDiscovery has all the same instance variables as Starship .

Of course, we can define new instance variables ( self.spore_drive ), and redefine inherited ones ( self._captain ).

We could have actually just called that initializer with Starship.__init__() , but then if we wanted to change what we inherit from, we'd have to change that line too. The super().__init__() approach is ultimately just cleaner and more maintainable.

Legacy Note:By the way, if you're using Python 2, that line is a little uglier: super(USSDiscovery, self).__init__() .

Before you ask: YES, you can do multiple inheritance with class C(A, B): . It actually works better than in most languages! Regardless, but you can count on a side order of headaches , especially when using super() .

Hold the Classes!

As you can see, Python classes are a little different from other languages, but once you're used to them, they're actually a bit easier to work with.

But if you've coded in class-heavy languages like C++ or Java, and are working on the assumption that you need classes in Python, I have a surprise for you. You really aren't required to use classes at all!

Classes and objects have exactly one purpose in Python: data encapsulation . If you need to keep data and the functions for manipulating it together in a handy unit, classes are the way to go. Otherwise, don't bother! There's absolutely nothing wrong with a module composed entirely of functions.

Review

Whew! You still with me? How many of those surprises about classes in Python did you guess?

Let's review...

  • The __init__(self) function is the initializer , and that's where we do all of our variable initialization.

  • Methods(member functions) must take self as their first argument.

  • Static methodsmust take cls as their first argument, and have the decorator @staticmethod on the line just above the function definition.

  • Instance variables(member variables) should be declared inside __init__(self) first. We don't declare them outside of the constructor, unlike most other object-oriented languages.

  • Static variablesare declared outside of any function, and are shared between all instances of the class.

  • There are no private members in Python! Precede a member variable or a method name with an underscore ( _ ) to tell developers they shouldn't mess with it.

  • If you precede a member variable or method name with two underscores ( __ ), Python will change its name using name mangling . This is more for preventing name conflicts than hiding things.

  • You can make any method into a property (it looks like a member variable) by putting the decorator @property on the line above its declaration. This can also be used to create getters .

  • You can create a setter for a property (e.g. foo ) by putting the decorator @foo.setter above a function foo .

  • A class (e.g. Dog ) can inherit from another class (e.g. Animal ) in this manner: class Dog(Animal): . When you do this, you should also start your initializer with the line super().__init__() to call the initializer of the base class.

  • Multiple inheritance is possible, but it might give you nightmares. Handle with tongs.

As usual, I recommend you read the docs for more:

Ready to go write some Python classes? Make it so!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK