Python学习笔记9:类
因为《Head Frist Python》一书的内容设置,所以我这个系列笔记也在这时候才介绍Python中的类。
本文内容和示例都基于笔者之前对Java和PHP运用的理解综合而成,和《Head First Python》一书关系不大,对原书内容感兴趣的强烈建议购买一本。
基本概念
在Python中使用类很简单,这里举一个最简单的例子:
class Test():
def __init__(self, a: int = 0):
self.a = a
def print(self) -> None:
print(self.a)
test = Test()
test2 = Test(2)
test.print()
test2.print()
输出
0
2
与其它编程语言相比,Python的类定义有以下几个特点:
-
类内部的所有方法声明中第一个参数必须为
self
。事实上命名也可以不是self,但self是Python约定的命名,最好不要使用其它。
-
魔术方法
__init__
是类的初始化方法,相当于构造函数。 -
对象的属性使用
self.a
的方式在初始化方法中声明并初始化。
现在我们进一步讨论Python为何会有这些特点。
在Python的类定义中,self
的作用和C++
中的对象指针或者Java
中的对象引用很相似,但其额外承担了初始化对象属性的作用。
至于为何Python需要在所有类的内部方法参数中加入self
,我们可以用下面的方式验证:
class Test():
def __init__(self, a: int = 0):
self.a = a
def print(self) -> None:
print(self.a)
test = Test()
test2 = Test(2)
Test.print(test)
Test.print(test2)
输出
0
2
我们仅仅修改了最后两行代码,使用类名调用print
并传入相应的对象,输出结果与上面的相同。
可以看到Python使用了类似类静态方法的方式来实现对象方法,而这种实现方式的前提是静态方法必须要接收一个对象引用,而这个对象引用就是方法定义参数self
,所以从这个角度上说,self是必不可缺的。
当然这种方式给编码会带来一些小困惑,虽然也能很快适应,但依然对Python为何这样设计很不解,毕竟这样做有连个缺陷:
- 类方法定义必须加入
self
参数,否则会报错,很多Python新手应该都遇到过。 - 无法实现类的静态方法。
虽然这两个缺陷也不是不能克服,毕竟类静态方法更多的是在优化程序性能上的作用,强行使用对象方法也不是不行。
- 此外类的方法也支持默认参数等,这些都是函数已有的特性,这里不再一一赘述。
- 复习可以阅读Python学习笔记4:函数
魔术方法
Python的类都继承自object,而object本身定义了很多特殊方法:
print(dir(object))
输出:
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
这些双下划线左右包裹方式命名的函数被称作魔术方法,通常都有特殊的用途,比如:
__dir__
:使用内部函数dir()
时候调用,收集对象的属性和方法等信息。__eq__
:使用==
逻辑运算符时候调用。__str__
:使用str()
时候调用。__repr__
:使用print()
时候调用。
通过重写这些魔术方法,我们可以改变类对象的一些行为,比如:
class Test():
def __init__(self, a: int = 0):
self.a = a
def print(self) -> None:
print(self.a)
def __repr__(self) -> str:
return "this is a Test object,a:"+str(self.a)
test = Test()
test2 = Test(2)
print(test)
print(test2)
输出
this is a Test object,a:0
this is a Test object,a:2
如果你学过C++的话,肯定会觉得熟悉,因为这就是某种意义上的运算符重载。
封装
我们都知道面向对象(OOP)是一个宏大的概念,主要包括封装、继承和多态。这部分内容不仅难学,而且还需要在项目中长期打磨才能领略其中的真谛。
这里之所以会在简单介绍完Python类的基本使用后介绍一点OOP概念,是因为Python的封装与其它流行语言有很大差别。
我们先写一个PHP的常用类结构,再写一个类似的Python类来对比说明。
<?php
class Calculator
{
private $a = 0;
private $b = 0;
/**
* 构造函数
* @param int $a
* @param int $b
*/
public function __construct($a, $b)
{
$this->a = $a;
$this->b = $b;
}
/**
* 求和
* @return int
*/
public function add()
{
$this->beforeRun(__FUNCTION__);
return $this->a + $this->b;
}
/**
* 执行数学计算前输出说明
* @param string $operateName
* @return void
*/
private function beforeRun($operateName)
{
print("calculator will operate " . $operateName . "
");
}
}
$calculator = new Calculator(1, 2);
print($calculator->add());
对应的python代码我们可以这样写:
class Calculator():
def __init__(self, a, b):
self.a = a
self.b = b
def add(self):
self.beforeRun("add")
return self.a+self.b
def beforeRun(self, operateName):
print("calculator will operate ", operateName)
cal = Calculator(1, 2)
result = cal.add()
print(result)
貌似没有任何问题,但是对OOP封装有所了解的就知道,两者的封装不同。
我们知道,在创建类的时候,对于类内部的属性和方法,都应该遵循最小访问权限这一封装规则。
所在在这个例子中,PHP中的属性均为private
,方法beforeRun
也是private
,对于类外部是不可见的,这样做是对的,毕竟在这个例子中Calculator
仅仅对外提供一个功能,即add()
调用。
而反观Python
的代码,并不能通过访问修饰符private/protected/public
来进行访问限定,这样的结果就是类的所有属性和方法对外部都是可见的,可以任意访问和修改。这会对OOP设计造成巨大破坏。
可能你会觉得这一个例子也没啥影响,但对于有大型应用团队开发经验的人都知道,一旦你开放某个本应该是私有的方法为公有,那你就不能怪某一天翻代码发现别人调用了这个方法,而导致某些很严重的系统问题,而且还要花很大力气去重构解决。
那Python能不能在没有访问限定符的情况下解决这个问题?答案是有的:
class Calculator():
def __init__(self, a, b):
self.__a = a
self.__b = b
def add(self):
self.__beforeRun("add")
return self.__a+self.__b
def __beforeRun(self, operateName):
print("calculator will operate ", operateName)
cal = Calculator(1, 2)
result = cal.add()
print(result)
# print(cal.__a)
# cal.__beforeRun("see")
如上边的例子中显示的那样,我们可以用双下划线来标记private
类型的属性和方法,而且在事实上的确也不能通过cal.__a
的方式调用,起到了封装的作用。
但需要说明的是,这种方式仅仅是看起来起到了封装的作用,事实上Python中对象的属性和方法是不存在访问限制的,你可以通过一些其它方式访问:
print(cal._Calculator__a)
就像上面这样,你可以通过特殊途径来访问到看似访问受限的对象属性,所以Python的这种私有声明也被叫做伪私有。
但是这依然给我们提供了一种实现OOP封装的途径,我们要尽量杜绝上面那样通过“歪门邪道”来进行不正常的对象属性、方法访问,那样会破坏OOP的封装原则,一旦我们有类似的需要,第一时间应该去重构类设计。
顺带一提,双下划线类似于private
,而单下划线类似于protected
,不过对于OOP的继承和多态这里不做深入讲解,在未来的某篇笔记中再做分析。