Python学习笔记9:类

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的类定义有以下几个特点:

  1. 类内部的所有方法声明中第一个参数必须为self

    事实上命名也可以不是self,但self是Python约定的命名,最好不要使用其它。

  2. 魔术方法__init__是类的初始化方法,相当于构造函数。

  3. 对象的属性使用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为何这样设计很不解,毕竟这样做有连个缺陷:

  1. 类方法定义必须加入self参数,否则会报错,很多Python新手应该都遇到过。
  2. 无法实现类的静态方法。

虽然这两个缺陷也不是不能克服,毕竟类静态方法更多的是在优化程序性能上的作用,强行使用对象方法也不是不行。

  • 此外类的方法也支持默认参数等,这些都是函数已有的特性,这里不再一一赘述。
  • 复习可以阅读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 . "\n");
    }
}
$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的继承和多态这里不做深入讲解,在未来的某篇笔记中再做分析。

posted @ 2021-03-17 15:50  魔芋红茶  阅读(81)  评论(0编辑  收藏  举报