Python - 类

150 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第28天,点击查看活动详情

一、定义类:

  • Python中的类(class)实际上就是数据类型。
  • Python的所有内置数据类型都是类,Python提供了强大的工具,用于对类的所有行为进行控制。
  • 类可用class语句进行定义:
class MyClass:
	body
		
# body部分由一系列Python语句组成,通常包含变量赋值和函数定义语句。不过赋值和函数定义语句都不是必需的,body可以只包含一条pass语句。

# 为了能让类标识符足够醒目,按惯例每个单词首字母应该大写。类定义完之后,只要将类名称作为函数进行调用,就可以创建该类的对象,即类的实例:

instance = MyClass()

将类的实例当作结构或记录使用:

  • 类的实例可被当作结构(structure)或记录(record)使用。

  • 通过在类的定义中包含初始化方法__init__,可以实现对实例的字段进行自动初始化。
  • 每次创建类的新实例时,该函数都会运行,新建实例本身将作为函数的第一个参数self代入。__init__方法类似于Java中的构造器,但其实什么都没有构造,只是可以用于初始化类的字段。与Java和C++不同,Python的类只能包含一个__init__方法。以下示例将会创建默认半径为1的圆:

按照惯例,__init__方法的第一个参数名始终是self。当__init__运行时,self会被置为新建的Circle实例❶。
接下来就会用到类的定义。首先创建一个Circle实例对象❷。
下一行代码是基于radius字段已被初始化这一事实❸。
radius字段的值可以被覆盖❹,
这就导致了最后一行打印出的结果与上一条print语句的不一样了❺。
  • Python还有更像构造器的__ new__方法,在对象创建时将会被调用,返回未经初始化的对象。除非要创建str或int这种不可变类型的子类,或者要通过元类(metaclass)修改对象的创建过程,否则很少会覆盖已有的__new__方法。

二、实例变量:

  • 实例变量是OOP(面向对象编程)最基本的特性。再来看一下Circle类:
class Circle:
	"""docstring for Circle"""
	# 1
	def __init__(self):
		self.radius = 1

# radius就是Circle的实例变量。
# 也就是说,Circle类的每个实例都拥有各自的radius副本,每个实例存储在副本radius中的值可以各不相同。
# 在Python中,可以按需创建实例变量,只要给类实例的字段赋值即可:

instance.variable = value

# 如果变量(variable)尚不存在,则会自动创建,__init__正是如此创建radius变量的。
  • 实例变量在每次使用时,不论是赋值还是访问,都需要显式给出包含该变量的实例,即采用instance.variable的格式。单单引用variable并不是对实例变量的引用,而是对当前执行的方法中的局部变量的引用。

三、方法:

  • 方法是与某个类关联的函数。
  • 上面已经介绍了特殊的__init__方法,当创建实例时会对新实例调用该方法。

  • 与__init__类似,area方法被定义为类定义内部的函数。方法的第一个参数一定是发起调用的实例,按惯例命名为self。在许多编程语言中,这时的实例常被命名为this,而且是作为隐含参数的,从来不会被显式传递。但Python的设计理念是,更愿意让事情明确。
  • 如果方法定义了能接受的参数,就可以在调用时使用。以下版本的Circle为__init__方法添加了一个参数,以便能创建给定半径的圆,而无须在对象创建之后再另行设置:
class Circle:
	def __init__(self,radius):
		self.radius = radius
	def area(self):
		return self.radius * self.radius * 3.14159

# 注意,这里用到了两个radius。self.radius是实例变量,单个radius则是局部的函数参数。
# 这两个radius可不是一回事!在实际编程时,局部的函数参数也许应该用r或rad之类的名称,以免混淆。

# 有了上述定义的Circle,就能通过调用一次Circle类生成任意半径的圆对象了
c = Circle(5)

# Python的所有标准函数特性,都可以用于方法,这些特性如参数默认值、不定参数、关键字参数等。__init__的第一行可以定义为:

def __init__(self,radius = 1):

# 然后在调用Circle时,带或不带参数都是可以的。Circle()将返回半径为1的圆,Circle(3)将返回半径为3的圆。

小结:

  • 先在实例的命名空间中查找方法名。如果方法在该实例中被修改或添加过,那就会优先调用该实例中的方法,而不是类或父类(superclass)中的方法。
  • 如果在实例的命名空间中找不到该方法,就会找到实例的类型,也就是其所属的类,并在其中查找该方法。在以上示例中,实例c的类型为Circle,也就是说c属于Circle类。
  • 如果方法还未找到,就查找父类中的方法。
  • 如果方法找到了,就会像普通的Python函数一样被直接调用,函数的第一个参数将是instance,方法调用中的其他参数则整体向右平移一个位置传入函数。因此instance.method(arg1, arg2, ...)就会成为class.method(instance,arg1, arg2, ...)。‘

四、类变量:

  • 类变量(class variable)是与类关联的变量,而不是与类的实例关联,并且可供类的所有实例访问。类变量可用于保存类级别的数据。
  • 类变量是通过类定义代码中的赋值语句创建的,而不是在__init__函数中创建的。类变量创建之后,就可被类的所有实例看到。可以用类变量创建pi值,供Circle类的所有实例访问:

  • 在类的方法中也可以访问类变量,只要带上类名即可。在Circle.area的定义中就是这么做的,这里的area函数明确引用了Circle.pi。实际运行达到了预期效果,从类中获取了正确的pi值并用于计算:

  • 或许有人会反对在上述类的方法中把类名写死。通过特殊的__class__属性可以避免这种写法,该属性可供Python类的所有实例访问。__class__属性会返回实例所属的类,例如:
