class A
经典类写法,查找方式深度优先class A(object)
新式类写法,查找方式广度优先
上面是python2的语法,python3里可能已经没有经典类了。不管有没有,都用形式类来写就对了。
上面都是上节讲的内容,再讲一下构造函数的问题。Father.__init__(self,name,age)
这个是经典类的构造函数写法,把父类的名字写在前面,但是问题是若干是多继承呢。这一句显然只继承了一个父类。其他父类的属性就没有继承到了。那么就是有几个父类要写几个构造函数了。super(Son,self).__init__(name,age)
# super就一次能把所有父类的属性继承到了
多继承的情况可能用不到,或者也可以用其他方法来替代,比如组合。暂时就掌握这么多了super(Son, self).__init__(*args, **kwargs)
# 这样写,不指定继承哪些参数,而是全部继承过来,推荐。
静态方法和类方法上一节已经讲过了。
静态方法,通过@staticmethod装饰,可以不用传入参数,无法动态的调用任何实例或者类的属性和方法
类方法,通过@classmethod装饰,必须传入一个类作为参数,可以动态的调用传入的类的属性和方法
class Test(object):
name = "This is a test"
@staticmethod
def test_static(a,b): # 没有像self那样的参数了
print(a,b)
@classmethod
def test_class(cls,a,b): # 这里的cls和self一样,只是类方法里这里指的是类名
print(cls.name,a,b)
Test.test_static("a","b") # 这里调用方法的变量(Test)没有任何意义
Test.test_class("a","b") # 这里会根据调用方法的变量(Test)所属的类,把这个类传入
属性方法,通过@property装饰,把一个方法变成一个属性。调用的时候是一个属性,定义的时候是用方法了定义的。
class Dog(object):
def __init__(self,name):
self.name = name
@property
def eat(self):
print("%s is eating %s"%(self.name,"meat"))
d1 = Dog("Eric")
#d1.eat() # 这样用回报错,现在eat已经是一个属性了,属性不能加()运行
d1.eat # 这是一个属性,直接这样就运行了
看着好像有点用,但是并没有什么实际用处。如果这个属性值是需要一系列的运算后才获得的,那么我可以把为了获取到这个属性值的操作都写在这个属性方法里。但是在类的外部只要把它当做一个属性来调用就好了。
比如一个人,我只需要一个姓,一个名,当需要用到全名的时候,我只有通过姓和名拼接后就可以获得全名
class Person(object):
def __init__(self,first_name,last_name):
self.first_name = first_name
self.last_name = last_name
#self.full_name = "%s %s"%(first_name,last_name) # 貌似这样也能实现,只怪运算不够复杂
@property
def full_name(self):
return "%s %s"%(self.first_name,self.last_name)
p1 = Person("Jack","Johnson")
print(p1.first_name)
print(p1.last_name)
print(p1.full_name)
上面的情况,调用full_name就很整齐,和其他两个一样都是通过属性调用的。当然其实在构造函数里写个self.full_name也是一样能实现的,只怪这个算法太简单。如果需要几行代码的话,只能另外写一个函数来计算并返回,然后self.full_name赋值那个函数的返回值,这一通操作之后也是一样的效果。权且先当到一个实现方法吧
下面是老师的例子:
status = input("请输入航班状态:")
class Flight(object):
def __init__(self,name):
self.name = name
def check_status(self):
"假设这里通过一系列的代码获取到了航班的状态,虽然其实是开始前输入的"
return status
@property
def flight_status(self):
return self.check_status()
f1 = Flight("MU9319")
f1_status = f1.flight_status # 这里看上去就是直接调用了类里的一个属性
print(f1.name,"航班状态:",f1_status)
属性方法还没完,既然是方法,那么就会有需要传参数,可是调用的时候又是属性,那么就没有()就没地方写参数了。不过既然是个属性,那么我们可以给它赋值,通过赋值来传参数。
虽然老师是这么讲的,但是或许该这么理解。这个方法现在就是一个属性,获取属性时用的是上面的方法,然后我们还可以给属性赋值(设置属性),删除属性(del 这个属性)。下面的例子就是分别写三个方法对应获取属性时使用的方法、设置属性时使用的方法、删除属性时使用的方法。普通的属性的设置和删除python有自己的方法,这里我们就通过属性方法自定义了自己的属性在上面3个操作的时候具体执行什么
status = "延误"
class Flight(object):
def __init__(self,name):
self.name = name
def check_status(self):
"假设这里通过一系列的代码获取到了航班的状态,虽然其实是开始前输入的"
return status
@property
def flight_status(self): # 这里是获取属性时使用的方法
print(self.name,"航班状态",self.check_status())
@flight_status.setter
def flight_status(self,status): # 这里是设置属性时使用的方法
print(self.name,"航班状态",status)
@flight_status.deleter
def flight_status(self): # 这里是删除属性时使用的方法
print(self.name,"航班已经起飞")
f1 = Flight("MU9319")
f1.flight_status # 触发property装饰的函数,现在是获取属性
f1.flight_status = "到达" # 触发setter装饰的函数,现在是设置属性
f1.flight_status = "登机"
del f1.flight_status # 触发deleter装饰的函数,现在是删除属性
del f1.flight_status
f1.flight_status = "返航"
上面的3个装饰器分别是获取属性是使用的方法,设置属性时使用的方法、删除属性时使用的方法。这里删除属性时一般如果就是要删除这个属性,那么就在方法里写一个del。不过这里我们想让他做点别的而不是删除,那么也是可以的。不过既然占用了删除属性的方法,那么就没办法主动删除这个属性喽。(好像一般也不会去主动删除掉哪个属性)
下面的例子用这3个装饰器来重构了name这个属性
class Person(object):
def __init__(self,name):
self.name = name
@property # 获取属性的方法
def name(self):
return self._name
@name.setter # 设置属性的方法
def name(self,name):
self._name = name
@name.deleter # 删除属性的方法
def name(self):
del self._name
p1 = Person("Tom")
print(p1.name)
p1.name = "Jerry"
print(p1.name)
del p1.name
#print(p1.name) # 属性已经被删除了,所以打印会报错,_name属性不存在
其实上面的代码并没有意义,和下面的一样,
class Person(object):
def __init__(self,name):
self.name = name
p1 = Person("Tom")
print(p1.name)
p1.name = "Jerry"
print(p1.name)
del p1.name
#print(p1.name)
但是现在我们可以在我们自己重构的属性方法里加入各种代码,来实现我们其他的需求。举个例子,比如检查属性类型:
class Person(object):
def __init__(self,name):
self.name = name
@property
def name(self):
"转成首字母大写的格式"
return self._name.capitalize()
@name.setter
def name(self,name):
"必须是字符串,否则抛出错误"
if not isinstance(name,str):
raise TypeError('name must is string type')
self._name = name
@name.deleter
def name(self):
"不允许删除属性,否则抛出错误"
raise AttributeError('Can not delete the name')
#p1 = Person(22) # 直接给name传入×××的话,会触发@name.setter的报错
name = input("输入名字(都会传成首字母大写):")
p2 = Person(name)
print(p2.name)
#del p2.name # 尝试删除属性的话,会抛出@name.deleter的报错
下面的内置函数是之前讲内置函数时跳过的,因为是一个类里使用的内置函数。但是其实这里还是要忽略。所以了解一下,然后忘记它。
内置方法property()
有一个同名的内置方法property(fget=None, fset=None, fdel=None, doc=None)。前3个参数就和上面装饰器的是一样的,分别是获取属性的方法、设置属性的方法、删除属性的方法。上面的函数可以改成这样:
class Person(object):
def __init__(self,name):
self.name = name
def get_name(self):
"转成首字母大写"
return self._name.capitalize()
def set_name(self,name):
"必须是字符串,否则抛出错误"
if not isinstance(name,str):
raise TypeError('name must is string type')
self._name = name
def del_name(self):
"不允许删除属性,否则抛出错误"
raise AttributeError('Can not delete the name')
name = property(get_name,set_name,del_name)
#p1 = Person(22) # 直接给name传入×××的话,会触发@name.setter的报错
name = input("输入名字(都会传成首字母大写):")
p2 = Person(name)
print(p2.name)
#del p2.name # 尝试删除属性的话,会抛出@name.deleter的报错
效果一样,但还是用装饰器来写,不过装饰器是只有在新式类中才有的。property()可以忘记它,用这个low了,具体啥原因不清楚,大概是要多起3个函数名?或者就结构不清晰,分成了独立的4部分,不像装饰器是绑在函数前面的。
__doc__
表示描述信息
这个并不是只属于类的方法,对于函数和模块同样有效。我们写函数或类的时候,应该在第一行以字符串的格式做说明。这里用字符串而不是注释的意义就在于,通过__doc__
是可以获取到的
def test():
"TEST"
pass
class Person(object):
'''描述人类的信息
测试__doc__
'''
def func(self):
"类的实例方法"
pass
print(Person.__doc__) # 打印类的描述
print(Person.func.__doc__) # 打印实例方法的描述
print(test.__doc__) # 打印普通函数的描述
import time # 模块的描述同样可以打印出来
print(time.__doc__)
print(time.time.__doc__)
__module__
对象在哪个模块
__file__
返回模块所在的目录,字符串
__class__
对象的是类什么
__init__
构造方法
__del__
析构方法
__call__
对象或类后面加( )括号,触发执行
class Test(object):
def __call__(self):
print("running call")
Test()() # 通过类触发执行,其实Test()是先实例化了,然后再后面一个( )触发执行
t1 = Test()
t1() # 通过对象触发执行,这个和上面的是一样的,只不过赋值给了t1,还能再调用这个对象
__dict__
查看类或对象中的所有成员
返回一个字典,key是属性名,value是属性值
class People(object):
display = "人类" # 注意公有属性的归属
def __init__(self,name,age,sex):
self.name = name
self.age = age
self.__sex = sex # 私有属性也没问题
p1 = People("Jerry",34,"M")
print(p1.__dict__) # 打印对象的所有属性,但是这里不包括公有属性,公有属性在类里面
print(People.__dict__) # 打印类的所有属性,这里会看到一些特殊属性
公有属性,打印对象的时候是获取不到的,因为记录在类的属性里
打印类的所有属性会看到一些特殊属性,但是不是全部,比如__call__
是没有的,但是如果定义了这个方法,就会显示出来
所以真的要用这个方法打印出所有属性,需要把类和对象的属性都找出来,去掉其中的特殊属性。类和对象中都有的属性,只要对象的。
__str__
打印对象时,打印__str__
的返回值
如果没有__str__
方法,则默认打印内存地址
__getitem__
获取key的方法
__setitem__
设置key的方法
__delitem__
删除key的方法
class Foo(object):
def __getitem__(self, key):
print('__getitem__',key)
def __setitem__(self, key, value):
print('__setitem__',key,value)
def __delitem__(self, key):
print('__delitem__',key)
obj = Foo()
obj['k1'] # 触发执行 __getitem__
obj['k2'] = 'alex' # 自动触发执行 __setitem__
del obj['k1'] # 自动触发执行__delitem__
这里的3个方法和属性方法比较类似了,通过这3个方法可以把对象当做是字典来操作了。或者说自定义一个字典。
或许还有自定义列表的方法,上课说python3里没了,就没讲。
元类是用来创建类的类。我们创建类是通过元类来创建的。通过了解元类创建类的过程,可以对类有更深的理解。当然不理解也不影响我们使用类和用面向对象的方法编程。
先学习2个基础一点的知识,然后在看看元类是什么,元类是如何创建类的。
__new__
创建实例的方法
创建实例我们之前都不知道new的存在,但是实例是通过new方法来创建的。先来看个例子,我们重构new方法
class Foo(object):
def __init__(self,name):
self.name = name
print("Foo.IIinit__") # 确认构造方法是否被执行了
def __new__(cls,*args,**kwargs):
print("Foo.__new__") # 确认new方法是否被执行了
obj = Foo() # 这里估计漏了参数,看构造函数,这个类实例化的时候是需要一个name参数的
运行结果,只有new方法被执行了,构造方法并没有被执行。当然没有执行构造方法也就不需要name参数,所以这里Foo()
并没有报错。按之前理解的,构造方法是在实例化的时候自动被执行的,这里我们写了new方法后就不自动执行了。因为这里我们重构了new方法,原本是通过new方法来调用执行构造函数的。另外,构造方法在实例化的时候自动执行并没有错,其实这里我们还没有完成实例化,因为new没有调用构造方法,没有做实例化的操作。所以new函数里应该有这么一句,如下
class Foo(object):
def __init__(self,name):
self.name = name
print("Foo.__init__") # 确认构造方法是否被执行了
def __new__(cls,*args,**kwargs):
print("Foo.__new__") # 确认new方法是否被执行了
# 上面的内容我们可以实现定制自己的类
# 下面2句return的效果是一样的,就是去继承一个__new__方法然后调用执行
return object.__new__(cls) # 经典类写法,指定继承object
#return super(Foo,cls).__new__(cls) # 新式类写法,没有指定继承谁
obj = Foo("Bob") # 现在会调用构造函数了,所以参数不写要报错的
print(obj.name) # 再打印个属性看看
上面的结果看,先执行的new方法,再执行构造方法。实例是通过new来创建的。如果你想定制你的类,在实例化之前定制,需要使用new方法。说到继承,这里的写法和构造方法是一样的,可以先理解经典类的写法,比较直观。新式类用super的写法参考之前的构造函数改一下也就出来了。
new方法必须要有返回值,返回实例化出来的实例。使用经典类写法指定的话,可以return父类的new方法出来的实例,也可以直接将object的new出来的实例返回。但是这个返回值和构造并看不出有什么关系,为什么就触发了构造方法呢?后面会继续讲。
现在我们已经知道了,类是通过自己的new方法来创建实例的。
用type创建类
先看一个简单的类
class Foo(object):
def __init__(self,name):
self.name = name
def func(self):
print("Hello %s"%self.name)
f1 = Foo("Jack")
f1.func()
print(type(f1))
print(type(Foo))
我们打印了对象f1的类型,f1对象是由Foo创建。在python中一切皆对象,那么Foo这个对象我们从输出结果看,应该是由type创建的。所以我们可以用tpye来创建Foo这个类
def __init__(self,name):
self.name = name
def func(self):
print("Hello %s"%self.name)
Foo = type("Foo",(object,),{'__init__': __init__,
'func': func})
f1 = Foo("Jack")
f1.func()
print(type(f1))
print(type(Foo))
上面就是用type创建类的方法,效果一模一样。这里type有三个参数
type(object_or_name, bases, dict)
object :第一个参数可以是另外一个对象,那么新创建的对象就是这object这个对象同一类型
name :第一个参数也可以是个名字,那么name就是这个新类型的名字
bases :第二个参数是当前类的基类,可以为空,那么就是一个经典类。我们这里是按新式类来基础object。这个参数值接收元组,所以这里要这么写(object,)
,这样就是一个只有一个元素的元组,没有逗号的话,会被作为一个type类型。
print(type((1))) # (1) 是 <class 'int'>
print(type((1,))) # (1,) 是 <class 'tuple'>
dict :第三个参数是一个字典,就是这个类的所有成员。公有属性以及方法
这里type也是一个类,叫元类
现在我们已经知道了,类是通过type类来创建的。
__metaclass__
由元类来创建一个类
类中有一个 __metaclass__
属性,表示该类是由谁来实例化创建的。之前我们默认创建的基类,都是由type元类来实例化创建的。__metaclass__
属性是python2中的讲法,在python3中已经变成了metaclass,已经不是一个属性了,但是作用没变。
上面的铺垫,主要是这2点:
- 实例是通过类的new方法来创建的
- 而类是通过type元类来创建的
元类创建类,然后类中有new方法来创建这个类的实例
现在我们看看type类内部是怎么来创建类的。我们可以为 __metaclass__
设置一个type类的派生类,加入print语句,从而查看类创建的过程。
class MyType(type):
def __init__(self,what,bases=None,dict=None):
print("MyType.__init__")
super(MyType,self).__init__(what,bases,dict)
def __call__(self,*args,**kwargs):
print("MyType.__call__")
obj = self.__new__(self,*args,**kwargs) # 注释掉这句,Foo.__new__不会执行
# 这里的self传入的是Foo,所以就是执行Foo的new方法赋值给了obj
self.__init__(obj,*args,**kwargs) # 注释掉这句,Foo.__init__不会执行
# 这里的self自然还是Foo,obj就是上面的new方法的返回值
# 这句是构造方法,调用的是Foo的构造方法,创建的就是Foo的对象
return obj # 注释掉这句,最后打印实例的时候,会打印None,因为这里没有return值了
# 上面已经将obj创建成为了对象,就在这里最后将这个对象作为整个过程的返回值返回
class Foo(object,metaclass=MyType):
"metaclass告诉python,这个类是由MyType来实例化创建的,所以到这里就会执行MyType的构造方法"
#__metaclass__ = MyType # 这是python2里的写法,当然上面括号里的内容就要去掉
def __init__(self,name):
self.name = name
print("Foo.__init__")
def __new__(cls,*args,**kwargs):
print("Foo.__new__")
return super(Foo,cls).__new__(cls)
obj = Foo("Bob") # 注释掉这句和下面的,就没有一个实例化的过程,依然会执行MyType的构造函数
print(obj) # 这里直接打印对象看看
执行后打印的结果:
MyType.__init__
MyType.__call__
Foo.__new__
Foo.__init__
<__main__.Foo object at 0x00000169BC078898>
执行了 obj = Foo("Bob")
后,从打印的结果可以看出上面的执行顺序。
先把 obj = Foo("Bob")
和后面打印对象的2句注释掉,我们发现虽然没有调用执行任何语句,只是定义了2个类,但是MyType.__init__
已经被执行了。因为Foo是元类MyType的一个对象,创建对象是通过类的构造方法,所以要创建Foo这个对象(即Foo类),元类的构造方法就被触发执行了。而这个Foo是MyType的类的一个对象的关系,就是通过Foo里的metaclass的值来确定的。
第一个被执行的是MyType.__init__
,元类执行它的构造函数,创建了元类的一个实例,这里就是Foo类。然后再是通过 obj = Foo("Bob")
这个实例化的语句来触发了后面的一系列的结果。
第二个被执行的是Mytype.__call__
,call方法打印之后,一次会执行后面的3句语句。把这3句全部注释掉之后,我们会发现不会再有任何输出。从上到下依次再去掉注释执行。去掉第一个后发现Foo.__new__
被执行了。
第三个被执行的是Foo.__new__
,所以类中的new方法是由metaclass指向的那个类(在这里是MyType)中的call方法来触发执行的。上面我们已经已经知道new方法需要一个返回值,而这个返回值就是返回给上面的call方法,用来继续执行下面的语句。现在可以去掉第二个注释,发现Foo.__init__
被执行了。
第四个被执行的是Foo.__init__
。这个当然就iFoo的构造方法了。构造方法是在new方法返回给上面的call方法之后,由call方法使用new的返回值继续调用执行的。
最后call方法还有一行return obj
,完成了将对象返回作为返回值返回。所以注释掉之后,打印对象是空,也就是上面一系列的过程执行过之后,生成的是这个obj作为Foo("Bob")这个实例话过程的返回值。
通过字符串映射或修改程序运行时的状态、属性、方法, 有以下4个方法
- hasattr(obj,name) :判断对象是否包含对应的属性
- getattr(object, name[, default]) :返回一个对象属性值,若没有对应属性返回default,若没设default将触发AttributeError
- setattr(obj,name,value) :设置对象属性值。和=赋值的等价
- delattr(obj,name) :删除对象的属性,不能删除方法。和del的效果等价
上面说的属性,对于方法来说都是一样对待的,还是因为一切皆对象,属性的理解比较直观,下面都用方法来举例子:class Dog(object): def __init__(self,name): self.name = name def eat(self,food): print("%s is eating %s"%(self.name,food)) d1 = Dog("Eric") choise = input(">>:").strip() # 输入eat或者其他 print(hasattr(d1,choise)) # 查看你输入的字符串是否在d1里有这个属性 if hasattr(d1,choise): getattr(d1,choise)("meat") # 如果有这个属性,则调用执行这个属性 else: print("没有 %s 这个方法"%choise)
setattr(obj,name,value)
这句就相当于是obj.name = value
,两句是等价的class Dog(object): def __init__(self,name): self.name = name def func(self): print("使用setattr来替代这个方法") def bulk(self): print("%s Wang~Wang~Wnag~~~ "%self.name) def eat(self,food): print("%s is eating %s'"%(self.name,food)) d1 = Dog("Eric") d1.func() # 这是原来的方法 setattr(d1,'func',d1.bulk) # 现在我们用bulk来替换 d1.func() # 现在执行的是bulk setattr(d1,'func',d1.eat) # 我们再用eat来替换 d1.func("meat") # 现在执行的是eat,注意eat是有参数的
delattr(obj,name)
这句就相当于是del obj.name
,两句是等价的class People(object): language = "English" class Chinese(People): def __init__(self,language): self.language = language c1 = Chinese("简体中文") print(c1.language) # 打印实例的属性,这是是成员属性 delattr(c1,"language") # 删除,删除了成员属性 print(c1.language) # 没有成员属性,现在打印的是继承自父类的公有属性
就是通过模块名的字符串形式来导入这个模块。语法比较简单,主要是应用场景可能一般用不到,希望有需要的时候还能想到
import importlib # 先导入这个模块
module = importlib.import_module('time') #官方建议的用法,然后要赋值
print(module) # 打印模块看看
print(module.asctime()) # 调用time模块打印时间
上面只能导入模块,比如 time.asctime
就不是模块了,导入会报错。另外还有一个是编译器内部使用的方法,下面贴出来。不过如果自己用,还是用观法建议的吧。
module = __import__('time')
print(module)
print(module.asctime())
在编程过程中为了增加友好性,在程序出现bug时一般不会将错误信息显示给用户,而是现实一个提示的页面。
简单的结构
print(a) # 这里没有给a变量赋值,所以a变量是不存在的。运行后抛出错误如下
'''抛出的异常如下:
Traceback (most recent call last):
File "test1.py", line 3, in <module>
print(a)
NameError: name 'a' is not defined
'''
我们可以把可能出现异常的语句放到下面的try里:
try:
print(a)
except NameError as e: # as前面是异常种类,后面是错误的信息,对应上面报错最后一行冒号前后的内容
print("变量名不存在:%s"%e)
print("===结束===")
上面可以写多个except来处理不同的异常类型。如果多个异常类型可以使用相同的出场方法,那么看下面的例子
多个异常
再加一个错误,让except同时处理多个异常类型
try:
('a')[1] # 这个元祖只有第0项,我想在要取第1项,会报错
print(a) # 上面已经捕获到异常了,try中后面的代码就不会再执行了,而是跳去执行except了
# 然而,这里except也没这个错误类型,仍然会抛出错误
except NameError as e:
print("变量名不存在:%s"%e)
print("===结束===")
'''抛出的异常如下:
Traceback (most recent call last):
File "test1.py", line 3, in <module>
('a')[1] # 这个元祖只有第0项,我想在要取第1项
IndexError: string index out of range
'''
虽然放到了try里,但是新的异常种类并没有写到except里,所以依然会抛出错误,下面再把这个异常种类写进去:
try:
('a')[1]
print(a)
except NameError as e:
print("变量名不存在:%s"%e)
except IndexError as e:
print("索引错误:%s"%e)
print("===结束===")
try中的代码块一旦执行到错误,就不会再执行后面的代码了。捕获到异常后,直接就去找except。如果错误类型不在except里,仍然会抛出错误。如果错误类型符合,就执行这个except代码块内的代码,然后跳出整个try代码块继续往后执行。
还可以这样,把几种异常种类写一起
try:
('a')[1]
print(a)
# except后面只接受1个参数,多个错误类型要写成元祖
except (NameError,IndexError) as e:
print("变量名或索引错误:%s"%e)
print("===结束===")
万能异常捕获
我们还可以使用Exception这个错误类型(也可以缺省错误类型),捕获所有的错误:
try:
('a')[1]
print(a)
# 现在try中无论是什么错误,都会被Exception捕获了
except Exception as e: # 这里也可以只写except后面不跟错误类型和错误信息,一样是捕获所有异常,但是无法获取到错误信息e
print("捕获到异常:%s"%e)
print("===结束===")
虽然什么错误都能捕获,但是不建议这么用。建议是,对于特殊处理或提醒的异常需要先定义,最后定义Exception来确保程序正常运行。
而且其实也不是什么错误都能捕获的。因为try本身也是代码,如果连编译器都不能识别的话,就无法执行try来捕获了,比如
try:
print('a') # 这里没缩进,会有缩进错误
# 然后Exception也无法捕获了,因为try本身都执行不下去了
except Exception as e:
print("捕获到异常:%s"%e)
print("===结束===")
else代码块
在异常处理最后可以加上else代码块,只有try中的内容无异常顺利执行完之后,才会运行esle代码块中的内容
try:
print('a') # 正常不会报错
except:
print('发现未知错误')
else:
print('执行完成,未发生异常')
print("===try1,结束===")
try:
print(a) # 这个会报错
except:
print('有异常,else中的内容将不会执行')
else:
print('不会执行这里')
print(("===try2,结束==="))
finally代码块
无论是否有异常,最后都会执行finally代码块中的内容。如果未能捕获到异常的类型,就会抛出异常然后终止程序运行。所以在抛出异常前会先执行finally里的代码块。这是代码放在finally中和放到整个异常代码块之后的区别,就是报错前仍然会先把finally里的执行完再报错然后终止
try:
print(a)
except NameError as e:
print("NameError:%s"%e)
finally:
print("先捕获异常,执行except"
'\n'
"然后再执行finally")
print("===try1,结束===")
try:
print(a)
finally:
print("在抛出异常前,会先执行finally"
"\n"
"然后就要抛出异常了...")
print("前面已经报错了,执行不到我这里")
小结
基本上所有的情况都有了,那么异常最复杂的情况大概就是下面这样,所有的都用上了
try:
# 这里写上你的代码,正确的或者有错误的
pass
except NameError as e:
# 处理单个异常类型
print("NameError: %s"%e)
except (IndexError,KeyError) as e:
# 处理多个异常类型
print("列表或字典错误:%s"%e)
except Exception as e:
# 处理其它异常
# 在处理完已知的异常后,还是可以这么写,处理一些未预见的情况
print("未知错误:%s"%e)
else:
# try里的代码正常执行完后,会执行这里的代码
print("未发现异常")
finally:
# 无论异常与否,最后都执行这里的代码
print("异常处理完成")
主动触发异常
使用raise可以主动触发一个异常,
raise [Exception [, args [, traceback]]] : Exception是异常类型,可以是python有的其他错误类型。可以缺省但是不能自创,缺省的话错误类型就是None,后面的一个参数是异常的信息,也就是上面例子中我们捕获的e。最后还有一个参数可省略,是跟踪错误对象的,上课没讲也很少用的到。
n = input("输入一个数字:") # 如果这里输入的是非数字,回车后就会报错
if not n.isdigit():
raise Exception ("发现非数字")
如果要捕获这个异常也和上面一样
try:
raise Exception ("发现非数字") # 直接把异常抛出
except Exception as e:
print ("触发自定义异常:",e)
print("===结束===")
自定义异常
首先异常也是类,上面的异常类型,其实都是类名。except匹配的异常类型就是匹配类名,所有的异常类型都是继承自Exception,所以可以使用Exception来捕获所有的异常。另外其实Exception是继承自BaseException,但是我们平时不需要知道BaseException的存在。
try:
raise Exception ("发现非数字") # 直接把异常抛出
except BaseException as e: # 通过BaseException同样可以捕获到异常
print ("触发自定义异常:",e)
print("===结束===")
自定义异常我们只要熟练运用类的方法就可以了。一般就是自定义继承Exception的新的异常类型,或者自定义继承自其它异常类型的子类异常类型。
class MyException(Exception):
"自定义新的异常类型"
def __init__(self,msg):
self.msg = msg
def __str__(self): # # 这段str方法可以不写,可以从父类继承到
"打印类的时候,会打印这里的返回值"
return self.msg
# 主动抛出异常并捕获
try:
raise MyException('我的异常')
except MyException as e:
print("这是我的异常:",e)
print("结束")
自定义的异常,应该只是逻辑上有错误,影响你程序的正常运行但是不影响python代码的执行。所以python是不会报错的,我们要触发自己定义的异常,都是通过逻辑判断后主动将自定义的异常通过raise抛出。
自定义异常类中的str方法,是不需要的,因为可以从父类继承到。这里写出来是为了说明,我们打印异常信息是通过str方法定义的。就是就是把你捕获到的异常对象通过as赋值,然后打印这个对象(打印这个对象就是调用这个对象的str方法)。当然也可以像例子中这样不继承,自己重构,自定义异常信息的处理。
断言
判断一个条件,为真则继续,否则抛出异常。异常类型:AssertionError
assert type('a') is str # 没有问题,会继续往下执行
assert type('a') is int # 执行后将抛出异常
traceback模块(补充)
python中用于处理异常栈的模块是traceback模块,它提供了print_exception、format_exception等输出异常栈等常用的工具函数。traceback对象中包含出错的行数、位置等数据,所以比e里的数据更详细,很有用。
except Exception as e:
print(str(e)) # 简单的信息
import traceback
error_msg = traceback.format_exc() # 通常就是把这个error的字符串记录下来或者返回
print(error_msg)
如果要打印的话,可以用这个方法 traceback.print_exc()
。不过通常是要保存下来,之后看,所以一般是例子里这么用,拿到详细的异常信息的字符串,返回或者记录下来。
socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
建立一个socket必须至少有2端, 一个服务端,一个客户端, 服务端被动等待并接收请求,客户端主动发起请求, 连接建立之后,双方可以互发数据。
简单的socket例子
先要有一个服务器端server:
import socket
# 设定地址簇和连接类型,下面都用了默认参数
# 默认是使用IPv4和TCP协议
server = socket.socket()
# 绑定套接字,只接受1个参数,格式取决于地址簇
# IPv4的通信需要IP地址和端口号,写成元祖的形式传入
# 第一部分是本机地址,这里localhost表示本机
# 端口号可以随便定一个1024以后的,这里先写死了
# 最好是定一个默认的,然后通过配置文件可修改
server.bind(('localhost',11111))
# 开启监听
server.listen()
print("监听已经开始")
# 等待连接,接受到连接后会返回(conn,addr)
# conn,新的套接字对象,用于接收和发送数据
# addr,接收到的请求连接的客户端的地址
conn,addr = server.accept()
print("发现连接请求:\n%s\n%s"%(conn,addr)) # 把返回值打印出来看一下
# 接收数据,复制到变量保存
# 参数是指定最多可以接受的字节数
data = conn.recv(1024) # 1024字节就1KB,如果是ASCII字符就是1024个
print("recv:",data) # 打印接收到的数据
# 发送数据,这里注意python3现在只能发送bytes类型了
conn.send(data.upper()) # 把收到的全部转成大写发回去
# 关闭连接
server.close()
运行后,会停留在监听的地方,直到监听到服务请求。
然后再写一个客户端client:
import socket
# 设定连接类型
client = socket.socket()
# 请求连接
client.connect(('localhost',11111))
# 发送数据
client.send(b"Hello World")
# 接收数据
data = client.recv(1024)
# 打印出接收到的数据
print('recv:',data)
# 关闭连接
client.close()
这里再运行一下上面的客户端,同时观察服务器端和客户端的反馈信息。
服务器端:accept到客户端的请求后,按我们写的打印出conn和addr,然后再将接收到的信息打印出来。最后给客户端会一条信息
客户端:打印出接收到的从服务器端发来的全部转成大写的信息
上面例子中的结束是以客户端发送一个空数据触发的
最后全部关闭连接
连续发送数据
上面的例子,只发送了一条数据就断开了。如果要持续交换数据,那么需要把交换数据的部分写到一个循环里,最好还有一个退出循环出的方法。
服务端:
import socket
server = socket.socket()
server.bind(('localhost',11111))
server.listen()
print("监听已经开始")
conn,addr = server.accept()
print("发现连接请求:\n%s\n%s"%(conn,addr))
# 持续接收数据,发回给客户端
while True:
data = conn.recv(1024)
if not data: break # 这句来控制跳出循环,否则在客户端断开后会报错
print("recv:",data.decode("utf-8"))
conn.send("收到:".encode("utf-8") + data) # 前面加点内容回给客户端
server.close()
客户端:
import socket
client = socket.socket()
client.connect(('localhost',11111))
msg = input(">>:")
# 把input的内容持续发送给服务器,如果发送空内容,就不发送直接跳出循环
while msg:
client.send(msg.encode("utf-8")) # 发送数据要转码成bytes类型
data = client.recv(1024) # 接收服务器端的回复
print('recv:',data.decode("utf-8")) # 打印出回复的内容
msg = input(">>:")
else:
input("准备断开连接,现在服务端还没断开\n"
"回车后客户端close,服务端也同时close")
client.close()
发不了空,不同协议不同系统发送和接收空的情况都不一样,有的当做没有任何操作,而有的会造成阻塞。所以不要尝试发送空。
例子中的退出的过程:
客户端,input收到空之后,并没有将这个空发出去。只是在输入空数据后就退出了循环然后close。
服务端,在客户端断开后,通过 if not data: break
这句触发跳出了循环。这里客户端没有发送空,而且也发不出空,但是依然触发了这句。正常recv是读取缓冲区数据并返回,如果缓冲区无数据,则阻塞直到缓冲区中有数据,只有在客户端close后读取缓存区才会返回空,所以这里能触发break。如果没有这句break语句,服务端在客户端close之后会报错,异常类型:“ConnectionAbortedError”。所以也可以通过异常处理来退出。
为多个客户端或多次提供服务
首先,目前我们的服务端一次还是只能连接一个客户端。并且后这段的后面也不会讲到同时处理多个连接的情况。
上面的例子在接收到客户端的连接请求后,可以持续为客户端提供服务。但是当这个客户端断开后,服务端也无法继续提供服务了(即使服务端最后不执行close)。如果希望在一次服务结束后不退出,而是可以继续准备提供下一次服务,那么就是要在客户端断开后,可以回到监听的状态,等待下一个客户端的连接请求。在上面的基础上,客户端不用修改,服务端需要再加上一个循环。
import socket
server = socket.socket()
server.bind(('localhost',11111))
server.listen()
print("监听已经开始")
count = 0
# 加个计数器,服务3次后停止服务
while count<3:
# accept是等待连接请求,所以在没有客户端连接的时候,希望回到这里
conn,addr = server.accept()
print("发现连接请求:\n%s\n%s"%(conn,addr))
# 持续接收数据,发回给客户端
while True:
data = conn.recv(1024)
if not data: break # 这样可以正常退出循环,没有这句客户端断开后会报错
print("recv:",data.decode("utf-8"))
conn.send("收到:".encode("utf-8") + data)
print("断开与 %s 的连接,再次开始监听等待"%str(addr))
count += 1
print("停止服务")
server.close()
客户端不用改,这里可以试一下同时连多个客户端。一个客户端连接成功后,别的客户端再连接也是可以连上的,但是发送不了数据。是能发一次数据,但是这时服务端在为其他客户端服务,暂时不会回复。等你这个客户端之前的客户端都断开后,服务端会马上处理你的数据并给你回复。
客服端发送命令到服务端执行并在客户端打印结果
服务端的话也不需要新的知识。只是需要用之前学的os模块或者subprocess模块,收到数据后作为命令执行然后将结果返回。
import socket
import subprocess
server = socket.socket()
server.bind(('localhost',11111))
server.listen()
print("监听已经开始")
count = 0
# 加个计数器,服务3次后停止服务
while count<3:
conn,addr = server.accept()
print("发现连接请求:\n%s\n%s"%(conn,addr))
while True:
data = conn.recv(1024)
if not data: break
# 现在讲接收到的字符串作为命令执行,将执行结果返回
# subprocess模块在之前的模块学习中已经详细学过了
res = subprocess.Popen(
data.decode("utf-8"),shell=True,stdout=subprocess.PIPE)
res_read = res.stdout.read() # 注意这里的编码格式是系统的编码格式,windows的话默认gbk
conn.send(res_read) # 这里read到的值已经是bytes类型了,所以不需要转码
print("断开与 %s 的连接,再次开始监听等待"%str(addr))
count += 1
print("停止服务")
server.close()
客户端没有太大的变动,不过这里服务端的代码比较简单。只能处理输入命令后能自动获得结果并返回的命令。就先拿个dir或者ls试一下。
import socket
client = socket.socket()
client.connect(('localhost',11111))
comm = "dir" # 可以替换其他命令,这里演示先把命令写死了
# 执行下面这种命令的话,需要等待一段时间才能收到服务端的返回数据,因为服务端执行也需要时间,然后才能获得结果发回来
# comm = "ping 127.0.0.1 -n 10"
while comm:
client.send(comm.encode("utf-8")) # 发送数据要转码成bytes类型
data = client.recv(1024) # 接收服务器端的回复
print('recv:',data.decode('gbk')) # 传过来的是执行命令的返回结果,操作系统的默认编码的byte类型
comm = input(">>:")
else:
print("准备断开连接")
client.close()
上面的代码比较简单,不能执行象telnet
或者nslookup
这类会有交互的命令,也不能是错误的命令。因为服务端执行命令后都不会自动回复返回值发送回客户端。这样会造成客户端和服务端程序阻塞,只能强行关闭了
这里因为和操作系统交互了,所以中间会有系统的编码,最后打印执行结果的时候需要注意一下字符编码
socket.recv的参数
之前例子中,recv的参数都设置了1024。这里1024是字节数限制,一次接收不能超过这个字节数。可以用上面的例子把这个参数改小一点,然后执行一个返回数据比较多的命令。比如ipconfig -all
或者 comm = "ping 127.0.0.1 -n 10"。
如果传入的数据超过了参数的字节限制,只会先接收限制的字节数。不过未接收的部分不会丢弃而是会继续留在队列是。等待下一次接收。并且后面传入的数据也会继续排队,要先收完前面的数据才能收到后面的数据。
这个参数不是无限大的,因为即使python可以设置一个很大的值,但是系统层面一次接收不了无限大,所以遇到大文件的情况的一次是接收不完的,需要反复接收
import socket
client = socket.socket()
client.connect(('localhost',11111))
comm = "ipconfig -all" # 这个命令返回的结果应该会比较多
while comm:
client.send(comm.encode("utf-8")) # 发送数据要转码成bytes类型
data = client.recv(100) # 假设我只能接收100个字节
print('recv:',data.decode('gbk')) # 打印结果,这里肯定接收不完
print("***一次接收不完***")
data = client.recv(1024) # 接收不完还能继续接收之前没收完的数据
print('recv:',data.decode('gbk'))
comm = input(">>:")
else:
print("准备断开连接")
client.close()
上面是故意调小了recv的参数,但是实际应用中,传递大文件设置是电影视频的话可能会超出系统的最大值的,所以一定有一次接收不完的情况,那就需要多收几次
socket.send
socket.send
将数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于string的字节大小。即:可能未将指定内容全部发送
和recv一样,sent也有字节数的限制。不过命令本身没有参数限制,系统还是有限制的。所以要发送大数据,send也需要反复发送多次。不过这里有一个sendall的方法可以使用socket.sendll
将数据发送到连接的套接字,但在返回之前会尝试发送所有数据。成功返回None,失败则抛出异常。有了这个方法,发送数据就比较简单了。其实sendall的内部也是通过递归调用send,将所有内容发送出去的。
顺便提一句,send有sendall,但是recv只有这一个方法。
发送和接收文件的方法
方法上面都提到了,数据太大可能需要多次send或者用sendall,收的时候也要收多次才能收完
发送端,任何文件都可以以rb的方式打开,然后读取二进制的内容,再把二进制发送出去。
接收端,同样以wb方式新建一个文件,然后把接收到的二进制顺序写入,最后保存。
如此便能完成文件的传送。
开发简单的FTP:
- 用户登录
- 上传/下载文件
- 不同用户家目录不同
- 查看当前目录下文件
- 充分使用面向对象知识