欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Python】类与对象(上)

程序员文章站 2024-03-16 10:14:55
...

类与对象简述.

面向对象编程(Object-Oriented Programming)是一种基于对象(Object)的编程范式。对象是对一种事物的抽象,人们在解决问题的时候倾向于将复杂的事物简单化,于是就会思考这些对象都是由哪些部分组成的。通常我们将对象划分为两个部分:静态部分与动态部分,也即属性与行为。静态部分通过成员变量来表示,指出这一类型的对象具有哪些属性,例如class People会由name、age、sex等属性;而动态部分则通过成员函数来表示,指出这一类型的对象能够做哪些事、完成哪些动作,例如People可以run()、eat()、sleep()等。
OOP的编程方式区别于面向过程编程(Procedure-Oriented Programming),POP解决问题的出发点是这个问题的解决需要经过哪些步骤,然后用函数将这些步骤一个个实现,解决的时候依次调用就可以了。但我们考虑一种实际情况,例如银行的办理柜台,柜台的服务人员并不知道何时何人会来办理何种业务,根本无法划定解决问题的步骤,POP也就无从谈起了。而OOP可以将柜台服务人员抽象成一类对象,比如class Server,它拥有属性name、Server_id等,拥有动作deal()、sayHello()等。显然在处理这种问题时,OOP的编程方式要比POP更为自然高效。
常用的高级语言中,C语言是纯面向过程的,这也是大多数人接触编程的入门语言。而C++、Java以及Python等高级语言都是支持面向对象编程的,而后两种只支持OOP编程,C++因为要兼顾C语言,支持OOP和POP. 在具体介绍Python面向对象编程的语法之前,我们简单看一个class的定义,对它有一个直观的感受:

class Example:
    def SayHello(self,name):
        self.name=name
        print("This is %s"%name)
one=Example()
one.SayHello("Esperanto.")

【Python】类与对象(上)
class作为一个关键字,指明这是一个类的定义,这一点和C++完全一致。我们在class Example中用def关键字定义了一个成员方法SayHello(),后续就可以通过Example类的对象实例one.SayHello()来调用这个方法。

类的定义.

class Example:
    def SayHello(self,name):
        self.name=name
        print("This is %s"%name)
one=Example()
one.SayHello("Esperanto.")

还是这一段类的定义代码,我们注意到SayHello()的函数定义基本上与前面介绍的函数定义一致,唯一的区别在于SayHello()方法必须要有一个名为self的形式参数,并且一定要放在参数列表的第一位(实际上它也可以不叫self,你非要写this这样的名字也无所谓,只是一种约定俗成)。这个self参数代表了将来在主程序中要创建的某个对象本身。我们要清楚,class Example在定义时,它并没有创建任何实例对象(但它其实创建了一个类对象),极端一点,如果你定义了一个class,而你在后续的代码中完全没有使用这个类来创建实例对象,这个类定义时的那么代码一句都不会执行到。这一点很合理,就好像你定义了一个函数,可你没有调用它,它如何执行自己代码?

对象的创建.

Python中存在着两种对象,类对象与实例对象。这和C++以及Java中的面向对象语法是存在一定区别的。类对象是唯一的,并且它在执行class语句时就已经创建了:

class Example:
    def SayHello(self,name):
        self.name=name
        print("This is %s"%name)
    print("Class object has constructed.")

【Python】类与对象(上)
我们注意到除了class Example的定义之外,我们没有任何代码,但程序还是输出了"类对象已被创建"的文字,这说明类对象确实是在class Example语句执行时被创建的。接下来我们在这段代码后面加一句print(type(Example)),它的结果为:<class 'type'>,这一语句说明了Example就是这一类对象的引用,type(Example)的结果是类对象的类型。看到这里大家可能会有一些疑问,先不着急,我们介绍完实例对象进行一个总结。

class Example:
    school="unknown"
    def SayHello(self,name):
        self.name=name
        print("Instance %s has constructed."%name)

one=Example()
one.SayHello("Esperanto")

print(type(one))
print(type(Example))

print(one.school)
print(Example.school)

print(one.name)

