分享

从C#到Python -- 4 类及面向对象

 无名小卒917 2014-11-11

如果你熟悉C#,那么对类(Class)和面向对象(Object Oriented)应该不会陌生。Python与C#一样,能够很好地支持面向对象的编程模式。本章对Python中面向对象编程的基本知识进行介绍,并将其与C#中的对应部分进行比较。

 

4.1  类的定义与实例化

4.1.1  类的定义

与C#一样,Python使用class关键字定义一个类。一个最简单的类定义语句如下:

1 class A:
2 pass

它等价于C#中的 class A{}。当然,以上语句没有任何实际意义,它只是告诉我们什么是定义一个类所必需的,即:class关键字,类名和冒号,pass关键字只用来占位,相当于C#中花括号的作用。

4.1.2  类的实例化

类是定义对象格式的模板,而对象则是类的实例,通过类创建对象的过程称为类的实例化。在C#中,需要使用new关键字实例化一个类,例如

A a = new A();  

在上条语句中,C#完成了两件事情:首先声明一个类型为A的变量a,然后用new运算符创建一个类型为A的对象,并将该对象的引用赋值给变量a。而在Python中没有new关键字,同时它是一种动态语言,不需要事先指定变量的类型,只需要:

a = A()

即可创建一个类型为A的对象,看起来好像是将类当作一个函数调用,返回值是新创建的对象。

 

4.2  类的成员变量

4.2.1  为类添加数据

通常我们利用类来定义各种新的数据类型,其中既包含数据内容,又包含对数据内容的操作。前面我们定义的类A暂时什么也不能做,因为它既没有数据,也没有操作。本节我们先讨论第一个问题,即如何为类添加数据。

在C#中,我们需要显示地对类的数据进行定义,例如:

1 class A {
2 public int x;
3 }

以上代码为类A定义了一个名为x的成员变量,用来存放整数型数据。对类实例化后,可以对此数据进行访问,例如:

1 A a = new A();
2 a.x = 2;
3 Console.Write(a1.x);

而Python类的数据添加方法与C#有一些不同,因为Python是一种动态语言,变量在使用之前不需要定义,所以你可以不在类定义中添加成员变量,而是在运行时动态地添加它们,例如:

1 class A:pass
2 a = A()
3 a.x =1
4  print a.x

当然上述方法不是一种好方法,也很少有人真正这么用(虽然语法上没有任何错误),类的成员变量最好是在类的初始化函数里面声明并初始化。

4.2.2  初始化函数

Python的类提供了类似C#构造函数的东西:__init__(注意前后是两个下划线),类在实例化时会首先调用这个函数。我们可以通过重写__init__函数,完成变量的初始化等工作。与C#不同的地方是,Python不支持无参数的初始化函数,你至少需要为初始化函数指定一个参数,即对象实例本身(self)。下面是一段简单的示例代码,在该代码中,我们重写了函数__init__,定义并初始化了一个类的成员变量 x:

1 class A:
2 def __init__(self):
3 self.x = 1
4 a = A()
5  print a.x

实际上Python允许在任何成员函数(不仅限于__init__)中声明类的成员变量,不过最好养成在初始化函数中声明成员变量的习惯。

另外,我们也可以定义带多个参数的初始化函数,如下:

1 class A:
2 def __init__(self,x):
3 self.x = x
4 a = A(1)
5  print a.x

当然,Python的类也有类似C#析构函数的东西:__del__函数,但不推荐使用它,Python的垃圾回收机制会帮你做好一切事情。

4.2.3  静态成员(类变量)

你可能已经注意到,在前面的代码中,成员变量x之前也有一个self,这说明该成员属于于类的实例(而不是类本身),这类似C#中的非静态成员。那么如何在Python中定义与类的静态成员呢?这个很简单,只需要在类定义中直接初始化一个变量即可,例如:

1 class A:
2 y = 2
3  print A.y
4  #输出2

与C#不同的是,Python中对象可以直接访问类的静态成员,例如:

1 class A:
2 y = 2
3 a = A()
4  print a.y
5  #输出2

但对象不能为类的静态成员赋值,例如:

复制代码
1 class A:
2 y = 2
3 a = A()
4 a.y = 3
5  print a.y
6  print A.y
7  #输出3 2
复制代码

事实上赋值语句a.y = 3相当于为实例a添加了一个非静态成员y,而类A的静态成员A.y没有发生改变(即使同名也不会冲突,因为它们不在同一个命名空间内)。 可以直接用A.y = 3为类变量赋值,这点与C#是一致的。

4.2.4  私有成员

在编写C#程序时,我们可以对类的成员使用不同的访问修饰符定义它们的访问级别。例如对于公有成员可以使用修饰符public,而对于私有成员(仅限于类中的成员可以访问)则使用修饰符private,此外C#还提供了保护成员(protected修饰)和内部成员(internal修饰)等。

但在Python中所有的成员都是公有成员,你可以不受限制地访问它们(变量和方法均是如此)。Python建议在类成员前面加_(一个下划线)的为私有成员,但这仅仅是个建议,没有语法上的强制限制,也就是说你还是可以调用_开头的变量和方法。不过,Python里以两个下划线开头的类成员就不能直接访问了,例如:

