经典类 和 新式类

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  # 执行后将抛出异常

Socket模块

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:

  1. 用户登录
  2. 上传/下载文件
  3. 不同用户家目录不同
  4. 查看当前目录下文件
  5. 充分使用面向对象知识