【Python】类与对象(上)
这段代码中的one就是一个实例对象,它是通过类对象Example()来创建的(就像调用函数那样),实例对象会继承类对象的属性。这段代码中类属性就是school,它是属于类对象Example的属性,但实例对象one显然也有这个属性。另外我们看到type(one)和type(Example)的结果,可以做出这样的总结:Example作为类,它给出了这一类的所有对象的设计蓝图,那么这一蓝图本身也是一个对象,它的名字就是类名Example;而实例对象作为类——蓝图的实例化,相当于对蓝图中没有细致描绘的地方进行了补充,实例对象的类型就是它的蓝图类。另外我们发现,通过类对象是无法访问实例属性(实例对象的属性)的;而实例对象是可以访问类属性的,前提是没有一个实例属性和类属性重名,否则实例对象只能访问自己的实例属性,所以作为一种规范,我们想要访问类属性的时候,一般通过类名来访问。如果我们加一句print(Example.name),编译器就会给出这样的错误信息AttributeError: type object 'Example' has no attribute 'name'. 熟悉C++以及Java的人可能隐隐约约感受到了,Python这里的类属性就相当于静态成员变量,它不为哪一个实例对象所有,它是类的公共财产,与静态成员变量相类似的还有静态成员方法,我们后面介绍。下面是两段设计静态成员变量以及静态成员方法的代码,一段是C++、一段是Java,可以帮助熟悉这两种语言的人快速了解Python中的"类属性"“类方法"以及"静态方法的概念”。

#include <iostream>

using namespace std;

class Node
{
public:
	int x = 0;
	int y = 0;
	static int count ;
	Node(int x, int y)
	{
		this->x = x;
		this->y = y;
		count = count + 1;
		cout << "Node num:" << count << endl;
		cout << &count << endl;
	}
};

int Node::count = 0;

int main()
{
	Node a = Node(1,2);
	Node b = Node(3, 4);
	Node c = Node(5, 6);
	return 0;
}

【Python】类与对象(上)
可以看到count被声明为static,从而它是一个为class Node所有的静态成员变量(也可以叫类变量),并不像成员变量x和y那样所有的实例对象人手一份,count是被所有实例属性共享的,每个实例对象的count都是同一个count,从我们输出的count的地址可以看出。至于C++中类的定义语法以及静态成员变量的类外初始化,有机会我们详细介绍。

public class Test
{
    public int x;
    public int y;
    public static int count=0;

    public Test(int x,int y)
    {
        this.x=x;
        this.y=y;
        System.out.println("Node num:"+(++count));
    }

    public static void main(String[] args)
    {
        Test a = new Test(1,2);
        Test b = new Test(3,4);
        Test c = new Test(7,9);
        
    }
}

【Python】类与对象(上)
结果也是符合静态变量的行为的。
另外在Python中,还可以通过赋值运算符来增加、修改类对象与实例对象的属性。修改属性还较为常见,至于通过=还增加属性,在C++中是难以想象的。

class Example:
    school="unknown"
    def SayHello(self,name):
        self.name=name
        print("Instance %s has constructed."%name)

one=Example()
one.SayHello("Esperanto")

one.name="Knuth"
print(one.name)
one.age=20
print(one.age)
Example.school="CS"
print(one.school)

one.school="SC"
print(Example.school)
print(one.school)

【Python】类与对象(上)
上述代码中,我们将one的name属性值改为了Knuth(一位图灵奖获得者),并且添加了实例属性age。后面我们修改了Example的类属性school,发现通过one访问的school也是修改后的值(虽然我们不提倡这样访问)。值得注意的是,我们one.school="SC"这一语句的效果并不是修改类变量,而是直接增加了一个实例属性给one,这也是为什么不提倡通过实例对象来访问类属性。

构造器与析构器.

这部分内容是所有支持面向对象的语言都会有的,构造器(constructor)也叫构造函数,析构器(destructor)也叫析构函数,不同的叫法而已。构造器的作用是在创建实例对象时,进行实例属性的初始化以及其他的初始化操作,构造器会在创建实例对象时被自动调用,那么问题就在于什么时刻才是"创建实例对象时"呢。

class Student:
    def __init__(self):
        print("Constructor.")
        self.school="Unknown."
        self.name="Kevin."
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))
        
me=Student()
me.Say()

【Python】类与对象(上)
可以看到,me=Student()这一行代码调用了class Student的构造器__init__()方法,输出内容中的"Constructor."证明了这一点,并且也完成了我们既定的初始化任务。实际上,在C++和Java中,构造器都是一个以类名作为函数名的成员方法,它没有返回值。但因为在Python中将类名认为是类对象,而__init__()才是构造器,其实是大同小异的,换汤不换药。我们在上面给出的类定义例子中的那段C++以及Java代码中,都有着构造器部分的代码。
__init__()方法的名字是固定不变的,如果用户没有显式地定义它,Python会提供一个默认的构造器。C++是明确支持函数重载的,构造器作为一种特殊的函数,也支持重载,只要程序员在定义函数时使得这些函数之间能够被编译器区分开,那么在实际运行时编译器就能够根据给定的参数调用合适函数。可惜的是,Python中并不支持函数重载,像下面这样的一段类定义:

