江湖浪人 发表于 2018-8-3 13:05:25

Python自动化开发学习7

经典类 和 新式类
  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(&quot;Bob&quot;)这个实例话过程的返回值。

反射
  通过字符串映射或修改程序运行时的状态、属性、方法, 有以下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')# 这个元祖只有第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')# 这个元祖只有第0项,我想在要取第1项
  
IndexError: string index out of range
  
'''
  

  虽然放到了try里,但是新的异常种类并没有写到except里,所以依然会抛出错误,下面再把这个异常种类写进去:
  

try:  ('a')
  print(a)
  
except NameError as e:
  print("变量名不存在:%s"%e)
  
except IndexError as e:
  print("索引错误:%s"%e)
  
print("===结束===")
  

  try中的代码块一旦执行到错误,就不会再执行后面的代码了。捕获到异常后,直接就去找except。如果错误类型不在except里,仍然会抛出错误。如果错误类型符合,就执行这个except代码块内的代码,然后跳出整个try代码块继续往后执行。
  还可以这样,把几种异常种类写一起
  

try:  ('a')
  print(a)
  
# except后面只接受1个参数,多个错误类型要写成元祖
  
except (NameError,IndexError) as e:
  print("变量名或索引错误:%s"%e)
  
print("===结束===")
  

万能异常捕获
  我们还可以使用Exception这个错误类型(也可以缺省错误类型),捕获所有的错误:
  

try:  ('a')
  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是异常类型,可以是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模块
  socket通常也称作&quot;套接字&quot;,用于描述IP地址和端口,是一个通信链的句柄,应用程序通常通过&quot;套接字&quot;向网络发出请求或者应答网络请求。
  建立一个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 = &quot;ping 127.0.0.1 -n 10&quot;。
  如果传入的数据超过了参数的字节限制,只会先接收限制的字节数。不过未接收的部分不会丢弃而是会继续留在队列是。等待下一次接收。并且后面传入的数据也会继续排队,要先收完前面的数据才能收到后面的数据。
  这个参数不是无限大的,因为即使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:


[*]用户登录
[*]上传/下载文件
[*]不同用户家目录不同
[*]查看当前目录下文件
[*]充分使用面向对象知识
页: [1]
查看完整版本: Python自动化开发学习7