# 1
>>> print(c.__class__)
<class '__main__.Circle'>
[Finished in 135ms]

# 2
>>>print(Circle)
<class '__main__.Circle'>
[Finished in 135ms]

  • 名为Circle的类在系统内部是用一个抽象数据结构表示的,该数据结构正是从c的__class__属性获取的,c就是Circle类的一个实例。以下示例由c获取Circle.pi的值,而无须显式引用Circle类名:

  • 在area方法内部,就可以用这种写法摆脱对Circle类的显式引用,只要用self.class.pi替换Circle.pi即可。

类变量的特异之处:

  • Python在查找实例变量时,如果找不到具有该名称的实例变量,就会在同名的类变量中查找并返回类变量值。只有在找不到合适的类变量时,Python才会报错。
  • 类变量可以高效地实现实例变量的默认值,只需创建一个具有合适默认值的同名类变量,就能避免每次创建类实例时初始化该实例变量的时间和内存开销。但这也很容易在无意之中引用了实例变量而不是类变量,不会有任何报错信息。
  • 首先,尽管c未包含名为pi的关联实例变量,但可以引用变量c.pi。
  • Python首先会寻找实例变量pi。如果找不到实例变量,Python就会查找Circle并找到类变量pi:

  • 上述结果可能是满足需求的,也可能不满足。这种技术用起来很方便,但容易出错,所以请小心使用。
  • 如果尝试将c.pi当作真正的类变量来使用,在某个实例中对其进行修改,并希望让所有实例都要看到这种修改,那将会发生什么?这里用到了Circle之前的定义:

  • 以上例子并没有像真正的类变量那样工作,c1现在有了自己的pi副本,与c2访问的Circle.pi并不相同。
  • 因为对c1.pi的赋值在c1中新建了一个实例变量,它不会对类变量Circle.pi产生任何影响,所以才会如此。后续对c1.pi的查找都会返回这个实例变量的值。而后续对c2.pi的查找则会先在c2中查找实例变量pi,可是没有找到,然后就会转而返回类变量Circle.pi的值。如果需要更改类变量的值,请通过类名进行访问,而不要通过实例变量self。

五、静态方法和类方法:

  • Java之类的编程语言还带有静态方法,Python类也拥有与静态方法明确对应的方法。
  • 此外,Python还拥有类方法,要比静态方法更高级一些。

静态方法:

  • 注意,这里用到了文档字符串。在实际的模块代码中,可能还会加入更多的信息字符串,在类的文档字符串中给出可用的方法,在方法的文档字符串中包括用法信息:

类方法:

  • 类方法与静态方法很相像,都可以在类的对象被实例化之前进行调用,也都能通过类的实例来调用。

六、继承:

如果要在绘图程序中使用这些类,必须定义每个实例在绘图表面的位置信息。在每个实例中定义x、y坐标,即可实现这一点:

class Square:
	def __init__(self,side=1,x=0,y=0):
		self.side = side
		self.x = x
		self.y = y
class Circle:
	def __init__(self,radius=1,x=0,y=0):
		self.radius = radius
		self.x = x
		self.y = y
  • 这种方式能起作用,但如果要扩展大量的形状类,就会产生大量重复代码,因为可能要让每种形状类都具备这种位置的概念。毫无疑问,这正是在面向对象语言中使用继承的标准场景。
  • 不用在每个形状类中都定义变量x和y,而可以将各种形状抽象为一个通用的Shape类,并让定义具体形状的类继承自该通用类。
class Shape:
	def __init__(self,x,y):
		self.x = x
		self.y = y

# 声明 Square 继承 Shape

class Square(Shape):
	def __init__(self,side = 1 , x = 0 , y = 0):
		# 调用 Shape 的 __init__ 方法必须得调用
		super().__init__(x,y)
		self.side = side

# 声明 Circle 继承自 Shape		
class Circle(Shape):
	def __init__(self,r=1,x=0,y=0):
		# 调用 Shape 的 __init__ 方法必须得调用
		super().__init__(x,y)
		self.radius = r
  • 在Python中使用继承类通常有两个要求,在Circle类和Square类的粗体代码中可以看到这两个要求。第一个要求是定义继承的层次结构,在用class关键字定义类名之后的圆括号中,给出要继承的类即可。在上述代码中,Circle和Square都继承自Shape。第二个要求比较微妙一些,就是必须显式调用被继承类的__init__方法。Python不会自动执行初始化操作,但可以用super函数让Python找到被继承的类。初始化的工作在示例中由super().__ init__(x, y)这行代码来完成,这里将调用Shape的初始化函数,用适当的参数初始化实例。如果没有显式调用父类的初始化方法,则本例中的Circle和Square的实例就不会给实例变量x和y赋值。
  • 可以不用super来调用Shape的__init__,而是用Shape.init(self, x, y)显式给出被继承类的名字,同样能够实现在实例初始化完毕后调用Shape的初始化函数。从长远来看,这种做法不够灵活,因为对被继承类名进行了硬编码。如果日后整体设计和继承架构发生了变化,这就可能成为问题。但在继承关系比较复杂的时候,采用super会比较麻烦。因为这两种方案无法完全混合使用,所以请把代码中采用的方案清楚地记录在文档中备查。

七、类及实例变量的继承:

  • 实例可以继承类的属性。实例变量是和对象实例关联的,某个名称的实例变量在一个实例中只会存在一个。