超简单的Python教程系列——第5篇:类

Posted 飞天程序猿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了超简单的Python教程系列——第5篇:类相关的知识,希望对你有一定的参考价值。

超简单的Python教程系列——第5篇:类_python

类和对象:许多开发人员的生计。面向对象编程是现代编程的支柱之一,因此 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篇:类的主要内容,如果未能解决你的问题,请参考以下文章

超简单的Python教程系列——第14篇:异步

超简单的Python教程系列——第7篇:循环和迭代器

超简单的Python教程系列——第3篇:项目结构和导入

超简单的Python教程系列——第6篇:错误异常

超简单的Python教程系列——第15篇:多线程

超简单的Python教程系列——第18篇:调试