概论:
oop方法将世界看作一个有结构、有组织、有层次、有普遍联系,有属性特征、有行为方法的有机体、生命体,在oop当中对象不仅有数据和结构,而且有控制代码、行为、函数、方法,自身特征与自身行为封装到类中,实例化到具体事务当中,每个层面均有自身意义,下层可以继承上层,乃至多个上层,下层可以传承、繁衍、发展、重写、多样化上层。不同层次、类别、事物之间有传承、组合、接口、繁衍关系。
oop之前的编程是过程式的,程序员将零散世界中的零散事物加以条理化程序化。
但是,世界实际上是有组织有结构的,支离破碎的抽象本质上不符合客观世界的原貌,因此,以前的编程方式也不适合刻画大型、复杂、贴合世界的应用。这时候更加贴合世界的叙述、刻画方式oop就粉墨登场了。
掌握oop方法需要一定的哲学思辨力,需要对客观世界本身存在的方式有良好的认知。能否掌握oop方式是初级程序员与中高级程序员的分水岭(当然,最近几年又出现了函数式编程,是一种新范式。这种范式有其自身优势,但目前还没有取代oop的可能。现在的中高级程序员仍需要掌握oop)
一个问题:类是对象吗?一般表述中说,类可以实例化出对象(更准确地说是instabce,或者occurrence),类是对象的抽象。甚至还有说法“对象可以是抽象的”。那么,类是对象吗?如果类不是对象(object),那么,面向对象编程(oop)的说法就有些残缺不全。如果类是object,那么类和实例化出的instance又有明显不同。比如,instance需要新的不同的存储空间……这个问题实质上不难理解,但命名上的确有些混乱。无论如何,可以这么理解:
Class | Object | |
---|---|---|
Definition |
Class is mechanism of bindingdata members and associated methods in a single unit. |
Instance of class or variable of class. |
Existence | It is logical existence | It is physical existence |
Memory Allocation |
Memory space is not allocated , when it is created. |
Memory space is allocated, when it is created. |
Declaration /definition |
Definition is created once. |
it is created many time as you require. |
一,University of Chicago,Introduction to Python: Class
Page Contents
- Class Definition Syntax
- Predefined Class Attributes
- Classes as Records / Structs
- Defining Functions in Classes: Methods
- Customizing Objects
- Inheritance
- Protection
- Polymorphism
Class Definition Syntax
A Python class is created by a class definition, has an associated name space, supports attribute reference, and is callable.
class name[(expr[,expr]*)]: suite
The class definition is an executable statement and as such can be used whereever an executable statement may occur. When executed, each expr is evaluated and must evaluate to a class; then the suite is executed in a new local name space: the assumption is that the statements in the suite will make bindings in this name space, so typically the statements in the suite are assignments and function definitions. After execution of the suite, name is bound to the new class in the outer (calling) name space, and the new name space of the class definition is associated with the class object.
Predefined Class Attributes#预定义的类的属性。可以查看这些属性以便了解类
Classes have five predefined attributes:
Attribute | Type | Read/Write | Description |
---|---|---|---|
__dict__ | dictionary | R/W | The class name space. |
__name__ | string | R/O | The name of the class. |
__bases__ | tuple of classes | R/O | The classes from which this class inherits. |
__doc__ | string OR None | R/W | The class documentation string. |
__module__ | string | R/W | The name of the module in which this class was defined. |
Classes as Records / Structs
The simplest use of classes is as simple Cartesian product types, e.g., the records of Pascal or the structs of C.
class foo:#定义类
a, b, c = 0, "bar", (1,2)
Instantiating Classes
A class is instantiated by calling the class object:
i = foo()#实例化类
print i.a, i.b, i.c#请改成python3的形式
In the above, i is an instance of the class foo. The slots a, b, and c of the instance can be modified by assignment:#可通过实例修改雷属性
i.a = 12
i.new = "yikes" # dynamic attribute creation!#此处还可可以创建实例自己的属性
Note that new slots, which weren't defined when the class was defined, can be created at will simply by assignment. Indeed, when working interactively, an empty class definition is often handy:
class foo: pass#甚至,类的属性也可以定义之后创建。只是这时候使用类名作为前缀
foo.a = 1
foo.b = 2
Instance Attributes
Instances have two predefined attributes:#实例的内建属性,通过它们查看该实例
Attribute | Type | Read/Write | Description |
---|---|---|---|
__dict__ | dictionary | R/W | The instance name space |
__class__ | class | R/W | The class of this instance |
Class Attributes vs Instance Attributes#类属性和实例属性的比较
It's important to understand the difference between class and instance attributes, especially since class attributes are accessible via instances.
An attribute defined in the class, either textually in a class definition or later by assignment to an attribute reference of the class, is a class attribute. It is stored in the class's name space (its __dict__).#类属性或者定义时给定,或者后来通过类名生成。它们都放在类的名字空间中,可以通过类的__dict__查看
An attribute defined in the instance, by assignment, is an instance attribute and is stored in the instance's name space -- even if there was a class attribute with the same name! Assignment via the instance results in an instance attribute that shadows the class attribute:#实例的属性村昂在实例的命名空间中,即时它与类属性重名也没有问题
class foo: a = 1 i = foo() foo.a => 1 i.a => 1 i.a = "inst" foo.a => 1 i.a => "inst"
It is possible to modify a class attribute from an instance, but you need to exploit Python's revelation of the respective name spaces:#也可以通过实例修改类变量,但此时必须精确指定类变量的路径。否则你只是生成了一个实例变量而已
foo.a => 1
i.__class__.__dict__[a] = "class"#此语法很有意思,表明python的隐藏变量仍然可见
foo.a => "class"
i.a => "inst"
When an attribute of an instance is referenced via the dot operator, Python first checks the instance name space, and then, if the attribute is not bound, it checks the class's name space. Here is the instance attribute lookup algorithm expressed in Python (N.B.: this is a simplified version of the real algorithm; we'll refine it when we introduce inheritance):#一般滴如果使用实例引用,一般滴先检查实例变量,然后才检查类变量
def instlookup(inst, name): # simplified algorithm... if inst.__dict__.has_key(name): return inst.__dict__[name] else: return inst.__class__.__dict__[name]
Note how this function will raise an AttributeError exception if the attribute is defined neither in the instance nor the class: just like Python.
Defining Functions in Classes: Methods#在类中定义函数。一般地,类中的函数被称作方法
(以下案例比较费解,用复杂古怪的方式演示基本内容)
Suppose we have Cartesian#笛卡儿 points:
cpt = (3,4)
and a function to compute the distance of the point to the origin:
def distanceToOrigin(p):
from math import floor, sqrt
return floor(sqrt(p[0]**2 + p[1]**2))
Now in our program, when manipulating points, we just call the function:
print distanceToOrigin(cpt)
Now suppose we introduce a new kind of point, a Manhattan point:
mpt = (3,4)
which has a different distance#曼哈顿距离 function. We immediately want to rename our first distance function:
CartesianDistanceToOrigin = distanceToOrigin
so that we can define the Manhattan version:
def ManhattanDistanceToOrigin(p):
return abs(p[0]) + abs(p[1])
This illustrates a name space problem: we should store our Cartesian and Manhattan functions in different name spaces. We could use Python's modules for this (cartesian.distanceToOrigin, manhattan.distanceToOrigin), but we would still have a problem: how do we know which points are which? We need to add a type tag to each tuple:
CARTESIAN, MANHATTAN = 0, 1 cpt = (CARTESIAN, 3, 4) mpt = (MANHATTAN, 3, 4)
(of course, since our objects' attributes are defined positionally, we now need to recode our distance functions: but that's not the problem we're considering...) and, worse, we need to write type checking code everywhere we use the points:
if pt[0] == CARTESIAN: print cartesian.distanceToOrigin(pt) elif pt[0] == MANHATTAN: print manhattan.distanceToOrigin(pt) else: raise TypeError, pt
To get around this problem we could write a generic distanceToOrigin function so that we could keep the conditional in one place, but we'd still have the problem of having to update that conditional everytime we added a new type of point. And if the author of the new point type isn't the author of the generic function, that can be a problem (the author of the new point type probably doesn't even know of all generic the point-manipulation functions out there, each of which will have a conditional that needs updating). The solution is to associate the functions that manipulate each type of object with the object itself. Such functions are called the methods of the object:
cpt = (3,4, lambda p: floor(sqrt(p[0]**2 + p[1]**2)))#这就是个元组
Now to find the distance to the origin for any kind of point pt, we no longer need the conditional: each point knows how to compute its own distance: pt[2](pt).
print cpt[2](cpt)#元组的第【2】项十个lambda函数,该函数以自身为参数时即将前两项变成参数,这种做法相关资料很少,比较费解
If the object carries around it's own functions, we don't need a conditional, nor the type information (at least, not for this purpose) and the author of a new type of point doesn't need to change somebody else's generic functions.
mpt = (3,4, lambda p: p[0] + p[1]) print mpt[2](mpt)
This is the fundamental idea of object-oriented programming.
One of the biggest problems with this demonstration is the use of tuples and their positional indexing. Clearly the use of dictionaries would be a big improvement:
cpt = { "x": 3, "y": 4, "distanceToOrigin": lambda p: floor(sqrt(p["x"]**2 + p["y"]**2)) } print cpt["distanceToOrigin"](cpt)
but using dictionaries doesn't give us any templating facility: with dictionaries, for each point we define, we'd need to copy in the definition of distanceToOrigin. What we want are the records of Pascal or the structs of C, and Python has the equivalent of these in its classes:
class cartesian: x, y = 0, 0 def distanceToOrigin(p): return floor(sqrt(p.x**2 + p.y**2)) cpt = cartesian() cpt.x, cpt.y = 3,4 # WARNING: the following is not correct Python code... print cpt.distanceToOrigin(cpt)
This is a lot better, but it's kind of annoying to always have to pass the object itself to its methods, especally since objects are first class and may be the value of complex expressions, e.g.:
x[y].distanceToOrigin(x[y])
This would be so error prone and potentially inefficient (due to reevaluation) that it would require us to always assign complex object expressions to local variables, so Python helps us out with a little bit of syntactic sugar: if you define a function in a class, it is assumed that you intend this function to be a class method, and therefore when you call such a function, Python passes in the instance as the first parameter implicitly: so the correct way to call the distanceToOrigin method is simply:
print cpt.distanceToOrigin()
Self#用self表达就清晰多了。也许上述古怪方法就是为了引出这个简便表述?
It's conventional in Python to name the first parameter of a method self, e.g.:
class cartesian: def distanceToOrigin(self): return floor(sqrt(self.x**2 + self.y**2))
This name isn't mandatory, but your code will look very strange to other Python hackers if you use another name.
Customizing Objects#定制你的对象,在实例化对象时运行此构造函数
Python allows you to customize your objects by defining some methods with special names:
__init__ Method
def __init__(self, parameters): suite
The parameters are as for ordinary functions, and support all the variants: positional, default, keyword, etc. When a class has an __init__ method, you pass parameters to the class when instantiating it#实例化时, and the __init__ method will be called with these parameters. Usually the method will set various instance variables#设置实例变量 via self.
class cartesian:#实例初始化
def __init__(self, x=0, y=0):
self.x, self.y = x, y
__del__ Method#删除实例时运行
def __del__(self): suite
A __del__ method is called when an object is deleted, which is when the garbage collector decides that their are no more references to an object. Note that this is not necessarily when the object is explicitly deleted with the del statement. The __del__ method takes exactly one parameter, self. Due to a weirdness in the current C implementation of Python, exceptions are ignored in __del__ methods: instead, an error will be printed to standard error.
__repr__ Method
#Python 定义了__str__()和__repr__()两种方法,__str__()用于显示给用户,而__repr__()用于显示给开发人员
def __repr__(self): suite
A __repr__ method takes exactly one parameter, self, and must return a string. This string is intended to be a representation of the object, suitable for display to the programmer, for instance when working in the interactive interpreter. __repr__ will be called anytime the builtin repr function is applied to an object; this function is also called when the backquote operator is used.
__str__ Method
def __str__(self): suite
The __str__ method is exactly like __repr__ except that it is called when the builtin str function is applied to an object; this function is also called for the %s escape of the % operator. In general, the string returned by __str__ is meant for the user of an application to see, while the string returned by __repr__ is meant for the programmer to see, as in debugging and development: but there are no hard and fast rules about this. You're best off just thinking, __str__ for %s, __repr__ for backquotes.
Inheritance#重要
Using classes to define objects provides a templating facility: class attributes and methods need only be defined once, and you can then instantiate any number of objects, all sharing the same methods.
But we could benefit from more sharing opportunities. Lots of times classes of related objects differ only slightly from one another. Consider the full definitions of our two classes of points:
class cartesian: def __init__(self, x=0, y=0): self.x, self.y = x, y def distanceToOrigin(self): return floor(sqrt(self.x**2 + self.y**2)) class manhattan: def __init__(self, x=0, y=0): self.x, self.y = x, y def distanceToOrigin(self): return self.x + self.y
Both of these classes share the same __init__ method, yet we have to code it twice. We can solve this problem by abstracting the common method into a new, more generic class called point:
class point:#共享基础类信息,可以用继承
def __init__(self, x=0, y=0):
self.x, self.y = x, y
Now we can redefine cartesian and manhattan and specify that they inherit from point:
class cartesian(point):#继承 def distanceToOrigin(self): return floor(sqrt(self.x**2 + self.y**2)) class manhattan(point): def distanceToOrigin(self): return self.x + self.y
We can define all behavior common to all types of points in the point class, and then define any number of subclasses of point which inherit from it. We could go farther and define subclasses of cartesian or manhattan if that were appropriate.
In some object-oriented languages (e.g., Java), point would be an abstract class#抽象类: in other words, a class that's used only to inherit from, and not itself directly instantiated. Python doesn't make this distinction: if you want to instantiate point, go right ahead!
Let's look at the class definitition syntax again:
class name[(expr[,expr]*)]:#可以多继承
suite
As mentioned earlier, each expr, if given, must evaluate to a class, and now we know why: these are called the base classes, and are the classes that the new class inherits from. If multiple base classes are given, the new class inherits from all of them: this is called multiple inheritance. See the next section for an explanation of how attribute reference works in the presence of multiple inheritance.
Attribute Reference in Detail
Now we can explain class and instance attribute reference in detail.
When looking up an attribute via a class object C, Python first searches the class's name space (C.__dict__); if it doesn't find the attribute, it then recursively searches the class's base classes, left to right and depth first.
When looking up an attribute via an instance object i, Python first searches the instance's name space (i.__dict__); if it doesn't find the attribute, it then searches the instance's class (i.__class__) as described in the previous paragraph.#检索变量的顺序
Here are the complete algorithms for class attribute lookup and instance attribute lookup. These functions each return a 2-tuple whose first element is a truth value indicating the success of the lookup, and whose second element is the value of the attribute, if the lookup was successful, or None if not:
def classlookup(C, name): if C.__dict__.has_key(name): return (1, C.__dict__[name]) else: for b in C.__bases__: success, value = classlookup(b, name) if success: return (1, value) else: pass else: return (0, None) def instlookup(I, name): if I.__dict__.has_key(name): return (1, I.__dict__[name]) else: return classlookup(I.__class__, name)
Protection#私有属性,或保护属性
Some B&D-oriented languages prevent access to the attributes of a class or instance, the idea being that if the author of the class didn't define a method to manipulate an attribute, then the user of the instance has no right to examine or change it. As you might have already guessed, Python doesn't take this approach. Attribute reference syntax can be used to access most instance and class attributes, and __dict__ attributes give the entire show away. The assumption is that you know what you're doing, and if you want to shoot yourself in the foot, that's your affair#不严格,靠自觉性.
That said, Python does support name mangling: if a method or other attribute name starts with two leading underscores (e.g., __secret), Python magically changes the name so that references to this attribute made in the usual way will fail:
class foo:
def __secret(self): pass
foo.__secret => AttributeError: __secret
This protection is purely advisory#建议性的,非强制, however: if we examine the class name space we can see what Python is up to:
foo.__dict__ => {'_foo__secret': <function __secret at fc328>, '__module__': '__main__', '__doc__': None}
The method name has been changed, or mangled, into _foo__secret: i.e., prefixed with underscore and the class name#使用下划线的类和变量仍可访问. Since this is documented behavior, you can use this name, either going through the __dict__ directly, or just via attribute reference (foo._foo__secret), to access the attribute.
Polymorphism#多态
Another important attribute of an object-oriented programming language is polymorphism: the ability to use the same syntax for objects of different types#相同的语法为不同对象产生不同行为. (Strictly speaking, this is ad-hoc polymorphism.) For example, in Python, the square bracket operator is used to perform indexing of various sequence types (list[3], dict["foo"]); polymorphism allows us to define our own types, as classes, that emulate builtin Python types like sequences and which therefore can use e.g. square brackets for indexing.
Customizing Attribute Reference#定制属性参考
We'll start by showing how to override the behavior of the dot operator, which does attribute reference in classes and instances. By customizing attribute reference, an object can perform an arbitrary action whenever one of its attributes is referenced, such as type checking.
__getattr__ Method
def __getattr__(self, name):
This method, if defined, is called when attribute lookup fails. (#__getattr__函数的作用: 如果属性查找(attribute lookup)在实例以及对应的类中(通过__dict__)失败, 那么会调用到类的__getattr__函数, 如果没有定义这个函数,那么抛出AttributeError异常。由此可见,__getattr__一定是作用于属性查找的最后一步)For example, consider the following:
class foo: a = 0 def __getattr__(self, name): return "%s: DEFAULT" % name i = foo() i.b = 1
Since the attribute a is a class attribute of instance i, and the attribute b is an instance attribute of i, the __getattr__ method isn't called when either of these are accessed:#因为这两个量可以找到,所以不调用__getattr__
i.a, i.b => 0, 1
But if we try to access an undefined attribute, say c, __getattr__ is called, with the attribute name as a parameter:#这个attr没有定义,找不到,所以执行该方法
i.c => "c: DEFAULT"
Note that __getattr__ won't be called if attribute lookup succeeds via inheritance.
The __getattr__ method should either return a value (of any type) or raise an AttributeError exception#如果实现了,则执行之。如果没有实现该方法则抛出异常。
__setattr__ Method
def __setattr__(self, name, value):
__setattr__ is called whenever an attribute assignment is attempted, regardless of whether or not the attribute is already bound in the instance or class. This happens instead of the normal mechanism of storing the value in the instance dictionary. This method can be used, for example, to perform type checking on a value before assigning it.
The __setattr__ method should not try to assign a value to an attribute in the usual way, i.e., self.name = value, as this will result in an infinite number of recursive calls to __setattr__; instead, the instance dictionary should be used directly:
def __setattr__(self, name, value): self.__dict__[name] = value
__delattr__ Method
def __delattr__(self, name):
This method is called when an attribute is deleted via the del statement.#当一个属性被删除时执行
二、较深入的介绍
Improve Your Python: Python Classes and Object Oriented Programming
The class
is a fundamental building block in Python. It is the underpinning
for not only many popular programs and libraries, but the Python standard library as
well. Understanding what classes are, when to use them, and how they can be
useful is essential, and the goal of this article. In the process, we'll explore
what the term Object-Oriented Programming means and how it ties together with
Python classes.
Everything Is An Object...#一切皆对象
What is the class
keyword used for, exactly? Like its function-based cousin
def
, it concerns the definition of things. While def
is used to define a
function, class
is used to define a class. And what is a class? Simply a
logical grouping of data and functions#数据和函数(方法) (the latter of which are frequently
referred to as "methods" when defined within a class).
What do we mean by "logical grouping"? Well, a class can contain any data we'd
like it to, and can have any functions (methods) attached to it that we please.
Rather than just throwing random things together under the name "class", we try
to create classes where there is a logical connection between things#事物间联系. Many
times, classes are based on objects in the real world (like Customer
or
Product
). Other times, classes are based on concepts in our system,
like HTTPRequest
or Owner
.#从现实世界和概念世界中抽象出来
Regardless, classes are a modeling technique; a way of thinking about programs. When you think about and implement your system in this way, you're said to be performing Object-Oriented Programming. "Classes" and "objects" are words that are often used interchangeably#互换, but they're not really the same thing. Understanding what makes them different is the key to understanding what they are and how they work.
..So Everything Has A Class?
Classes can be thought of as blueprints for creating objects.#类是创造对象的模具 When I define a
Customer class using the class
keyword, I haven't actually created a customer.
Instead, what I've created is a sort of instruction manual for constructing "customer"
objects. Let's look at the following example code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Customer(object):
"""A customer of ABC Bank with a checking account. Customers have the
following properties:
Attributes:
name: A string representing the customer's name.
balance: A float tracking the current balance of the customer's account.
"""
def __init__(self, name, balance=0.0):
"""Return a Customer object whose name is *name* and starting
balance is *balance*."""
self.name = name
self.balance = balance
def withdraw(self, amount):
"""Return the balance remaining after withdrawing *amount*
dollars."""
if amount > self.balance:
raise RuntimeError('Amount greater than available balance.')
self.balance -= amount
return self.balance
def deposit(self, amount):
"""Return the balance remaining after depositing *amount*
dollars."""
self.balance += amount
return self.balance
|
The class Customer(object)
line does not create a new customer. That is, just because we've defined a Customer
doesn't mean we've created one; we've merely outlined the blueprint to create a Customer
object. To do so, we call the class's __init__
method with the proper number of arguments (minus self
, which we'll get to in a moment).
So, to use the "blueprint" that we created by defining the class Customer
(which is used to create Customer
objects), we call the class name almost as if it were a function: jeff = Customer('Jeff Knupp', 1000.0)
. This line simply says "use the Customer
blueprint to create me a new object, which I'll refer to as jeff
."
The jeff
object, known as an instance, is the realized version of the Customer
class. Before we called Customer()
, no Customer
object existed. We can, of course, create as many Customer
objects as we'd like. There is still, however, only one Customer
class, regardless of how many instances of the class we create.
self#当场的你,类似于this,自体、本体、因人而异
?
So what's with that self
parameter to all of the Customer
methods? What is it? Why, it's the instance, of course! Put another way, a method like withdraw
defines the instructions for withdrawing money from some abstract customer's account. Calling jeff.withdraw(100.0)
puts those instructions to use on the jeff
instance.
So when we say def withdraw(self, amount):
, we're saying, "here's how you withdraw money from a Customer object (which we'll call self
) and a dollar figure (which we'll call amount
). self
is the instance of the Customer
that withdraw
is being called on. That's not me making analogies, either. jeff.withdraw(100.0)
is just shorthand for Customer.withdraw(jeff, 100.0)
, which is perfectly valid (if not often seen) code.
__init__#构造对象
self
may make sense for other methods, but what about __init__
? When we call __init__
, we're in the process of creating an object, so how can there already be a self
? Python allows us to extend the self
pattern to when objects are constructed as well, even though it doesn't exactly fit. Just imagine that jeff = Customer('Jeff Knupp', 1000.0)
is the same as calling jeff = Customer(jeff, 'Jeff Knupp', 1000.0)
; the jeff
that's passed in is also made the result.
This is why when we call __init__
, we initialize objects by saying things like self.name = name
. Remember, since self
is the instance, this is equivalent to saying jeff.name = name
, which is the same as jeff.name = 'Jeff Knupp
. Similarly, self.balance = balance
is the same as jeff.balance = 1000.0
. After these two lines, we consider the Customer
object "initialized" and ready for use.
Be careful what you __init__
After __init__
has finished, the caller can rightly assume that the object is ready to use. That is, after jeff = Customer('Jeff Knupp', 1000.0)
, we can start making deposit
and withdraw
calls on jeff
; jeff
is a fully-initialized object.
Imagine for a moment we had defined the Customer
class slightly differently:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class Customer(object):
"""A customer of ABC Bank with a checking account. Customers have the
following properties:
Attributes:
name: A string representing the customer's name.
balance: A float tracking the current balance of the customer's account.
"""
def __init__(self, name):
"""Return a Customer object whose name is *name*."""
self.name = name
def set_balance(self, balance=0.0):
"""Set the customer's starting balance."""
self.balance = balance
def withdraw(self, amount):
"""Return the balance remaining after withdrawing *amount*
dollars."""
if amount > self.balance:
raise RuntimeError('Amount greater than available balance.')
self.balance -= amount
return self.balance
def deposit(self, amount):
"""Return the balance remaining after depositing *amount*
dollars."""
self.balance += amount
return self.balance
|
This may look like a reasonable alternative; we simply need to call set_balance
before we begin using the instance. There's no way, however, to communicate this to the caller. Even if we document it extensively, we can't force the caller to call jeff.set_balance(1000.0)
before calling jeff.withdraw(100.0)
. Since the jeff
instance doesn't even have a balance attribute until jeff.set_balance
is called, this means that the object hasn't been "fully" initialized.
The rule of thumb is, don't introduce a new attribute outside of the __init__
method, otherwise you've given the caller an object that isn't fully initialized. There are exceptions, of course, but it's a good principle to keep in mind. This is part of a larger concept of object consistency: there shouldn't be any series of method calls that can result in the object entering a state that doesn't make sense.
Invariants (like, "balance should always be a non-negative number") should hold both when a method is entered and when it is exited. It should be impossible for an object to get into an invalid state just by calling its methods. It goes without saying, then, that an object should start in a valid state as well, which is why it's important to initialize everything in the __init__
method.
Instance Attributes and Methods#实例的属性和方法,即特色特有的你的特质
An function defined in a class is called a "method". Methods have access to all the data contained on the instance of the object; they can access and modify anything previously set on self
. Because they use self
, they require an instance of the class in order to be used. For this reason, they're often referred to as "instance methods".
If there are "instance methods", then surely there are other types of methods as well, right? Yes, there are, but these methods are a bit more esoteric. We'll cover them briefly here, but feel free to research these topics in more depth.
Static Methods#类属性,公用属性
Class attributes are attributes that are set at the class-level, as opposed to the instance-level. Normal attributes are introduced in the __init__
method, but some attributes of a class hold for all instances in all cases. For example, consider the following definition of a Car
object:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Car(object):
wheels = 4
def __init__(self, make, model):
self.make = make
self.model = model
mustang = Car('Ford', 'Mustang')
print mustang.wheels
# 4
print Car.wheels
# 4
|
A Car
always has four wheels
, regardless of the make
or model
. Instance methods can access these attributes in the same way they access regular attributes: through self
(i.e. self.wheels
).
There is a class of methods, though, called static methods, that don't have access to self
. Just like class attributes, they are methods that work without requiring an instance to be present. Since instances are always referenced through self#通过instance参照
, static methods have no self
parameter#静态方法无self参数.
The following would be a valid static method on the Car
class:
1 2 3 4 |
class Car(object):
...
def make_car_sound():#无self
print 'VRooooommmm!'
|
No matter what kind of car we have, it always makes the same sound (or so I tell my ten month old daughter). To make it clear that this method should not receive the instance as the first parameter (i.e. self
on "normal" methods), the @staticmethod
decorator is used, turning our definition into:
1 2 3 4 5 |
class Car(object):
...
@staticmethod
def make_car_sound():
print 'VRooooommmm!'
|
Class Methods
A variant of the static method is the class method. Instead of receiving the instance as the first parameter, it is passed the class. It, too, is defined using a decorator:
1 2 3 4 5 |
class Vehicle(object):
...
@classmethod
def is_motorcycle(cls):
return cls.wheels == 2
|
Class methods may not make much sense right now, but that's because they're used most often in connection with our next topic: inheritance.
Inheritance
While Object-oriented Programming is useful as a modeling tool, it truly gains power when the concept of inheritance is introduced. Inherticance is the process by which a "child" class derives the data and behavior of a "parent" class#自类从父类继承数据和行为. An example will definitely help us here.
Imagine we run a car dealership. We sell all types of vehicles, from motorcycles to trucks. We set ourselves apart from the competition by our prices. Specifically, how we determine the price of a vehicle on our lot: $5,000 x number of wheels a vehicle has. We love buying back our vehicles as well. We offer a flat rate - 10% of the miles driven on the vehicle. For trucks, that rate is $10,000. For cars, $8,000. For motorcycles, $4,000.
If we wanted to create a sales system for our dealership using Object-oriented techniques, how would we do so? What would the objects be? We might have a Sale
class, a Customer
class, an Inventory
class, and so forth, but we'd almost certainly have a Car
, Truck
, and Motorcycle
class.
What would these classes look like? Using what we've learned, here's a possible implementation of the Car
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Car(object):
"""A car for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the car has.
miles: The integral number of miles driven on the car.
make: The make of the car as a string.
model: The model of the car as a string.
year: The integral year the car was built.
sold_on: The date the vehicle was sold.
"""
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Car object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this car as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the car."""
if self.sold_on is None:
return 0.0 # Not yet sold
return 8000 - (.10 * self.miles)
...
|
OK, that looks pretty reasonable. Of course, we would likely have a number of other methods on the class, but I've shown two of particular interest to us: sale_price
and purchase_price
. We'll see why these are important in a bit.
Now that we've got the Car
class#已经拥有car类, perhaps we should crate a Truck
class? Let's follow the same pattern we did for car:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
class Truck(object):
"""A truck for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the truck has.
miles: The integral number of miles driven on the truck.
make: The make of the truck as a string.
model: The model of the truck as a string.
year: The integral year the truck was built.
sold_on: The date the vehicle was sold.
"""
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Truck object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this truck as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the truck."""
if self.sold_on is None:
return 0.0 # Not yet sold
return 10000 - (.10 * self.miles)
...
|
Wow. That's almost identical to the car class. One of the most important rules of programming (in general, not just when dealing with objects) is "DRY" or "Don't Repeat Yourself. We've definitely repeated ourselves here. In fact, the Car
and Truck
classes differ only by a single character (aside from comments).
So what gives? Where did we go wrong? Our main problem is that we raced straight to the concrete: Car
s and Truck
s are real things, tangible objects that make intuitive sense as classes. However, they share so much data and functionality in common#不要直接走向细节,先看大类,看共同点。需要一个抽象 that it seems there must be an abstraction we can introduce here. Indeed there is: the notion of Vehicle
s.
Abstract Classes#抽象类
A Vehicle
is not a real-world object. Rather, it is a concept that some real-world objects (like cars, trucks, and motorcycles) embody. We would like to use the fact that each of these objects can be considered a vehicle to remove repeated code. We can do that by creating a Vehicle
class:#共性部分
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
base_sale_price = 0
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Vehicle object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
Now we can make the Car
and Truck
class inherit from the Vehicle
class#让car和truck继承交通工具 by replacing object
in the line class Car(object)
. The class in parenthesis is the class that is inherited from (object
essentially means "no inheritance". We'll discuss exactly why we write that in a bit).
We can now define Car
and Truck
in a very straightforward way:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Car(Vehicle):
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Car object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
self.base_sale_price = 8000
class Truck(Vehicle):
def __init__(self, wheels, miles, make, model, year, sold_on):
"""Return a new Truck object."""
self.wheels = wheels
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
self.base_sale_price = 10000
|
This works, but has a few problems. First, we're still repeating a lot of code. We'd ultimately like to get rid of all repetition. Second, and more problematically, we've introduced the Vehicle
class, but should we really allow people to create Vehicle
objects (as opposed to Car
s or Truck
s)? A Vehicle
is just a concept, not a real thing, so what does it mean to say the following:
1 2 |
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
print v.purchase_price()
|
A Vehicle
doesn't have a base_sale_price
, only the individual child classes like Car
and Truck
do. The issue is that Vehicle
should really be an Abstract Base Class. Abstract Base Classes are classes that are only meant to be inherited from; you can't create instance of an ABC. That means that, if Vehicle
is an ABC, the following is illegal:
1 |
v = Vehicle(4, 0, 'Honda', 'Accord', 2014, None)
|
It makes sense to disallow this, as we never meant for vehicles to be used directly. We just wanted to use it to abstract away some common data and behavior. So how do we make a class an ABC? Simple! The abc
module contains a metaclass called ABCMeta
(metaclasses are a bit outside the scope of this article). Setting a class's metaclass to ABCMeta
and making one of its methods virtual makes it an ABC. A virtual method is one that the ABC says must exist in child classes, but doesn't necessarily actually implement#抽象类不可实例化,只提供基本信息. For example, the Vehicle class may be defined as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
from abc import ABCMeta, abstractmethod
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
__metaclass__ = ABCMeta
base_sale_price = 0
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
@abstractmethod
def vehicle_type():
""""Return a string representing the type of vehicle this is."""
pass
|
Now, since vehicle_type
is an abstractmethod
, we can't directly create an instance of Vehicle
. As long as Car
and Truck
inherit from Vehicle
and define vehicle_type
, we can instantiate those classes just fine.
Returning to the repetition in our Car
and Truck
classes, let see if we can't remove that by hoisting up common functionality to the base class, Vehicle
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
from abc import ABCMeta, abstractmethod
class Vehicle(object):
"""A vehicle for sale by Jeffco Car Dealership.
Attributes:
wheels: An integer representing the number of wheels the vehicle has.
miles: The integral number of miles driven on the vehicle.
make: The make of the vehicle as a string.
model: The model of the vehicle as a string.
year: The integral year the vehicle was built.
sold_on: The date the vehicle was sold.
"""
__metaclass__ = ABCMeta
base_sale_price = 0
wheels = 0
def __init__(self, miles, make, model, year, sold_on):
self.miles = miles
self.make = make
self.model = model
self.year = year
self.sold_on = sold_on
def sale_price(self):
"""Return the sale price for this vehicle as a float amount."""
if self.sold_on is not None:
return 0.0 # Already sold
return 5000.0 * self.wheels
def purchase_price(self):
"""Return the price for which we would pay to purchase the vehicle."""
if self.sold_on is None:
return 0.0 # Not yet sold
return self.base_sale_price - (.10 * self.miles)
@abstractmethod
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
pass
|
Now the Car
and Truck
classes become:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Car(Vehicle):
"""A car for sale by Jeffco Car Dealership."""
base_sale_price = 8000
wheels = 4
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'car'
class Truck(Vehicle):
"""A truck for sale by Jeffco Car Dealership."""
base_sale_price = 10000
wheels = 4
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'truck'
|
This fits perfectly with our intuition: as far as our system is concerned, the only difference between a car and truck is the base sale price. Defining a Motorcycle
class, then, is similarly simple:
1 2 3 4 5 6 7 8 9 |
class Motorcycle(Vehicle):
"""A motorcycle for sale by Jeffco Car Dealership."""
base_sale_price = 4000
wheels = 2
def vehicle_type(self):
""""Return a string representing the type of vehicle this is."""
return 'motorcycle'
|
Inheritance and the LSP
Even though it seems like we used inheritance to get rid of duplication, what we were really doing was simply providing the proper level of abstraction. And abstraction is the key to understanding inheritance. We've seen how one side-effect of using inheritance is that we reduce duplicated code, but what about from the caller's perspective. How does using inheritance change that code?
Quite a bit, it turns out. Imagine we have two classes, Dog
and Person
, and we want to write a function that takes either type of object and prints out whether or not the instance in question can speak (a dog can't, a person can). We might write code like the following:
1 2 3 4 5 6 7 |
def can_speak(animal):
if isinstance(animal, Person):
return True
elif isinstance(animal, Dog):
return False
else:
raise RuntimeError('Unknown animal!')
|
That works when we only have two types of animals, but what if we have twenty, or two hundred? That if...elif
chain is going to get quite long.
The key insight here is that can_speak
shouldn't care what type of animal it's dealing with, the animal class itself should tell us if it can speak. By introducing a common base class, Animal
, that defines can_speak
, we relieve the function of it's type-checking burden. Now, as long as it knows it was an Animal
that was passed in, determining if it can speak is trivial:
1 2 |
def can_speak(animal):
return animal.can_speak()
|
This works because Person
and Dog
(and whatever other classes we crate to derive from Animal
) follow the Liskov Substitution Principle. This states that we should be able to use a child class (like Person
or Dog
) wherever a parent class (Animal
) is expected an everything will work fine. This sounds simple, but it is the basis for a powerful concept we'll discuss in a future article: interfaces.
Summary
Hopefully, you've learned a lot about what Python classes are, why they're useful, and how to use them. The topic of classes and Object-oriented Programming are insanely deep. Indeed, they reach to the core of computer science. This article is not meant to be an exhaustive study of classes, nor should it be your only reference. There are literally thousands of explanations of OOP and classes available online, so if you didn't find this one suitable, certainly a bit of searching will reveal one better suited to you.
As always, corrections and arguments are welcome in the comments. Just try to keep it civil.
Lastly, it's not too late to see me speak at the upcoming Wharton Web Conference at UPenn! Check the site for info and tickets.
三,更多内容,静态方法以及修饰符
Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: OOP Method Types in Python: @classmethod vs @staticmethod vs Instance Methods
In this tutorial I’ll help demystify what’s behind class methods, static methods, and regular instance methods.
If you develop an intuitive understanding for their differences you’ll be able to write object-oriented Python that communicates its intent more clearly and will be easier to maintain in the long run.
Free Bonus: Click here to get access to a free Python OOP Cheat Sheet that points you to the best tutorials, videos, and books to learn more about Object-Oriented Programming with Python.
Instance, Class, and Static Methods — An Overview
Let’s begin by writing a (Python 3) class that contains simple examples for all three method types:
class MyClass:
def method(self):
return 'instance method called', self
@classmethod
def classmethod(cls):
return 'class method called', cls
@staticmethod
def staticmethod():
return 'static method called'
NOTE: For Python 2 users: The
@staticmethod
and@classmethod
decorators are available as of Python 2.4 and this example will work as is. Instead of using a plainclass MyClass:
declaration you might choose to declare a new-style class inheriting fromobject
with theclass MyClass(object):
syntax. Other than that you’re good to go.
Instance Methods
The first method on MyClass
, called method
, is a regular instance method. That’s the basic, no-frills method type you’ll use most of the time. You can see the method takes one parameter, self
, which points to an instance of MyClass
when the method is called (but of course instance methods can accept more than just one parameter).
Through the self
parameter, instance methods can freely access attributes and other methods on the same object. This gives them a lot of power when it comes to modifying an object’s state.
Not only can they modify object state, instance methods can also access the class itself through the self.__class__
attribute. This means instance methods can also modify class state.
Class Methods
Let’s compare that to the second method, MyClass.classmethod
. I marked this method with a @classmethod
decorator to flag it as a class method.
Instead of accepting a self
parameter, class methods take a cls
parameter that points to the class—and not the object instance—when the method is called.
Because the class method only has access to this cls
argument, it can’t modify object instance state. That would require access to self
. However, class methods can still modify class state that applies across all instances of the class.
Static Methods
The third method, MyClass.staticmethod
was marked with a @staticmethod
decorator to flag it as a static method.
This type of method takes neither a self
nor a cls
parameter (but of course it’s free to accept an arbitrary number of other parameters).
Therefore a static method can neither modify object state nor class state. Static methods are restricted in what data they can access - and they’re primarily a way to namespace your methods.
Let’s See Them In Action!
I know this discussion has been fairly theoretical up to this point. And I believe it’s important that you develop an intuitive understanding for how these method types differ in practice. We’ll go over some concrete examples now.
Let’s take a look at how these methods behave in action when we call them. We’ll start by creating an instance of the class and then calling the three different methods on it.
MyClass
was set up in such a way that each method’s implementation returns a tuple containing information for us to trace what’s going on — and which parts of the class or object the method can access.
Here’s what happens when we call an instance method:
>>> obj = MyClass()
>>> obj.method()
('instance method called', <MyClass instance at 0x101a2f4c8>)
This confirmed that method
(the instance method) has access to the object instance (printed as <MyClass instance>
) via the self
argument.
When the method is called, Python replaces the self
argument with the instance object, obj
. We could ignore the syntactic sugar of the dot-call syntax (obj.method()
) and pass the instance object manually to get the same result:
>>> MyClass.method(obj)
('instance method called', <MyClass instance at 0x101a2f4c8>)
Can you guess what would happen if you tried to call the method without first creating an instance?
By the way, instance methods can also access the class itself through the self.__class__
attribute. This makes instance methods powerful in terms of access restrictions - they can modify state on the object instance and on the class itself.
Let’s try out the class method next:
>>> obj.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)
Calling classmethod()
showed us it doesn’t have access to the <MyClass instance>
object, but only to the <class MyClass>
object, representing the class itself (everything in Python is an object, even classes themselves).
Notice how Python automatically passes the class as the first argument to the function when we call MyClass.classmethod()
. Calling a method in Python through the dot syntax triggers this behavior. The self
parameter on instance methods works the same way.
Please note that naming these parameters self
and cls
is just a convention. You could just as easily name them the_object
and the_class
and get the same result. All that matters is that they’re positioned first in the parameter list for the method.
Time to call the static method now:
>>> obj.staticmethod()
'static method called'
Did you see how we called staticmethod()
on the object and were able to do so successfully? Some developers are surprised when they learn that it’s possible to call a static method on an object instance.
Behind the scenes Python simply enforces the access restrictions by not passing in the self
or the cls
argument when a static method gets called using the dot syntax.
This confirms that static methods can neither access the object instance state nor the class state. They work like regular functions but belong to the class’s (and every instance’s) namespace.
Now, let’s take a look at what happens when we attempt to call these methods on the class itself - without creating an object instance beforehand:
>>> MyClass.classmethod()
('class method called', <class MyClass at 0x101a2f4c8>)
>>> MyClass.staticmethod()
'static method called'
>>> MyClass.method()
TypeError: unbound method method() must
be called with MyClass instance as first
argument (got nothing instead)
We were able to call classmethod()
and staticmethod()
just fine, but attempting to call the instance method method()
failed with a TypeError
.
And this is to be expected — this time we didn’t create an object instance and tried calling an instance function directly on the class blueprint itself. This means there is no way for Python to populate the self
argument and therefore the call fails.
This should make the distinction between these three method types a little more clear. But I’m not going to leave it at that. In the next two sections I’ll go over two slightly more realistic examples for when to use these special method types.
I will base my examples around this bare-bones Pizza
class:
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
def __repr__(self):
return f'Pizza({self.ingredients!r})'
>>> Pizza(['cheese', 'tomatoes'])
Pizza(['cheese', 'tomatoes'])
Note: This code example and the ones further along in the tutorial use Python 3.6 f-strings to construct the string returned by
__repr__
. On Python 2 and versions of Python 3 before 3.6 you’d use a different string formatting expression, for example:def __repr__(self): return 'Pizza(%r)' % self.ingredients
Delicious Pizza Factories With @classmethod
If you’ve had any exposure to pizza in the real world you’ll know that there are many delicious variations available:
Pizza(['mozzarella', 'tomatoes'])
Pizza(['mozzarella', 'tomatoes', 'ham', 'mushrooms'])
Pizza(['mozzarella'] * 4)
The Italians figured out their pizza taxonomy centuries ago, and so these delicious types of pizzas all have their own names. We’d do well to take advantage of that and give the users of our Pizza
class a better interface for creating the pizza objects they crave.
A nice and clean way to do that is by using class methods as factory functions for the different kinds of pizzas we can create:
class Pizza:
def __init__(self, ingredients):
self.ingredients = ingredients
def __repr__(self):
return f'Pizza({self.ingredients!r})'
@classmethod
def margherita(cls):
return cls(['mozzarella', 'tomatoes'])
@classmethod
def prosciutto(cls):
return cls(['mozzarella', 'tomatoes', 'ham'])
Note how I’m using the cls
argument in the margherita
and prosciutto
factory methods instead of calling the Pizza
constructor directly.
This is a trick you can use to follow the Don’t Repeat Yourself (DRY) principle. If we decide to rename this class at some point we won’t have to remember updating the constructor name in all of the classmethod factory functions.
Now, what can we do with these factory methods? Let’s try them out:
>>> Pizza.margherita()
Pizza(['mozzarella', 'tomatoes'])
>>> Pizza.prosciutto()
Pizza(['mozzarella', 'tomatoes', 'ham'])
As you can see, we can use the factory functions to create new Pizza
objects that are configured the way we want them. They all use the same __init__
constructor internally and simply provide a shortcut for remembering all of the various ingredients.
Another way to look at this use of class methods is that they allow you to define alternative constructors for your classes.
Python only allows one __init__
method per class. Using class methods it’s possible to add as many alternative constructors as necessary. This can make the interface for your classes self-documenting (to a certain degree) and simplify their usage.
When To Use Static Methods
It’s a little more difficult to come up with a good example here. But tell you what, I’ll just keep stretching the pizza analogy thinner and thinner… (yum!)
Here’s what I came up with:
import math
class Pizza:
def __init__(self, radius, ingredients):
self.radius = radius
self.ingredients = ingredients
def __repr__(self):
return (f'Pizza({self.radius!r}, '
f'{self.ingredients!r})')
def area(self):
return self.circle_area(self.radius)
@staticmethod
def circle_area(r):
return r ** 2 * math.pi
Now what did I change here? First, I modified the constructor and __repr__
to accept an extra radius
argument.
I also added an area()
instance method that calculates and returns the pizza’s area (this would also be a good candidate for an @property
— but hey, this is just a toy example).
Instead of calculating the area directly within area()
, using the well-known circle area formula, I factored that out to a separate circle_area()
static method.
Let’s try it out!
>>> p = Pizza(4, ['mozzarella', 'tomatoes'])
>>> p
Pizza(4, ['mozzarella', 'tomatoes'])
>>> p.area()
50.26548245743669
>>> Pizza.circle_area(4)
50.26548245743669
Sure, this is a bit of a simplistic example, but it’ll do alright helping explain some of the benefits that static methods provide.
As we’ve learned, static methods can’t access class or instance state because they don’t take a cls
or self
argument. That’s a big limitation — but it’s also a great signal to show that a particular method is independent from everything else around it.
In the above example, it’s clear that circle_area()
can’t modify the class or the class instance in any way. (Sure, you could always work around that with a global variable but that’s not the point here.)
Now, why is that useful?
Flagging a method as a static method is not just a hint that a method won’t modify class or instance state — this restriction is also enforced by the Python runtime.
Techniques like that allow you to communicate clearly about parts of your class architecture so that new development work is naturally guided to happen within these set boundaries. Of course, it would be easy enough to defy these restrictions. But in practice they often help avoid accidental modifications going against the original design.
Put differently, using static methods and class methods are ways to communicate developer intent while enforcing that intent enough to avoid most slip of the mind mistakes and bugs that would break the design.
Applied sparingly and when it makes sense, writing some of your methods that way can provide maintenance benefits and make it less likely that other developers use your classes incorrectly.
Static methods also have benefits when it comes to writing test code.
Because the circle_area()
method is completely independent from the rest of the class it’s much easier to test.
We don’t have to worry about setting up a complete class instance before we can test the method in a unit test. We can just fire away like we would testing a regular function. Again, this makes future maintenance easier.
Key Takeaways
- Instance methods need a class instance and can access the instance through
self
. - Class methods don’t need a class instance. They can’t access the instance (
self
) but they have access to the class itself viacls
. - Static methods don’t have access to
cls
orself
. They work like regular functions but belong to the class’s namespace. - Static and class methods communicate and (to a certain degree) enforce developer intent about class design. This can have maintenance benefits.