1 class A:
2 def __init__(self):
3 self.__x = 1
4 a = A()
5  print a.__x
6  #输出 AttributeError: A instance has no attribute '__x'

但实际上我们还是可以不受限制地访问这个“私有”成员,只需要在变量名(或方法名)前加下划线和类名称,如:

1 print a._A__x
2  #输出 1

我认为Python应该彻底封掉这个口子,支持真正的私有成员,这样才符合面向对象编程中“封装”的基本思想。

 

4.3  类的方法

4.3.1  为类添加方法

方法是对类数据内容的操作,在Python中,定义类的方法与定义一个普通的函数在语法上基本相同(见上一章《函数及函数编程》)。C#程序员需要注意的是,在类中定义的常规方法的第一个参数总是该类的实例,即self。同时注意在方法中引用类的另一个方法必须使用类名加方法名的形式,下面是一个定义类方法的简单例子:

复制代码
1 class A:
2 def prt(self):
3 print "My Name is A"
4 def reprt(self):
5 prt(self) #错误,NameError: global name 'prt' is not defined
6   A.prt(self) #正确
7  a = A()
8 a.prt()
9 a.reprt()
复制代码

不能定义一个不操作实例的方法(这点在开始时经常会被忘记,请注意):

复制代码
1 class A:
2 def prt():
3 print "My Name is A"
4
5 a = A()
6 a.prt()
7  #TypeError: prt() takes no arguments (1 given)
复制代码

4.3.2  静态方法

Python与C#一样支持静态方法。在C#中需要使用关键字static声明一个静态方法,而在Python中是通过静态方法修饰符@staticmethod来实现的,下面是示例代码:

1 class A:
2 @staticmethod
3 def prt():
4 print "My Name is A"
5
6 A.prt()

如你所见,静态方法可以直接被类调用,它没有常规方法那样的特殊行为(默认的第一个参数是self等),你完全可以将静态方法当成一个用属性引用方式调用的普通函数。任何时候定义静态方法都不是必须的,静态方法能实现的功能都可以通过定义一个普通函数来实现。一般认为,当有一堆函数仅仅为某一特定类编写时,将这些函数包装成静态这种方式可以提供使用上的一致性。(以上修改自《Python精要参考》)

4.3.3  方法重载

在C#中,同一个类中的两个或多个方法可以共享相同的名称,只要它们的参数(数量或类型)不同即可,这种过程被称为方法重载(Method overloading),方法重载是C#实现多态特性的方式之一。

很不幸,Python不支持同名方式的方法重载,虽然你可以在类中定义多个同名的方法,但前边定义的方法会被后边定义的所覆盖(Python使用名字来绑定一个对象)。但Python作为面向对象语言,自然不会丢掉方法重载这个面向对象的重要特性。

如前所述,C#重载方法的主要方式是定义不同类型或数量的参数。由于Python本身是动态语言,方法的参数是没有类型的,当调用传值的时候才确定参数的类型,故对参数类型不同的方法无需考虑重载。对参数数量不同的方法,则(大多数情况下)可以采用参数默认值来实现,具体内容就不再重复介绍了,可以参考上一章《函数及函数编程》。

4.3.4  运算符重载

运算符重载是实现多态的另外一种重要手段。在C#中,我们通过使用关键字operator定义一个运算符方法,并定义与所在类相关的运算符行为。在Python中,运算符重载的方式更为简单——每一个类都默认内置了所有可能的运算符方法,只要重写这个方法,就可以实现针对该运算符的重载。例如以下是重载加法操作:

复制代码
1 class A:
2 def __init__(self,sum = 0):
3 self.sum = sum
4 def __add__(self,x):
5 return A(self.sum + x.sum)
6 def __str__(self):
7 return str(self.sum)
8 a = A(1)
9 b = A(2)
10  print a + b
11  #输出 3
复制代码

Python具体的内建运算符方法列表,请参看《Python精要参考》中的表 3.10. 数学操作的方法

 

4.4  类的继承

面向对象的三个基本特征是封装、继承和多态,前面的内容已经涉及了封装和多态,本节介绍Python中类的继承。

继承是创建新类的机制之一,它通过对一个已有类进行修改和扩充来生成新类。这个原始的类被称为基类或超类,新生成的类称为该类的派生类或子类。当通过继承创建一个类时,它会自动'继承'在基类中定义的属性。一个子类也可以重新定义父类中已有的属性或定义新的属性,这也是实现多态的一种重要方式。(以上修改自《Python精要参考》)

4.4.1  单继承

Python用类名后加括号的方式实现继承,下面是一个简单的示例:

1 class A:
2 x = 1
3  class B(A):
4 x = 2
5  print B.x,B.y

与C#一样,Python中如果要引用子类的某个属性,会首先在子类中寻找,没有就去到父类中寻找它的定义,再没有的话,就一直向上找下去,知道找到为止(最不利的情况是找到object,如果还没有就只能报错了)。

