超简单的Python教程系列——第5篇:类
Posted 飞天程序猿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第5篇:类相关的知识,希望对你有一定的参考价值。
类和对象:许多开发人员的生计。面向对象编程是现代编程的支柱之一,因此 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
该类。
从 Python 3 开始,我们也可以这样声明:
class Starship:
就个人而言,考虑到 The Zen Of Python关于“显式胜于隐式”的那句话,我喜欢第一种方式。让我们明确一点,这两种方法在 Python 3 中效果一样,不用纠结这种,继续往下。
旧版注释:如果你希望你的代码在 Python 2 上运行,你必须加上(object)
.
方法
我要跳到这儿来说.
def warp(self, factor):
显然,这是一个成员函数或方法。在 Python 中,我们将self
第一个参数作为第一个参数传递给每个方法。之后,我们可以拥有任意数量的参数,就像使用任何其他函数一样。
我们实际上不必调用第一个参数self
;无论如何,它都会起作用。但是这是作为一种风格,我们总是在那里使用这个名字。没有正当理由违反该规则。
“但是,但是……你简直就是自己打破了规则!看到下一个功能了吗?”
@classmethod
def make_sound(cls):
你可能还记得在面向对象编程中,类方法是在类的所有实例(对象)之间共享的方法。类方法从不接触成员变量或常规方法。
如果你还没有注意到,我们总是通过点运算符访问类中的成员变量:self.
。因此,为了使其更加清晰,我们不能在类方法中这样做,我们调用第一个参数cls
。事实上,当调用类方法时,Python 将类传递给该参数,而不是传递对象。
对于类方法,我们还必须将装饰器 @classmethod
放在函数声明的正上方。这告诉 Python 语言你正在创建一个类方法,并且你不仅仅对参数self
的名称进行了创建。
上面的那些方法可以这样调用......
uss_enterprise = Starship() # Create our object from the starship class
# Note, we arent 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()
最后两行都将以完全相同的方式打印出“Vrrrrrrrrrrrrrrrrrrrrr”。(请注意,我在之前的函数中提到了cls.sound
。)
类与静态方法
与许多其他语言不同,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++,那么你可能也习惯于考虑作用域(私有、保护、公共)及其传统假设:变量应该是私有的,而函数应该是私有的(通常) 公开。获取者和设置者统治着所有!
我对 C++ 面向对象编程方面也很熟练的,我不得不说,我认为 Python 处理作用域问题的方法远远优于典型的面向对象作用域规则。一旦你掌握了如何用 Python 设计类,这些原则可能会应用到你其他语言的标准实践中……我坚信这是一件好事。
准备好了吗?你的变量实际上不需要是私有的。
是的,我刚刚听到后面那个 Java 书呆子提出疑问。“但是……但是……我将如何防止开发人员篡改任何对象的实例变量?”
通常,这种担忧是建立在三个有缺陷的假设之上的。让我们先把这些设置好:
- 几乎可以肯定,使用你的类的开发人员没有直接修改成员变量的习惯,就像他们习惯在烤面包机中插入叉子一样。
- 如果他们确实在烤面包机上插了叉子,众所周知,后果是他们是白痴,而不是你。
- “如果你知道为什么不应该用金属物体从烤面包机中取出粘住的吐司,那么你就可以这样做。”
换句话说,使用你的类的开发人员可能比你更了解他们是否应该修改实例变量。
现在,有了这个,我们接近 Python 中的一个重要前提:没有实际的“私有”范围。我们不能只是在变量前面加上一个花哨的小关键字来使其私有。
我们可以做的就是在名字前面加上一个下划线,像这样:self._engine
.
这个下划线并不神奇。对于使用你的类的任何人来说,这只是一个警告标签:“我建议你不要轻易修改它。我正在用它做一些特别的事情。”
现在,在你坚持_
所有实例变量名称的开头之前,想想这个变量实际上是什么,以及你如何使用它。直接修改它真的会导致问题吗?在我们的示例类的情况下,正如它现在所写的那样,不。这实际上是完全可以接受的:
uss_enterprise.engine_speed = 6
uss_enterprise.engage()
另外,注意到一些东西吗?我们没有编写一个 getter 或 setter!在任何语言中,如果 getter 或 setter 在功能上与直接修改变量相同,那么它们绝对是一种浪费。这也是是 Python 如此干净的语言的原因之一。
你还可以将此命名约定与你不打算在类外使用的方法一起使用。
注意:在你离开并避开private
你protected
的 Java 和 C++ 代码之前,请了解作用域是有时间和地点的。下划线约定是 Python 开发人员之间的一种社会契约,大多数语言都没有这样的约定。因此,如果你使用的是具有作用域的语言,请在 Python 中使用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 ninjas 那里得到一些__
严重的问题,所以要谨慎使用它。
属性
正如我之前所说,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: cant set attribute
在这种情况下,这确实是有道理的,但它可能不是你在其他时候想要的。只是为了好玩,让我们也为我们的属性定义一个 setter;至少有一个输出比那个可怕的错误更好。
@engine_strain.setter
def engine_strain(self, value):
print("Im giving her all shes got, Captain!")
我们在方法之前加上装饰器@NAME_OF_PROPERTY.setter
。我们还必须接受一个单一的value
参数(当然是在self
之后),除此之外什么都没有。你会注意到在这种情况下我们实际上并没有对value
参数做任何操作。
uss_enterprise.engine_strain = 10
>>> Im giving her all shes 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!
说明:如果你想创建类属性用于进行一些hack测试。网上有很多解决方案,如果你需要这个,去研究吧,这儿也不做过多的阐述!
如果我没有指出的话,一些 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 中只有一个目的:数据封装。如果你需要将数据和操作数据的函数放在一个方便的单元中,那么类是你的最佳选择。否则,不要使用!完全由函数组成的模块也没问题。
总结
让我们来复习一下:
__init__(self)
函数是初始化函数,这是我们进行所有变量初始化的地方。- 方法(成员函数)必须
self
作为它们的第一个参数。 - 类方法必须
cls
作为它们的第一个参数,并且装饰器@classmethod
位于函数定义的正上方。他们可以访问类变量,但不能访问实例变量。 - 静态方法类似于类方法,不同之处在于它们不作为
cls
第一个参数,并且前面有装饰器@staticmethod
。他们不能访问任何类或实例变量或函数。他们甚至不知道他们是一个类的一部分。 - 实例变量(成员变量)应该先在
__init__(self)
里面声明。与大多数其他面向对象的语言不同,我们不会在构造函数之外声明它们。 - 类变量或静态变量在任何函数之外声明,并在类的所有实例之间共享。
- Python 中没有私有成员!在成员变量或方法名称前加上下划线 (
_
) 以告诉开发人员不要乱用它。 - 如果在成员变量或方法名称前加上两个下划线 (
__
),Python 将使用name mangling更改其名称。这更多是为了防止名称冲突而不是隐藏东西。 - 你可以通过将装饰器放在其声明上方的行中,将任何方法变成一个属性(它看起来像一个成员变量)。
@property
这也可以用来创建getter。 - 你可以通过将装饰器
@foo.setter
放在函数foo
之上来为属性(例如foo
)设置setter。 - 一个类(例如
Dog
)可以继承另一个类(例如Animal
):class Dog(Animal):
当你这样做时,你还应该使用super().__init__()
调用基类的初始化函数的来启动你的初始化函数。 - 多重继承是可以的,但它可能会给你带来噩梦。小心处理!
以上是关于超简单的Python教程系列——第5篇:类的主要内容,如果未能解决你的问题,请参考以下文章