python和java一样,是一种面向对象的语言,作为一种面向对象的语言,python中同样有类、对象、属性、方法等常用的概念。
定义类
python中使用class关键字来定义一个类,比如我们定义一个people类,有姓名和年龄两个属性,可以执行走路的方法。
class People:
'''这是类的说明文档'''
name = ""
age = 0
def walk(self):
print("people is walking")
如上面的代码所写,和函数一样,我们也可以为类定义说明文档,位置在类体的第一行。
类的构造方法
python中的构造方法是__init__()方法,在创建类时,可以手动添加该方法。每当创建这个类的对象时,都会调用类的构造方法。
__ init__()中可以包含多个参数,但是第一个参数必须是self参数。如果没有定义类的构造函数,python会自动生成一个参数仅包含self的构造函数。
和java不同的是,python的语法不支持方法重载,所以一个类中只能有一个构造函数。
class People:
'''这是类的说明文档'''
name = "ZhangSan"
age = 18
def __init__(self):
print("不带参数的构造函数")
def walk(self):
print("people is walking")
类对象的创建和使用
类对象的创建
以上面定义的People类为例,我们将People类实例化为liSi对象。
liSi = People()
在python的类中,self是一个特殊参数,不需要手动传值,python会自动给它传值。
类对象的使用
可以直接使用已创建好的对象来访问其中的属性和方法。
print(liSi.name)
print(liSi.age)
liSi.walk()
#修改变量中的值
liSi.age = 30
print(liSi.age)
#给对象增加属性
liSi.height = 178
print(liSi.height)
#删除对象的属性
del liSi.age
print(liSi.age)
del liSi.height
print(liSi.height)
从上面的删除方法中可以看出,在删除age属性时,删除前的值为30,删除后的值为18;而在删除height属性时,删除后再调用该属性直接就会报错,这一点主要是因为前面通过liSi对象,赋值了age和height两个属性,实际上这两个属性都是实例变量,当删除了这两个实例变量后,age变为了类变量,而height就没有了。这一点后面的类变量和实例变量会讲到。
既然可以给对象动态增加属性,那当然也可以给类动态增加方法,不过需要注意的是,给对象动态添加的方法,不会自动将调用者绑定到self参数,因此,需要手动将调用者绑定到第一个参数。
def say(self):
print(self.name,self.age)
liSi.say = say
liSi.say(liSi)
#使用lambda表达式给对象动态添加方法
liSi.jump = lambda self:print("LiSi is jumping")
liSi.jump(liSi)
通过types模块下的MethodType,我们可以实现不用手动绑定self值给对象添加方法。
def run(self):
print(self.name,"is running")
from types import MethodType
liSi.run = MethodType(run,liSi)
liSi.run()
self详解
事实上,python只是规定,无论构造方法还是实例方法,都至少要包含一个参数,将其命名为self是一种约定俗成。python的每个类可以实例化成无数的对象,self参数就是用来区分这些类,确保每个类都只调用它自己的属性和方法。这一点就和java中的this非常类似。
类属性和实例属性
在python类中,根据变量的位置,可以将变量分为三种类型,类变量、实例变量和局部变量。
类变量
在类中,各个方法之外的变量,就叫做类变量,也叫做类的属性。比如下面的Author类的name和gender,就是类变量。
class Author:
name = "Andy"
gender = 0
def write(self):
print(self.name,"is","writing")
类变量的特点是,在所有实例中都共享类变量,类变量是公共的。类变量可以通过类名调用,也可以通过实例进行调用。
print(Author.name)
a = Author()
print(a.name)
需要注意的是,因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。
Author.name = "Bob"
print(Author.name)
print(a.name)
另外,通过对象是无法修改类变量的,通过对象对类变量进行赋值,其本质不再是修改类变量的值,而是给该对象定义新的实例变量,接下来的实例变量会详细介绍。
除了可以通过类名访问类变量之外,还可以通过类名给给整个类动态的添加类变量。
Author.nation = "China"
print(a.nation)
实例变量
实例变量指的是在类的方法内部,以self.变量名的方式定义的变量,其特点是只作用于调用方法的对象。实例变量只能通过对象名进行访问,无法通过类名访问。
class People:
name = "ZhangSan"
age = 18
def __init__(self,name,age):
self.name = name
self.age = age
def growUp(self):
self.age+=1
像这个People类,上面的name和age就是类变量,但是构造函数中的self.name和self.age是实例变量,也就是说,当调用该构造函数创建实例对象时,通过该实例对象调用的name和age属性已经变成了构造函数中的两个实例变量。同理,gouwUp()方法中的age操作的也是实例变量。
zhangSan = People("ZhangSan",18)
print(zhangSan.age)
liSi = People("LiSi",20)
print(liSi.age)
zhangSan.growUp()
print(zhangSan.age)
print(liSi.age)
上面说到,通过对象可以访问类变量,但是无法修改类变量的值。这是因为通过对象修改类变量的值,不是在修改类变量,而是在定义新的实例变量,例如对上面的Author类和a对象进行如下操作:
a.gender = 1
print(a.gender)
print(Author.gender)
这也是造成讲类对象的使用的时候,删除liSi对象的age属性和height属性造成的结果不同的原因。
简单而言,通过修改某个实例对象的值,既不会影响其他实例化的对象,也不会影响类中同名的变量,只会影响该对象自身的实例变量。
局部变量
局部变量就没有那么多弯弯绕了,局部变量就是只在类方法中作用的变量,通常情况下,当方法执行完毕,局部变量也会被销毁。
class Calculation:
def add(x,y):
s = x+y
return s
print(Calculation.add(1,2))
如上面的Calculation类中的变量s,就是一个局部变量。
实例方法、静态方法和类方法
和属性一样,类里的方法也可以进行细分,可以分为实例方法、静态方法和类方法。
实例方法
通常情况下,在类里定义的方法默认都是实例方法,构造方法本质上也是一个实例方法。实例方法最大的特点就是它最少要包含一个self参数,用于绑定调用该函数的实例对象。
实例对象支持通过对象名直接调用,也可以通过类名进行调用,此时需要手动传入self的值。
a.write()
Author.write(a)
类方法
类方法和实例方法类似,也最少需要一个参数,不过这个参数通常命名为cls,python会自动将类本身绑定给cls参数。
和实例方法不同的是,类方法需要用@classmethod进行修饰。
class Author:
name = "Andy"
gender = 0
def write(self):
print(self.name,"is","writing")
@classmethod
def die(cls):
print("大作家死了,并留下了一份巨著")
类方法推荐直接使用类名调用,不推荐使用实例进行调用。
Author.die()
静态方法
静态方法没有必须的参数,和全局空间的函数一样,唯一的区别就是静态方法定义在类这个空间里。也由于静态方法中没有self和cls这样的特殊参数,静态方法无法调用任何类属性和类方法。
静态方法使用@staticmethod来修饰。
class Author:
name = "Andy"
gender = 0
def write(self):
print(self.name,"is","writing")
@classmethod
def die(cls):
print("大作家死了,并留下了一份巨著")
@staticmethod
def born(address):
print("大作家出生在",address)
静态方法的调用既可以使用类名,也可以使用对象。
Author.born("北京")
a = Author()
a.born("巴黎")
描述符
python中使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。
从本质上来说,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理完全委托给描述符类。
描述符类基于__set__(self, obj, type=None),__ get__(self, obj, value)和__delete__(self, obj)三个方法,分别对应设置属性、读取属性和del属性。其中,实现了get和set方法的描述符被称为数据描述符;如果只实现了get方法,则称为非数据描述符。
实际上,在每次查找属性时,描述符协议中的方法都由对象的特殊方法__getattribute__()调用。也就是说,每次使用对象.属性,或者getattr(对象,属性值)的方式调用时,都会隐式的调用__getattribute__()方法,它会按照以下的顺序查找该属性:
1.验证该属性是否为实例对象的数据描述符
2.如果不是,就查看该属性是否能在类实例对象的__dict__中找到
3.最后,查看该属性是否为实例对象的非数据描述符
class revealAccess:
def __init__(self, initval = None, name = 'var'):
self.val = initval
self.name = name
def __get__(self, obj, objtype):
print("Retrieving",self.name)
return self.val
def __set__(self, obj, val):
print("updating",self.name)
self.val = val
class MyClass:
x = revealAccess(10,'var "x"')
y = 5
m = MyClass()
print(m.x)
m.x = 20
m.y = 8
print(m.x)
print(m.y)
从上面的例子可以看出,如果一个类的属性有数据描述符,那么每次查找这个数据时,都会调用描述符的get方法,并返回它的值;同样,每次对该属性赋值时,也会调用描述符的set方法。而没有描述符的y属性则不会。
虽然上面的例子没有__del__() 方法,但是当每次使用del 对象.属性或者delattr(类对象,属性)时,都会调用该方法。
property()函数
前面的代码中,我们一直在用对象.属性的方式来访问类中定义的属性,其实在面向对象的语言中,这种方式破坏了类的封装原则。就像java的类变量通常都写成private一样,类的属性通常都是不可见的,只允许通过类提供的方法来对其进行操作。因此,通常我们会在类中为各个属性提供getter和setter方法用来访问类的属性。
class People:
def __init__(self,name):
self.name = name
def setName(self,name):
self.name = name
def getName(self):
return self.name
def delName(self):
self.name = None
p = People("Jack")
print(p.getName())
p.setName("Rose")
print(p.getName())
p.delName()
print(p.getName())
python中提供了一个property()方法,可以用于在不破坏类的封装性的前提下,继续使用对象.属性的方式访问类中的属性。
property() 函数的完整语法为:属性名=property(fget=None, fset=None, fdel=None, doc=None),其中fget指用于获取该属性值的方法,fset指用来设置属性值的方法,fdel指用来删除属性值的方法,doc是一个文档字符串,用来说明此函数的作用。
比如刚才的People类我们可以改成:
class People:
def __init__(self,n):
self.__name = n
def setName(self,n):
self.__name = n
def getName(self):
return self.__name
def delName(self):
self.__name = None
name = property(getName, setName, delName, '对name属性进行操作')
help(People.name)
p = People("Jack")
p.name = "Rose"
print(p.name)
del p.name
print(p.name)
需要注意的是,由于getName()方法中需要返回name属性,如果使用self.name的话,又回去调用getName(),形成死循环。为了避免这种情况出现,程序中的name属性必须设置为私有属性,即__name。这个我还没学到,后面学到了再详细说。
@property装饰器
python中内置了三种装饰器,前面我们说到了两种@classmethod和@staticmethod,现在来说说最后一种@property。例如上面的People类,我们可以写成这样。
class People:
def __init__(self,n):
self.__name = n
@property
def name(self):
return self.__name
@name.setter
def name(self,n):
self.__name = n
@name.deleter
def name(self):
self.__name = None
p = People("Jack")
p.name = "Rose"
print(p.name)
del p.name
print(p.name)
可以看出使用@property装饰器和使用property()方法创建委托没有什么区别,需要注意的就是@property、@name.setter、@name.deleter 装饰的方法的方法名都要相同,这个方法名就相当于托管属性的名称。
封装
简单的理解的话,封装就是在设计类的时候,将一些属性和方法隐藏在类的内部,这样在使用类的时候,将无法使用对象名来直接调用这些属性或方法,只能使用为隐藏的类方法间接操作这些隐藏的属性和方法。
和java不同的是,python中没有public和private修饰符,所以python中采用了如下的方法:
1.默认情况下,python中类的变量和方法的名称前都没有下划线,这种变量和方法都是公有的。
2.如果变量和方法的名称以__开头,则该变量或方法为私有。
除了上面两种情况外,还可以定义_开头的变量或方法,这种命名方式虽然可以通过对象名正常访问,但是一般我们约定将其视为私有的类和方法。
class People:
def __init__(self,n):
self.__name = n
@property
def name(self):
return self.__name
@name.setter
def name(self,n):
self.__name = n
@name.deleter
def name(self):
self.__name = None
def __run(self):
print(self.name,"is running")
def run(self):
self.__run()
p = People("Andy")
p.run()
继承
python中的继承,只需要在定义子类时,将父类放在子类后面的括号里即可。
class Man(People):
def pee(self):
print("Gundam,standing on the ground.")
m = Man("Jack")
print(m.name)
m.pee()
可以看到Man类中既有People类中的name属性,又有自己的pee()方法。
python中的类可以继承多个父类。这种情况下就会面临多个类中有同名的属性或方法的情况,python中的解决方案是,根据子类继承父类时这些父类的前后顺序,排在前面的父类将会覆盖后面所有父类里同名的属性和方法。
父类方法重写
python中虽然没有方法重载,但是可以对父类的方法进行重写。
class Man(People):
def run(self):
self.__run()
def __run(self):
print(self.name,"跑不动了")
def pee(self):
print("Gundam,standing on the ground.")
m = Man("Jack")
m.run()
super()方法
前面我们提到,python中的子类可以继承多个父类,那么当继承多个父类的时候,构造方法也会被第一个父类给覆盖,这就会导致有一些变量在声明对象时无法赋值。为了解决这个问题,需要重写子类的构造方法,这种时候,可以用到super()方法。
super()函数可以用来调用父类的构造方法,但是当子类继承了多个父类时,只能调用第一个父类的构造方法。
class Transport:
def __init__(self,speed):
self.__speed = speed
def showSpeed(self):
print("速度:",self.__speed)
class Car:
def __init__(self,way):
self.__way = way
def showWay(self):
print("方式:",self.__way)
class Benz(Transport,Car):
def __init__(self, speed,way):
#相当于Transport.__init__(self,speed)
super().__init__(speed)
#调用其他父类的构造方法,需要手动绑定self
Car.__init__(self,way)
b = Benz(80,"跑")
b.showSpeed()
b.showWay()
__ slots__
上文中,我们说到了动态的为类和对象添加属性。在python中,也可以动态的为类和对象添加方法,类可以动态的添加实例方法、静态方法和类方法,但是对于对象,只能动态添加实例方法。
class TestClass():
pass
def method1(self):
print("实例方法")
@classmethod
def method2(cls):
print("类方法")
@staticmethod
def method3():
print("静态方法")
#类可以动态添加三种方法,并影响所有实例对象
TestClass.method1 = method1
TestClass.method2 = method2
TestClass.method3 = method3
t = TestClass()
t.method1()
t.method2()
t.method3()
#实例对象只能动态添加实例方法,不会影响其他实例对象
t2 = TestClass()
t2.method1 = method1
#需要手动传入self
t2.method1(t2)
但是如果频繁地动态添加属性或方法,可能会存在一定的隐患,因此,python提供了__slots__属性。这个属性实际上是一个元组,只有其中指定的元素,才可以作为动态添加的属性和方法的名称。
需要注意的是,__slots__属性限制的是类的实例对象,因此直接给类动态添加属性和方法是不受这个属性限制的。还有就是由该类派生出的子类,也是不受限制的,如果子类也要进行限制,则需要给子类也加上__slots__属性,此时子类的__slots__值为子类和父类的值的和。
class TestClass:
__slots__ = ("m1","m2")
def m1(self):
print("m1")
t = TestClass()
t.m1 = m1
t.m1(t)
type()
type()函数是python的内置函数,可以用来查看变量的具体类型。除此之外,type()函数还有一种更高级的用法,即动态创建一个类。type()函数的语法有两种:
1.type(obj),用来查看某个变量的具体类型。
2.type(name, bases, dict),用来创建一个类,name表示类的名称;bases是一个元组,表示该类的父类;dict是一个字典,表示类中的属性和方法。这里我们重点介绍一下这种用法。
def m1(self):
print("m1:",self.__name)
TestClass = type("TestClass",(object,),dict(__name = "m1",m1 = m1))
t = TestClass()
t.m1()
可以看到,使用type()函数创建的类,和直接使用class定义的类并没有区别,实际上,在使用class定义类的时候,python解释器的底层也是使用type()函数来创建这个类的。
MetaClass元类
MetaClass元类的本质也是一个类,但是和普通类的用法不同,它可以对类内部的定义(包括属性和方法)进行动态的修改。使用元类的主要目的就是为了实现在创建类时,能够动态的改变类中的属性和方法。
举个例子,如果在开发过程中,我们需要为多个类添加一个name属性和一个say()方法,逐个添加是比较麻烦的,这种时候可以使用MetaClass元类。
如果想把一个类设计成元类,需要满足两个条件:
1.必须显式继承type类。
2.类中需要实现__new__()方法,该方法一定要返回一个类的实例对象,因为在使用元类创建类时,会自动执行__new__()方法,用来修改新建的类。
class FirstMetaClass(type):
#cls代表动态修改的类
#name代表动态修改的类名
#bases代表被动态修改的类的所有父类
#attrs代表被动态修改的类的属性和方法
def __new__(cls,name,bases,attrs):
attrs['name'] = "张三"
attrs['say'] = lambda self:print("我叫:",self.name)
return super().__new__(cls,name,bases,attrs)
可以看到,在我们创建的元类中,动态的添加了name属性和say()方法,通过该元类创建的类,会额外添加name属性和say()方法。
class TestClass(object,metaclass = FirstMetaClass):
pass
t = TestClass()
print(t.name)
t.say()
按照我个人理解,元类是type类的子类,实际创建TestClass的时候,指定了FirstMetaClass类代替了原本的type类。
多态
面向对象的三个特性我们已经讲了封装和继承,现在来说一说多态。
class C1:
def m1(self):
print("C1")
class C2(C1):
def m1(self):
print("C2")
class C3(C1):
def m1(self):
print("C3")
c = C1()
c.m1()
c = C2()
c.m1()
c = C3()
c.m1()
从上面的例子可以看到,同一个变量a在执行m1()方法时,a表示不同的实例对象时,调用的并不是同一个m1()方法,这就是多态。多态必须满足两个前提条件:1.多态一定是发生在父类和子类之间;2.子类重写了父类的方法。
python在多态的基础上,衍生出了一种灵活的编程机制,通常被称为鸭子模型。
class whoC:
def m1(self,who):
who.m1()
w = whoC()
w.m1(C1())
w.m1(C2())
w.m1(C3())
我们通过创建了一个whoC类,给其传入一个who参数,其内部利用传入的who调用m1()方法,当我们调用whoC的m1()方法时,我们传入的who是什么类,就会去调用哪个类的m1()方法。
枚举类
Python中定义一个枚举类,只需要让类继承enum 模块中的 Enum 类就可以了。
from enum import Enum
class Mouth(Enum):
January = 1
February = 2
March = 3
和普通的类不同,枚举类不能实例化,可以通过如下方法来调用枚举类中的变量。
#调用枚举成员
print(Mouth.January)
print(Mouth['February'])
print(Mouth(1))
#调用枚举成员的值或name
print(Mouth.February.value)
print(Mouth.February.name)
#遍历枚举成员
for m in Mouth:
print(m)
枚举类的成员不能用来比大小,但是可以用==或者is来判断是否相等。
print(Mouth.February == Mouth.January)
print(Mouth.February.value is Mouth.January.value)
除此之外,枚举类还提供了__members__属性,该属性是一个包含了枚举类中所有成员的字典,通过遍历该属性,也可以访问枚举类中的成员。
for name,member in Mouth.__members__.items():
print(name,"->",member)
枚举类中的成员name不能相同,但是value可以相同。如果想要避免这种情况,可以使用@unique装饰器,这样当枚举类中出现值相同的成员时,会抛出ValueError异常。
from enum import Enum,unique
@unique
class Mouth(Enum):
January = 1
February = 2
March = 3
除了可以通过继承Enum的方式来创建枚举类,还可以使用Enum()函数创建枚举类。
Mouth = Enum("Mouth",('January','February','March'))
这样创建的枚举类value会从1开始递增,所以这个枚举类和上面的创建方法是一样的。