方法的寻找方式与属性相同。子类的方法可以重定义父类的方法,也可以在子类中直接调用父类中的方法,方式如下:

BaseClass.method(self, arguments)

(以上部分修改自《Python学习笔记》)

4.4.2  多继承

Python支持多继承(C#需要用接口实现),实现方式很简单,只需要在类名后的括号中,写入用逗号分隔的多个父类名即可。不过当父类中有重名的属性时,需要了解Python的搜索顺序,具体可以参考《Python精要参考》的第七章,本文不再详细介绍。

4.4.3  接口与抽象类

Python没有C#中接口的概念,因为可以实现多重继承,使用接口的意义不大,而且Python的标准类基本包含C中接口的大部分功能(不过据说新版本的Python已经考虑要加入接口的功能了)。而C#中抽象类在Python中则可以通过一些变通的方法实现。常用的方法是用NotImplementedError异常模拟抽象类,示例代码如下:

复制代码
1 # -*- coding: utf-8 -*-
2  def abstract():
3 raise NotImplementedError("abstract")
4
5  class Car:
6 def __init__(self):
7 if self.__class__ is Car:
8 abstract()
9 print "Car"
10
11  class BMW(Car):
12 def __init__(self):
13 Car.__init__(self)
14 print "bmw"
15
16 bmw = BMW() #子类可以实例化
17  Car() #抽象类实例化会报错:NotImplementedError: abstract
复制代码

(以上代码引自《Python模拟抽象类》,见http://www./PHP/751.htm ,略有修改)

 

 

4.5  获取对象的信息

在Python这类动态编程语言中,我们经常需要检查一个类、对象或模块,以确定它是什么、它知道什么或它能做什么。在Python中,我们把这类功能叫做自省(Introspection),它非常类似于C#中的反射(Reflection)。

4.5.1  对象的特殊属性

除了用户自定义的类属性外,Python中的所有对象都拥有一些内置的特殊属性,它们可以帮助我们回答如下问题:

    * 对象的名称是什么(__name__)?
    * 这是哪种类型的对象(__class__)?
    * 对象知道些什么(__doc__)?
    * 对象能做些什么(__dict__)?
    * 对象(的类)继承自谁(__bases__)?
    * 它在哪一个模块中(__module__)?    

限于篇幅,本文不一一解释这些特殊属性的含义,你可以试着找一个对象(Python中一切皆对象,所以这些属性适用于所有类型的对象,包括字符串、数值、列表、元组、字典、函数、自定义类、类实例和类方法等),打印它的这些属性,不难明白它们是做什么用的。

4.5.2  自省函数

Python提供了很多有用的自省函数,帮助我们找到对象的有用信息,常用的自省函数包括:

    * id()  返回对象唯一的标识符
    * repr()  返回对象的标准字符串表达式
    * type()  返回对象的类型
    * dir() 返回对象的属性名称列表
    * vars()  返回一个字典,它包含了对象存储于其__dict__中的属性(键)及值
    * hasattr() 判断一个对象是否有一个特定的属性
    * getattr() 取得对象的属性
    * setattr() 赋值给对象的属性
    * delattr() 从一个对象中删除属性
    * callable() 测试对象的可调用性
    * issubclass() 判断一个类是另一个类的子类或子孙类
    * isinstance() 判断一个对象是否是另一个给定类的实例
    * super()   返回相应的父类

同样,我建议你在Shell中逐项试试这些函数的功能,以更好地体会自省的威力。

 

4.6  本章小结

本章介绍了Python中面向对象编程的基本知识,并与C#做了简单对比,要点如下:

(1) Python用class关键字、类名、括号加父类(可选)及冒号定义类,实例化类时不需要new关键字;
(2) Python可以对象中动态添加成员变量,但建议在__init__函数中添加成员变量并初始化;
(3) Python的类中可以通过名称前加双下划线定义私有变量(及私有方法);
(4) Python中定义类的方法与定义普通函数在语法上基本相同,区别是非静态方法的第一个参数总是类的实例,即self;
(5) 通过修饰符@staticmethod可以定义一个类的静态方法;
(6) Python对类方法重载和运算符重载的实现方式与C#有一定区别,不过我认为Python的方式更简单一些;
(7) Python支持类的单继承和多继承,但不支持接口,也不(直接)支持抽象类;
(8) 通过自省,可以在运行时获得对象的有用信息。

 

关于Python的面向对象编程还有很多高级主题,限于本人的能力和精力,不能一一详细介绍,推荐阅读文后的参考资料[2]。

 

进一步阅读的参考:

[1] 《Python面向对象初级教程》,大量示例代码加详细的注释,见http://www./wiki/doku.php?id=python面向对象初级教程
[2] 《Python核心编程(中文第二版》的第十三章《面向对象编程》,全面详细地讨论了Python的面向对象编程,不过也有一定深度,适合有一定基础后再阅读(下载地址:http://wenku.baidu.com/view/9d3bc1aedd3383c4bb4cd25c.html)。
[3]《Python 自省指南》,见http://www.ibm.com/developerworks/cn/linux/l-pyint/),关于Python自省的经典文章。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多