极其简单的 Python:类
课程正在进行中
宣言
方法
类方法与静态方法
初始化器和构造函数
变量
范围:私人和公共
特性
遗产
继续上课!
审查
喜欢这些文章吗?那就买本书吧! Jason C. McDonald 的《Dead Simple Python》现已由 No Starch Press 出版。
类和对象:许多开发人员的日常工作。面向对象编程是现代编程的支柱之一,因此 Python 能够做到这一点也就不足为奇了。
但是如果你在使用 Python 之前用过其他任何语言进行过面向对象编程,我几乎可以肯定你做错了。
朋友们,坚持你们的范式,这将是一次坎坷的旅程。
课程正在进行中
我们先开个班,先熟悉一下。大部分内容对任何人来说都不会感到惊讶。
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
@classmethod
def make_sound(cls):
print(cls.sound)
一旦声明了,我们就可以从这个类创建一个新的实例或对象。所有成员函数和变量都可以使用点符号访问。
uss_enterprise = Starship()
uss_enterprise.warp(4)
uss_enterprise.engage()
uss_enterprise.engines
>>> True
uss_enterprise.engine_speed
>>> 8
哇,杰森,我知道这应该是“非常简单”,但我想你只是让我睡着了。
没什么好惊讶的,对吧?不过,再看看,不是看有什么,而是看没有什么。
你没发现?好吧,我们来分析一下。看看你能不能在我之前发现这些惊喜。
宣言
我们从类本身的定义开始:
class Starship(object):
Python 可以被认为是真正面向对象的语言之一,因为它的设计原则是“一切皆对象”。所有其他类都继承自该类object
。
当然,大多数 Pythonistas 确实讨厌样板代码,所以从 Python 3 开始,我们也可以这样说,并称之为好:
class Starship:
就我个人而言,考虑到《Python 之禅》中关于“显式优于隐式”的说法,我更喜欢第一种方式。我们真的可以争论到天荒地老,所以,我们先明确一点,在 Python 3 中,这两种方法的作用是一样的,然后继续讨论。
遗留注意事项:如果您希望您的代码在 Python 2 上运行,您必须说(object)
。
方法
我要跳到这一行...
def warp(self, factor):
显然,这是一个成员函数或方法。在 Python 中,我们将其self
作为第一个参数传递给每个方法。之后,我们可以拥有任意数量的参数,就像任何其他函数一样。
实际上,我们不必调用第一个参数self
;无论如何,它都会正常工作。但是,出于风格考虑,我们总是在那里使用名称self
。没有任何理由违反这条规则。
“但是,但是……你自己确实违反了规则!看到下一个功能了吗?”
@classmethod
def make_sound(cls):
你可能还记得,在面向对象编程中,类方法是类的所有实例(对象)共享的方法。类方法永远不会触及成员变量或常规方法。
如果您还没有注意到,我们总是通过点运算符访问类中的成员变量:self.
。因此,为了更加清楚地说明我们不能在类方法中这样做,我们将第一个参数称为cls
。实际上,当调用类方法时,Python 会将类传递给该参数,而不是对象。
和以前一样,我们可以随意调用cls
任何我们想要的,但这并不意味着我们应该这样做。
对于类方法,我们还必须将装饰器 @classmethod
放在函数声明的上方。这告诉 Python 语言你正在创建一个类方法,而不是仅仅在参数名称上随意发挥self
。
上述方法将被这样调用......
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 class functions on the object, or directly on the class.
uss_enterprise.make_sound()
Starship.make_sound()
最后两行将以完全相同的方式打印出“Vrrrrrrrrrrrrrrrrrrrrrr”。(请注意,我指的cls.sound
是该函数。)
...
什么?
拜托,你知道小时候你给想象中的宇宙飞船做过音效吗?别评判我。
类方法与静态方法
俗话说得好:活到老学到老。Kinyanjui Wangonya在评论中指出,没必要使用cls
“静态方法”——我在本文初版中用过这个说法。
事实证明他是对的,而我却感到困惑!
与许多其他语言不同,Python 区分静态方法和类方法。从技术上讲,它们的工作方式相同,因为它们都在对象的所有实例之间共享。只有一个关键的区别……
静态方法不访问任何类成员;它甚至不在乎自己是否是类的一部分!因为它不需要访问类的任何其他部分,所以它不需要cls
参数。
让我们对比一下类方法和静态方法:
@classmethod
def make_sound(cls):
print(cls.sound)
@staticmethod
def beep():
print("Beep boop beep")
由于beep()
不需要访问类,我们可以使用@staticmethod
装饰器将其设置为静态方法。Python 不会像类方法那样将类隐式传递给第一个参数(make_sound()
)。
尽管存在这种差异,但两者的调用方式是一样的。
uss_enterprise = Starship()
uss_enterprise.make_sound()
>>> Vrrrrrrrrrrrrrrrrrrrrr
Starship.make_sound()
>>> Vrrrrrrrrrrrrrrrrrrrrr
uss_enterprise.beep()
>>> Beep boop beep
Starship.beep()
>>> Beep boop beep
初始化器和构造函数
每个 Python 类都需要有且仅有一个函数,__init__(self)
它被称为初始化函数。
def __init__(self):
self.engine_speed = 1
self.shields = True
self.engines = False
如果你真的不需要初始化器,从技术上来说,跳过定义它是可以的,但这普遍被认为是不好的做法。至少,定义一个空的初始化器吧……
def __init__(self):
pass
虽然我们倾向于像在 C++ 和 Java 中那样使用它,但它并不是__init__(self)
构造函数!初始化器负责初始化实例变量,我们稍后会详细讨论。
我们很少需要真正定义自己的构造函数。如果你真的知道自己在做什么,你可以重新定义__new__(cls)
函数……
def __new__(cls):
return object.__new__(cls)
顺便说一句,如果您正在寻找析构函数,那就是该__del__(self)
函数。
变量
在 Python 中,我们的类可以有实例变量,这些变量对于我们的对象(实例)来说是唯一的,还有类变量(又名静态变量),它们属于该类,并且在所有实例之间共享。
我必须坦白:在 Python 开发的最初几年里,我的做法完全是错的!因为我之前学过其他面向对象语言,所以我当时真的以为我应该这么做:
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
代码运行正常,那么这幅图有什么问题呢?再读一遍,看看你能不能弄清楚发生了什么。
播放《危险边缘》的最终音乐
也许这样就能让它变得明显。
uss_enterprise = Starship()
uss_enterprise.warp(4)
print(uss_enterprise.engine_speed)
>>> 8
print(Starship.engine_speed)
>>> 0
你发现了吗?
类变量在所有函数外部声明,通常在顶部。而实例变量则在__init__(self)
函数内部声明:例如self.engine_speed = 0
。
在我们的小例子中,我们声明了一组同名的类变量和一组实例变量。当访问对象上的变量时,实例变量会遮蔽(隐藏)类变量,使其行为符合我们的预期。然而,通过打印我们可以看到Starship.engine_speed
,类中有一个单独的类变量,只是占用了空间。真是冗余。
有人猜对了吗?斯隆猜对了,他打赌……一万片皇蚕叶。看来树懒领先了。真是太神奇了。
顺便说一句,你可以在任何实例方法中首次声明实例变量,而不是在初始化器中。但是……你猜对了:不要这样做。惯例是始终在初始化器中声明所有实例变量,只是为了防止发生一些奇怪的事情,例如函数尝试访问尚不存在的变量。
范围:私人和公共
如果您之前学习过其他面向对象语言,例如 Java 和 C++,您可能也习惯于思考作用域(private、protected、public)及其传统假设:变量应该是私有的,函数(通常)应该是公有的。getter 和 setter 是当今的主流!
我也是 C++ 面向对象编程的专家,不得不说,我认为 Python 处理作用域问题的方法远胜于典型的面向对象作用域规则。一旦你掌握了如何在 Python 中设计类,这些原则很可能会渗透到你使用其他语言的标准实践中……我坚信这是一件好事。
准备好了吗?你的变量实际上不需要是私有的。
是的,我刚才听到后面那个 Java 宅男发出了尖叫声。“可是……可是……我该如何阻止开发人员篡改对象的任何实例变量呢?”
这种担忧通常建立在三个错误的假设之上。我们先来纠正一下:
-
使用您的类的开发人员几乎肯定没有直接修改成员变量的习惯,就像他们没有将叉子插入烤面包机的习惯一样。
-
如果他们确实把叉子插进了烤面包机,那么,用俗话说,后果是他们自己承担的,因为他们太愚蠢,而不是你。
-
正如我的 Freenode #python 朋友
grym
曾经说过的,“如果你知道为什么你不应该用金属物体从烤面包机中取出卡住的面包,那么你可以这样做。”
换句话说,使用你的类的开发人员可能比你更了解他们是否应该摆弄实例变量。
现在,解决了这个问题,我们来接近 Python 中的一个重要前提:Python 中不存在真正的“私有”作用域。我们不能仅仅在变量前面加一个花哨的小关键字,就让它变成私有的。
我们可以做的是在名称前面加上下划线,如下所示:self._engine
。
那个下划线没什么魔力。它只是给所有使用你的类的人一个警告标签:“我建议你别乱动它。我要用它做一些特别的事情。”
现在,在你_
开始为所有实例变量命名之前,先想想这个变量到底是什么,以及如何使用它。直接修改它真的会出问题吗?就我们示例类的情况而言,就像现在这样写,不会。实际上,这样写是完全可以接受的:
uss_enterprise.engine_speed = 6
uss_enterprise.engage()
另外,注意到其中的美妙之处了吗?我们一个 getter 或 setter 都没写!在任何语言中,如果 getter 或 setter 的功能与直接修改变量相同,那绝对是浪费。这种理念正是 Python 如此简洁的原因之一。
您还可以将此命名约定用于您不打算在类外使用的方法。
附注:在你开始在 Java 和 C++ 代码中避免使用“与”之前,请理解作用域的使用是有时间和地点的。下划线约定是private
Python开发者之间的社会契约,而大多数语言都没有这样的约定。所以,如果你使用的语言支持作用域,那么在任何变量前面使用“或” ,就像在 Python 中需要加下划线一样。protected
private
protected
私人……某种程度上
现在,在极少数情况下,你可能会有一个实例变量,它绝对、绝对、永远不应该在类之外直接修改。在这种情况下,你可以在变量名前加上两个下划线 ( __
),而不是一个。
这实际上并没有使其成为私有的;相反,它执行了一些称为名称修改的操作:它更改变量的名称,在前面添加一个下划线和类的名称。
在 的情况下class Starship
,如果我们将其更改self.shields
为self.__shields
,则其名称将被改写为self._Starship__shields
。
因此,如果您知道名称修改的工作原理,您仍然可以访问它......
uss_enterprise = Starship()
uss_enterprise._Starship__shields
>>> True
需要注意的是,如果要使其正常工作,尾部下划线不能超过一条。(__foo
并且__foo_
会被破坏,但__foo__
不会)。但是,PEP 8 通常不鼓励使用尾部下划线,所以这有点没有意义。
顺便说一句,双下划线( )名称修饰的目的__
实际上与私有作用域无关;它只是为了防止某些技术场景下的名称冲突。事实上,你使用它可能会遭到一些 Python 忍者的严厉批评__
,所以请谨慎使用。
特性
正如我之前所说,getter 和 setter 通常毫无意义。然而,有时它们也有其用途。在 Python 中,我们可以以这种方式使用属性,并且还能实现一些非常巧妙的技巧!
通过在方法前添加 来简单地定义属性@property
。
我最喜欢的属性技巧是使方法看起来像一个实例变量......
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
elif 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
当我们使用这个类时,我们可以将其视为engine_strain
对象的实例变量。
uss_enterprise = Starship()
uss_enterprise.engine_strain
>>> 0
很美,不是吗?
不幸的是,我们无法以同样的方式进行修改。 engine_strain
uss_enterprise.engine_strain = 10
>>> Traceback (most recent call last):
>>> File "<stdin>", line 1, in <module>
>>> AttributeError: can't set attribute
在这种情况下,这确实有意义,但在其他情况下可能并非你想要的。为了好玩,我们也为属性定义一个 setter;至少让它的输出比那个可怕的错误更美观。
@engine_strain.setter
def engine_strain(self, value):
print("I'm giving her all she's got, Captain!")
我们在方法前面添加了装饰器 (decorator @NAME_OF_PROPERTY.setter
)。我们还必须接受一个value
参数(当然self
,在之后),并且绝对不能再添加其他参数。你会注意到,在这种情况下,我们实际上并没有对参数进行任何操作value
,这对于我们的例子来说没有问题。
uss_enterprise.engine_strain = 10
>>> I'm giving her all she's got, Captain!
这好多了。
正如我之前提到的,我们可以将它们用作实例变量的 getter 和 setter。以下是一个简单的示例:
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!")
我们只是在这些函数所关注的变量前面加了下划线,以此向其他人表明我们打算自己管理这个变量。getter 函数相当平淡无奇,显而易见,只需要提供预期的行为即可。setter 函数才是真正有趣的地方:我们击溃了任何试图叛变的行为。这位船长是不会换人的!
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!
技术兔子线索:如果你想创建类属性,那需要你进行一些 hacking。网上有很多解决方案,如果你需要,可以去研究一下!
要是我不指出的话,肯定会有不少 Python 书呆子来找我麻烦,其实还有另一种方法,不用装饰器就能创建属性。所以,顺便说一下,这个方法也行得通……
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)
(是的,最后一行存在于任何函数之外。)
与往常一样,有关属性的文档包含更多信息,以及一些有关属性的更巧妙的技巧。
遗产
最后,我们回到第一行再看一眼。
class Starship(object):
还记得为什么它(object)
在那里吗?我们继承自 Python 的object
类。啊,继承!它就在那里。
class USSDiscovery(Starship):
def __init__(self):
super().__init__()
self.spore_drive = True
self._captain = "Gabriel Lorca"
这里唯一真正的谜团是那一super().__init__()
行。简而言之,super()
指的是我们继承的类(在本例中是Starship
),并调用它的初始化方法。我们需要调用它,所以USSDiscovery
拥有与 相同的所有实例变量Starship
。
当然,我们可以定义新的实例变量(self.spore_drive
),并重新定义继承的实例变量(self._captain
)。
我们实际上可以直接用 调用该初始化函数Starship.__init__()
,但如果我们想更改继承自的内容,就必须同时修改该行。这种super().__init__()
方法最终更简洁,也更易于维护。
遗留注意事项:顺便说一句,如果您使用的是 Python 2,那么这一行会稍微丑一些:super(USSDiscovery, self).__init__()
。
在你问之前:是的,你可以用 实现多重继承class C(A, B):
。它实际上比大多数语言都好用!无论如何,但你肯定会遇到一些麻烦,尤其是在使用 的时候super()
。
继续上课!
如您所见,Python 类与其他语言略有不同,但一旦您习惯了它们,它们实际上会更容易使用。
但是,如果你曾经使用过 C++ 或 Java 等类密集型语言编写代码,并且假设在 Python 中需要类,那么我要告诉你一个惊喜:你真的完全不需要使用类!
在 Python 中,类和对象只有一个目的:数据封装。如果你需要将数据和操作数据的函数放在一个方便的单元中,那么类是最佳选择。否则,就别费心了!完全由函数组成的模块绝对没有问题。
审查
呼!你还在等我吗?关于 Python 中的类,你猜对了多少个惊喜?
让我们回顾一下...
-
该
__init__(self)
函数是初始化程序,我们在其中进行所有变量的初始化。 -
方法(成员函数)必须将其
self
作为其第一个参数。 -
类方法必须将 设置
cls
为其第一个参数,并@classmethod
在函数定义的上一行添加装饰器。它们可以访问类变量,但不能访问实例变量。 -
静态方法与类方法类似,不同之处在于它们不接受
cls
修饰符作为第一个参数,并且以装饰器开头@staticmethod
。它们无法访问任何类或实例变量或函数。它们甚至不知道自己属于某个类。 -
实例变量(成员变量)应该首先在构造函数内部 声明
__init__(self)
。与大多数其他面向对象语言不同,我们不在构造函数外部声明它们。 -
类变量或静态变量在任何函数之外声明,并在类的所有实例之间共享。
-
Python 中没有私有成员!在成员变量或方法名前加上下划线 (
_
),是为了告诉开发人员不要乱动它。 -
如果在成员变量或方法名前加上两个下划线 ( ),Python 会使用名称重整 ( )
__
来更改其名称。这样做的主要目的是为了防止名称冲突,而非隐藏内容。 -
你可以将任何方法变成属性(看起来像成员变量),只需将装饰器
@property
放在其声明上方即可。这也可以用于创建getter。 -
您可以通过将装饰器放在函数上方来为属性创建设置器(例如) 。
foo
@foo.setter
foo
-
一个类(例如)可以通过这种方式
Dog
从另一个类(例如)继承: 。执行此操作时,还应该使用该行启动初始化程序以调用基类的初始化程序。Animal
class Dog(Animal):
super().__init__()
-
多重继承是可行的,但它可能会给你带来噩梦。用钳子处理。
与往常一样,我建议您阅读文档以了解更多信息:
准备好写一些 Python 类了吗?赶紧动手吧!
感谢deniska
( grym
Freenode IRC #python
) 和@wangonya (Dev) 提出的修改建议。