class Student:
    def __init__(self):
        print("Constructor 1.")
        self.school="Unknown."
        self.name="Kevin."
    def __init__(self,school):
        print("Constructor 2.")
        self.school=school+"."
        self.name="Kevin."
    def __init__(self,school,name):
        self.school=school+"."
        self.name=name+"."
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))

虽然不会产生编译错误,但实际上后面定义的__init__()方法覆盖了前面的版本,最后留下的只有一个,但我们可以通过一个小技巧,来实现__init__()方法的重载,途径就是我们前面提到过的不定长参数(这段代码中的类似school+"."这样的语句属于运算符重载的功能,后面会介绍)。

class Student:
    def __init__(self,*parameter):
        if len(parameter)==0:
            self.school="Unknown."
            self.name="Kevin."
        elif len(parameter)==1:
            self.school=parameter[0]
            self.name="Kevin"
        elif len(parameter)==2:
            self.school=parameter[0]
            self.name=parameter[1]
        else:
            print("Undefined")
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))
        
me=Student()
me.Say()
you=Student("CSU")
you.Say()
he=Student("XXX","Whokonws")
he.Say()
she=Student(1,2,3,4)

我们在__init__()方法中判断用户输入了几个参数,以此来选择合适的初始化方式,这种方法虽然一定程度上实现了构造器的重载,但代价是在构造对象时做了太多的条件判断,效率并不是太高。C++和Java都支持函数的重载,我们简单给出一段代码,看看正统的函数重载(overload)是什么样子:

#include <iostream>
using namespace std;
class Test
{
private:
	int one;
	int two;
	int three;

public:
	Test()
	{
		one = 1;
		two = 2;
		three = 3;
		cout << "Constructor 1." << endl;
	}

	Test(int a) :one(a), two(2), three(3)
	{
		cout << "Constructor 2." << endl;
	}

	Test(int a, int b) :one(a), two(b), three(3)
	{
		cout << "Constructor 3." << endl;
	}
	
	void say()
	{
		cout << "one:" << one << " two:" << two << " three:" << three << endl;
	}
};

int main()
{
	Test a = Test();
	a.say();
	Test b = Test(9);
	b.say();
	Test c = Test(10, 15);
	c.say();
	return 0;
}

【Python】类与对象(上)
这段C++代码中我们在Test类中定义了三个构造器,分别是无参,单参以及两个参数,main()方法中我们分别定义三个对象,给出的参数数量分别对应于无参、单参以及两个参数的构造器。根据输出结果,我们可以判断,函数重载功能是正确进行了。至于代码中C++一些细节,例如构造器的列表初始化、explicit限制的单参构造函数来防止无意识下的自动转型等,有兴趣可以看C++的教材。Java也支持函数重载,不过Java在构造器方面和C++有些不同,C++不允许同一个类中的重载构造器相互调用,而Java允许同一个类中构造器相互调用。涉及到类与类的继承关系后,Java在构造子类对象时,可以通过super()方法来传递参数给父类的构造器以完成父类对象的构造,而C++只能通过列表初始化。

重载(overload)函数是函数的一种特殊情况,为方便使用,C++允许在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同,也就是说用同一个函数完成不同的功能。这就是重载函数。重载函数常用来实现功能类似而所处理的数据类型不同的问题。不能只有函数返回值类型不同。

????,言归正传,对应于构造器还有析构器,析构器用于释放对象占用的资源,在删除对象和回收对象资源时被自动调用。如果没有显式调用del语句的话,默认是在程序结束时自动调用,Python解释器会自动检测当前是否还存在未被释放的对象,如果存在,则自动使用del语句来释放对象。C++中语法,在类名前面加一个腭化符号~来指明这是析构器,而Java由于存在垃圾自动回收机制,并不让程序员接触到析构器,那我们自然也乐得清闲,不用理会繁琐的内存问题(最经典应该是C语言中指针的malloc()和free()了,C++中换成了new和delete,换汤不换药)。

class Student:
    def __init__(self,name):
            self.name=name
    def __del__(self):
        print("Destruct ",self.name+".")
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))
        
