Python基础之面向对象
1 面向对象
Python
从设计之初就已经是一门面向对象的语言,正因为如此,在Python
中创建一个类和对象是很容易的
1.1 类方法
1.1.1 实例方法
普通方法,默认有个self
参数
1.1.1.1 实例调用
在类的内部,使用 def
关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数 self
, 且为第一个参数,self
代表的是类的实例,而非类
class Test:
def prt(self):
print(self)
print(self.__class__)
t = Test()
t.prt()
输出:
<__main__.Test instance at 0x100771878>
__main__.Test
从执行结果可以很明显的看出,self
代表的是类的实例,代表当前对象的地址,而 self.class
则指向类
1.1.1.2 类调用
我们知道,实例方法的调用方式其实有 2 种,既可以采用类对象调用,也可以直接通过类名调用。
通常情况下,我们习惯使用类对象调用类中的实例方法。但如果想用类调用实例方法,不能像如下这样:
class CLanguage:
def info(self):
print("我正在学 Python")
#通过类名直接调用实例方法
CLanguage.info()
运行上面代码,程序会报出如下错误:
Traceback (most recent call last):
File "D:\python3.6\demo.py", line 5, in <module>
CLanguage.info()
TypeError: info() missing 1 required positional argument: 'self'
其中,最后一行报错信息提示我们,调用 info()
类方式时缺少给 self
参数传参。这意味着,和使用类对象调用实例方法不同,通过类名直接调用实例方法时,Python
并不会自动给 self
参数传值。
self
参数需要的是方法的实际调用者(是类对象),而这里只提供了类名,当然无法自动传值。
因此,如果想通过类名直接调用实例方法,就必须手动为 self 参数传值。例如修改上面的代码为:
class CLanguage:
def info(self):
print("我正在学 Python")
clang = CLanguage()
#通过类名直接调用实例方法
CLanguage.info(clang)
再次运行程序,结果为:
我正在学 Python
可以看到,通过手动将 clang 这个类对象传给了 self 参数,使得程序得以正确执行。实际上,这里调用实例方法的形式完全是等价于 clang.info()
上面的报错信息只是让我们手动为 self 参数传值,但并没有规定必须传一个该类的对象,其实完全可以任意传入一个参数,例如:
class CLanguage:
def info(self):
print(self,"正在学 Python")
#通过类名直接调用实例方法
CLanguage.info("zhangsan")
运行结果为:
zhangsan 正在学 Python
可以看到,"zhangsan"
这个字符串传给了 info()
方法的 self
参数。显然,无论是 info()
方法中使用 self 参数调用其它类方法,还是使用 self 参数定义新的实例变量,胡乱的给 self 参数传参都将会导致程序运行崩溃。
总的来说,Python
中允许使用类名直接调用实例方法,但必须手动为该方法的第一个 self
参数传递参数,这种调用方法的方式被称为非绑定方法
。
用类的实例对象访问类成员的方式称为绑定方法
,而用类名调用类成员的方式称为非绑定方法。
1.1.2 类方法
类方法,默认有个 cls
参数(注意,绑定的不是类对象),可以被类
和对象
调用,需要加上 @classmethod
装饰器
我们在调用类方法时,无需显式为 cls 参数传参。
和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已
class CLanguage:
#类构造方法,也属于实例方法
def __init__(self):
self.name = "test"
self.add = "test"
#下面定义了一个类方法
@classmethod
def info(cls):
print("正在调用类方法",cls)
# 通过类直接调用类方法
CLanguage.info()
# 通过类的实例调用类方法(尽管这通常不是首选方式,因为类方法与特定实例的状态无关)
instance = CLanguage()
instance.info()
注意
:如果没有@classmethod
,则 Python
解释器会将 fly()
方法认定为实例方法,而不是类方法
1.1.3 类静态方法
静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中。
静态方法没有类似 self、cls
这样的特殊参数,因此 Python
解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。
静态方法需要使用@staticmethod
修饰,例如:
class CLanguage:
@staticmethod
def info(name,add):
print(name,add)
静态方法的调用,既可以使用类名
,也可以使用类对象
,例如:
#使用类名直接调用静态方法
CLanguage.info("test","测试")
#使用类对象调用静态方法
clang = CLanguage()
clang.info("Python","测试Python")
运行结果为:
test 测试
Python 测试Python
在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能,但在一些特殊的场景中(例如工厂模式中),使用类方法和静态方法也是很不错的选择
1.1.4 类属性
类属性是属于类的属性,由类的所有实例共享。这意味着类属性在类的所有实例之间是公共的,对一个实例的类属性的修改会影响到所有其他实例。类属性通常在类级别定义,使用类名直接访问。
示例:
class MyClass:
class_attribute = "这是一个类属性"
def __init__(self, instance_attribute):
self.instance_attribute = instance_attribute
# 创建两个实例
obj1 = MyClass(1)
obj2 = MyClass(2)
# 修改类属性
MyClass.class_attribute = "更新后的类属性"
# 访问类属性
print(obj1.class_attribute) # 输出: 更新后的类属性
print(obj2.class_attribute) # 输出: 更新后的类属性
1.1.5 成员属性
成员属性是属于类的每个实例的属性,每个实例都有自己的独立副本。成员属性通常在类的构造函数 __init__
中定义,并且每个实例的成员属性只对该实例可见。
示例:
class MyClass:
def __init__(self, instance_attribute):
self.instance_attribute = instance_attribute # 成员属性
# 创建两个实例
obj1 = MyClass(1)
obj2 = MyClass(2)
# 访问成员属性
print(obj1.instance_attribute) # 输出: 1
print(obj2.instance_attribute) # 输出: 2
在这个例子中,instance_attribute 是成员属性,每个实例 obj1 和 obj2 都有自己的 instance_attribute 值。对 obj1 的 instance_attribute 的修改不会影响 obj2 的 instance_attribute。
1.1.6 其他相关方法
type()
:判断对象类型,isinstance()
:判断类实例,dir()
:获得一个对象的所有属性和方法
>>> type(123)
<type 'int'>
stu=Student("tset",12)
print(isinstance(stu,Student))
结果:True
print(dir(stu))
结果:['_Student__no', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'info', 'name']
1.2 继承
1.2.1 单继承
Python
同样支持类的继承,如果一种语言不支持继承,类就没有什么意义。派生类的定义如下所示:
class DerivedClassName(BaseClassName):
<statement-1>
.
<statement-N>
子类(派生类 DerivedClassName)会继承父类(基类 BaseClassName)的属性和方法。
BaseClassName(实例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以用表达式,基类定义在另一个模块中时这一点非常有用:
class DerivedClassName(modname.BaseClassName):
#!/usr/bin/python3
#类定义
class people:
#定义基本属性
name = ''
age = 0
#定义私有属性,私有属性在类外部无法直接进行访问
__weight = 0
#定义构造方法
def __init__(self,n,a,w):
self.name = n
self.age = a
self.__weight = w
def speak(self):
print("%s 说: 我 %d 岁。" %(self.name,self.age))
#单继承示例
class student(people):
grade = ''
def __init__(self,n,a,w,g):
#调用父类的构函
people.__init__(self,n,a,w)
self.grade = g
#覆写父类的方法
def speak(self):
print("%s 说: 我 %d 岁了,我在读 %d 年级"%(self.name,self.age,self.grade))
s = student('ken',10,60,3)
s.speak()
输出结果为:
ken 说: 我 10 岁了,我在读 3 年级
1.2.2 多继承
Python
同样有限的支持多继承形式。多继承的类定义形如下例:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
<statement-N>
需要注意圆括号中父类的顺序
,若是父类中有相同的方法名,而在子类使用时未指定,python从左至右
搜索 即方法在子类中未找到时,从左到右查找父类中是否包含方法。
#!/usr/bin/python3
#类定义
class people:
#定义基本属性
name = ''
age = 0
#定义私有属性,私有属性在类外部无法直接进行访问
__weight = 0 #前面是两个下划线
#定义构造方法
def __init__(self,n,a,w):
self.name = n
self.age = a
self.__weight = w
def speak(self):
print("%s 说: 我 %d 岁。" %(self.name,self.age))
#单继承示例
class student(people):
grade = ''
def __init__(self,n,a,w,g):
#调用父类的构函
people.__init__(self,n,a,w)
self.grade = g
#覆写父类的方法
def speak(self):
print("%s 说: 我 %d 岁了,我在读 %d 年级"%(self.name,self.age,self.grade))
#另一个类,多重继承之前的准备
class speaker():
topic = ''
name = ''
def __init__(self,n,t):
self.name = n
self.topic = t
def speak(self):
print("我叫 %s,我是一个演说家,我演讲的主题是 %s"%(self.name,self.topic))
#多重继承
class sample(speaker,student):
a =''
def __init__(self,n,a,w,g,t):
student.__init__(self,n,a,w,g)
speaker.__init__(self,n,t)
test = sample("Tim",25,80,4,"Python")
test.speak() #方法名同,默认调用的是在括号中参数位置排前父类的方法
输出结果为:
我叫 Tim,我是一个演说家,我演讲的主题是 Python
1.3 方法重写
如果父类方法的功能不能满足需求,可以在子类重写父类的方法,实例如下:
#!/usr/bin/python3
class Parent: # 定义父类
def myMethod(self):
print ('调用父类方法')
class Child(Parent): # 定义子类
def myMethod(self):
print ('调用子类方法')
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法
super(Child,c).myMethod() #用子类对象调用父类已被覆盖的方法
输出结果为:
调用子类方法
调用父类方法
super(当前类名, self).函数()
,super()
函数是用于调用父类(超类)的一个方法
1.4 类私有属性方法和专有方法
1.4.1 类的私有属性
__private_attrs
:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs
有些时候,或许会看到以一个下划线开头的实例变量名,比如_name
,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。
双下划线
开头的实例变量是不是一定不能从外部访问呢,其实也不是。不能直接访问__name
是因为Python解释器对外把__name
变量改成了_Student__name
,所以,仍然可以通过_Student__name
来访问__name
变量:
class Student():
__name = '测试'
stu=Student()
stu._Student__name
'测试'
1.4.2 类的私有方法
__private_method
:两个下划线开头
,声明该方法为私有方法,只能在类的内部调用 ,不能在类的外部调用。self.__private_methods
#!/usr/bin/python3
class JustCounter:
__secretCount = 0 # 私有变量
publicCount = 0 # 公开变量
def count(self):
self.__secretCount += 1
self.publicCount += 1
print (self.__secretCount)
counter = JustCounter()
counter.count()
counter.count()
print (counter.publicCount)
print (counter.__secretCount) # 报错,实例不能访问私有变量
输出结果为:
1
2
2
Traceback (most recent call last):
File "test.py", line 16, in <module>
print (counter.__secretCount) # 报错,实例不能访问私有变量
AttributeError: 'JustCounter' object has no attribute '__secretCount'
1.4.3 类专有方法
类的专有方法(前后两个下划线):
__new__
:在实例创建之前被调用的,因为它的任务就是创建实例然后返回该实例对象,是个静态方法
依照Python官方文档的说法,__new__
方法主要是当继承一些不可变class
时(比如int, str, tuple), 提供给你一个自定义这些类的实例化过程的途径。还有就是实现自定义的metaclass__init__
:当实例对象创建完成后被调用的,然后设置对象属性的一些初始值,通常用在初始化一个类实例的时候。是一个实例方法__del__
:析构函数,释放对象时使用__repr__
:打印,转换__setitem__
:按照索引赋值__getitem__
:按照索引获取值__len__
:获得长度__cmp__
:比较运算__call__
:函数调用,直接对实例进行调用__add__
::加运算__sub__
:减运算__mul__
:乘运算__truediv__
:除运算__mod__
:求余运算__pow__
:乘方__iter__
:方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()
方法并通过StopIteration
异常标识迭代的完成。__next__
:方法(Python 2 里是 next())会返回下一个迭代器对象__getattr__
:动态返回一个属性
1.4.4 __slots__
正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义class:
class User():
pass
然后,尝试给实例绑定一个属性:
>>> s = User()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
但是,只能给一个实例绑定,另一个实例不能用
class User():
pass
u = User()
u.name= 'Test'
u1 = User()
print(u1.name)
直接报错
如果想要限制class的属性怎么办?比如,只允许对User实例添加name和age属性。为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__
变量,来限制该class能添加的属性:
>>> class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
由于score
没有被放到__slots__
中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。
注意
:使用__slots__
要,__slots__
定义的属性仅对当前类起作用
,对继承的子类是不起作用的:
>>> class GraduateStudent(Student):
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
除非在子类中也定义__slots__
,这样,子类允许定义的属性就是自身的__slots__
加上父类的__slots__
。
1.5 @property
Python内置的@property
装饰器负责把一个方法
变成属性
调用的
class Student(object):
@property
def score(self):
return self._score
@score.setter
def score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value
@property
的实现比较复杂,我们先考察如何使用。把一个getter
方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@score.setter
,负责把一个setter
方法变成属性赋值,于是,我们就拥有一个可控的属性操作:
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!
注意到这个神奇的@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法来实现的。
还可以定义只读属性,只定义getter
方法,不定义setter
方法就是一个只读属性:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2014 - self._birth
上面的birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
1.6 动态类
1.6.1 type
type()
函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()
函数创建出Hello
类,而无需通过class Hello(object)...的定义:
>>> def fn(self, name='world'): # 先定义函数
... print('Hello, %s.' % name)
...
>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
>>> h = Hello()
>>> h.hello()
Hello, world.
>>> print(type(Hello))
<type 'type'>
>>> print(type(h))
<class '__main__.Hello'>
要创建一个class对象,type()函数依次传入3个参数:
- class的名称;
- 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
- class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。
通过type()
函数创建的类和直接写class
是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。
正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。
1.6.2 metaclass
1.6.2.1 简介
除了使用type()
动态创建类以外,要控制类的创建行为,还可以使用metaclass
。
metaclass,直译为元类,简单的解释就是:当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。
但是如果我们想创建出类呢,那就必须根据metaclass创建出类,所以:先定义metaclass
,然后创建类。
连接起来就是:先定义metaclass
,就可以创建类,最后创建实例。
所以,metaclass
允许你创建类或者修改类。换句话说,可以把类看成是metaclass
创建出来的“实例”。
metaclass
是Python面向对象里最难理解,也是最难使用的魔术代码。
我们先看一个简单的例子,这个metaclass可以给我们自定义的MyList增加一个add方法:
定义ListMetaclass,按照默认习惯,metaclass的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
# metaclass是创建类,所以必须从`type`类型派生:
class ListMetaclass(type):
def __new__(cls, name, bases, attrs):
attrs['add'] = lambda self, value: self.append(value)
return type.__new__(cls, name, bases, attrs)
python2 中这样写
class MyList(list):
__metaclass__ = ListMetaclass # 指示使用ListMetaclass来定制类
python3 中这样写
class MyList(list, metaclass=ListMetaclass): # 在 Python 3 中使用 metaclass 关键字
pass
当我们写下__metaclass__ = ListMetaclass
语句时,魔术就生效了,它指示Python
解释器在创建MyList时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。
__new__()
方法接收到的参数依次是:当前准备创建的类的对象;类的名字;类继承的父类集合;类的方法集合。
测试一下MyList是否可以调用add()方法:
>>> L = MyList()
>>> L.add(1)
>>> L
[1]
而普通的list没有add()方法:
>>> l = list()
>>> l.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'
1.6.2.2 动态修改
动态修改有什么意义?直接在MyList定义中写上add()方法不是更简单吗?正常情况下,确实应该直接写。
但是,总会遇到需要通过metaclass修改类定义的。ORM就是一个典型的例子。
ORM
全称Object Relational Mapping
,即对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样,写代码更简单,不用直接操作SQL语句。
要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
让我们来尝试编写一个ORM框架。
编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待他写出这样的代码:
class User(Model):
# 定义类的属性到列的映射:
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
# 创建一个实例:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
# 保存到数据库:
u.save()
其中,父类Model和属性类型StringField、IntegerField是由ORM框架提供的,剩下的魔术方法比如save()全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但ORM的使用者用起来却异常简单。
现在,我们就按上面的接口来实现该ORM。
首先来定义Field类,它负责保存数据库表的字段名和字段类型:
class Field(object):
def __init__(self, name, column_type):
self.name = name
self.column_type = column_type
def __str__(self):
return '<%s:%s>' % (self.__class__.__name__, self.name)
在Field的基础上,进一步定义各种类型的Field,比如StringField,IntegerField等等:
class StringField(Field):
def __init__(self, name):
super(StringField, self).__init__(name, 'varchar(100)')
class IntegerField(Field):
def __init__(self, name):
super(IntegerField, self).__init__(name, 'bigint')
下一步,就是编写最复杂的ModelMetaclass了:
class ModelMetaclass(type):
def __new__(cls, name, bases, attrs):
if name=='Model':
return type.__new__(cls, name, bases, attrs)
mappings = dict()
for k, v in attrs.iteritems():
if isinstance(v, Field):
print('Found mapping: %s==>%s' % (k, v))
mappings[k] = v
for k in mappings.iterkeys():
attrs.pop(k)
attrs['__table__'] = name # 假设表名和类名一致
attrs['__mappings__'] = mappings # 保存属性和列的映射关系
return type.__new__(cls, name, bases, attrs)
以及基类Model:
class Model(dict):
__metaclass__ = ModelMetaclass
def __init__(self, **kw):
super(Model, self).__init__(**kw)
def __getattr__(self, key):
try:
return self[key]
except KeyError:
raise AttributeError(r"'Model' object has no attribute '%s'" % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k, v in self.__mappings__.iteritems():
fields.append(v.name)
params.append('?')
args.append(getattr(self, k, None))
sql = 'insert into %s (%s) values (%s)' % (self.__table__, ','.join(fields), ','.join(params))
print('SQL: %s' % sql)
print('ARGS: %s' % str(args))
当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找__metaclass__
,如果没有找到,就继续在父类Model中查找__metaclass__
,找到了,就使用Model中定义的__metaclass__
的ModelMetaclass来创建User类,也就是说,metaclass可以隐式地继承到子类,但子类自己却感觉不到。
在ModelMetaclass中,一共做了几件事情:
- 排除掉对Model类的修改;
- 在当前类(比如User)中查找定义的类的所有属性,如果找到一个Field属性,就把它保存到一个
__mappings__
的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误; - 把表名保存到
__table__
中,这里简化为表名默认为类名。
在Model类中,就可以定义各种操作数据库的方法,比如save(),delete(),find(),update等等。
我们实现了save()方法,把一个实例保存到数据库中。因为有表名,属性到字段的映射和属性值的集合,就可以构造出INSERT语句。
编写代码试试:
u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd')
u.save()
输出如下:
Found model: User
Found mapping: email ==> <StringField:email>
Found mapping: password ==> <StringField:password>
Found mapping: id ==> <IntegerField:uid>
Found mapping: name ==> <StringField:username>
SQL: insert into User (password,email,username,uid) values (?,?,?,?)
ARGS: ['my-pwd', 'test@orm.org', 'Michael', 12345]
可以看到,save()方法已经打印出了可执行的SQL语句,以及参数列表,只需要真正连接到数据库,执行该SQL语句,就可以完成真正的功能。