me=Student("me")
you=Student("you")
he=Student("he")

【Python】类与对象(上)
我们在代码中并没有显式调用del语句来删除对象,但在程序的输出中,显然还是调用了__del__()方法,另外我们也可以发现一点,Python的析构顺序和构造顺序是一致的,显然这不会是栈式的内存分配策略,因为像C++那样的栈式内存分配,析构顺序和构造顺序应该完全相反。Python在栈中存放的是对象的引用,而实际的对象数据存放在堆中。需要禁止的是对一个已经被del的对象再次使用del语句,会引发NameError: name 'me' is not defined,而在C/C++这甚至会导致整个程序的崩溃。按照惯例,给出一段C++的代码(IntelliJ启动实在太慢了,所以不写Java),来看看C++中的析构顺序:

#include <iostream>
using namespace std;
class Test
{
private:
	int one;
	int two;
	int three;

public:
	Test()
	{
		one = 1;
		two = 2;
		three = 3;
		cout << "Constructor 1." << endl;
	}

	Test(int a) :one(a), two(2), three(3)
	{
		cout << "Constructor 2." << endl;
	}

	Test(int a, int b) :one(a), two(b), three(3)
	{
		cout << "Constructor 3." << endl;
	}
	
	void say()
	{
		cout << "one:" << one << " two:" << two << " three:" << three << endl;
	}

	~Test()
	{
		cout << "Destruct" << this->one << endl;
	}
};

int main()
{
	Test a = Test();
	a.say();
	Test b = Test(9);
	b.say();
	Test c = Test(10, 15);
	c.say();
	return 0;
}

【Python】类与对象(上)
这段代码中我们加入了析构器,根据输出结果,确实是最后构造的对象c最先被析构,而最先构造的对象a存活了最久的时间,很stack. C++中的this指针类似于Python中self的概念,其实也没什么区别,它们本身都不是该类的对象,this指向当前正在操作的对象,self引用的是当前操作的对象。

类方法.

类方法和前面提到的类属性类似,后者是一个属性,而前者是属于类的方法,我们提倡通过类名,也就是类对象来调用(虽然通过实例对象也可以调用到),但出于强调"类方法属于类"这个概念,提倡通过类名调用。在定义一个类方法时,我们在前面添加@classmethod来指明这是个类方法,其余并无不同。

class Student:
    num=0   #class_attr
    
    @classmethod #class_method
    def count(cls):
        print("Student num:",cls.num)
        
    def __init__(self,name):
        self.name=name
        Student.num=Student.num+1
        
    def __del__(self):
        print("Destruct ",self.name+".")
        
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))
        
me=Student("me")
Student.count()
you=Student("you")
Student.count()
he=Student("he")
he.count()

【Python】类与对象(上)
着眼于class Student中的count()方法,count()方法的cls参数表示类本身,和self一样,这也是一个约定俗成的参数名,你要是看它不爽非要换个别的名字,也无可厚非,只是不提倡这么做。在类方法count()中,我们只能访问类的属性,而无法访问实例属性。这句话在C++中是另一个说法,静态成员方法中只能使用静态成员属性,说的都是同一件事。我们可以通过类对象Student调用count()方法,也可以通过实例对象名来调用count(),但还是那句话,推荐使用类对象调用属于类的东西,不论是类属性Student.num还是类方法Student.count()

静态方法.

说实话不是很理解为什么Python在已经有了类方法的基础上还搞一个静态方法的概念,就为了那个"无参"吗?静态方法大体上和类方法一样,修饰器变成了@staticmethod,在静态方法中可以访问类属性,而无法访问实例属性。静态方法的参数列表可以是无参的,而不像类方法那样要有一个cls作为参数代表类本身。

class Student:
    num=0   #class_attr
    
    @staticmethod #class_method
    def count():
        print("Student num:",Student.num)
        
    def __init__(self,name):
        self.name=name
        Student.num=Student.num+1
        
    def __del__(self):
        print("Destruct ",self.name+".")
        
    def Say(self):
        print("School:%s\nName:%s"%(self.school,self.name))
        
me=Student("me")
Student.count()
you=Student("you")
Student.count()
he=Student("he")
he.count()

【Python】类与对象(上)
上面一段介绍类方法的代码,小小地修改就变了使用静态方法的代码,@staticmethod以及在count()函数体通过Student.num来访问类属性num。很疑惑为什么要有这个设定,如果大家知道,可以在下方评论。【Python】类与对象(上)