曾经,我非常羡慕那些人见人爱的人,我也想要变成那样,可是后来我才明白人见人爱也是需要天赋的,后来我开始默默努力,我想,就算我不能让每个人都喜欢我,至少因为我做的努力能得到别人的尊重。

python再探

  python是一门强大的高级编程语言,之前的文章中介绍了python的基础知识,接下来会介绍一些python更为高级的知识。 

  

面向对象编程

基本知识

  一般编程思想分为面向过程和面向对象,前者的基本单元是函数,一步做一件具体的事情;而后者的基本单元是对象,是根据功能进行编程。 面向对象更具有扩展性、维护性等优点,下面我们就介绍python的面向对象编程。

  面向对象的核心概念是封装、多态、继承,而最为基础的是类和实例,类是抽象的概念,其中包括了数据和方法,我们可以认为类是模板;而实例是具体的概念,是通过类实例化而得到的,实例就是类这个模板生产出来的。 

  在python中,类的声明使用的也是class关键词,如下所示(该文件名称为foo.py):

class Student(object):

  def __init__(self, name, score):
    self.name = name
    self.score = score

  def print_score(self):
    print('%s: %s' % (self.name, self.score))
  • class作为声明类的创建词,类可以看做一个模板,其中的Student的首字母S要大写,另外,object是说这个类继承的时object,但也可以继承其他需要继承的类,只是如果没有可继承的了,就继承object,因为所有的类都是继承了object了的。 
  • __init__函数是初始化所必须的函数,并且一定是在init前后有分别有两个下划线,且self变量是第一个变量,这个死规定,self类似于this,用于指向生成的实例,而这里的name和score是程序员自定义的。于是,这里的name和score可以看做类的数据。 
  • 而print_score()是Student类的方法,这个方法也必须接受至少一个参数self,用于使用Student类的私有变量。

  上述代码在foo.py文件中,如果我们希望使用,一般需要将之看做一个模块,然后在另外一个文件main.py中使用,如下:

from foo import Student

wayne = Student('wayne', 100)
hedy = Student('hedy', 99)

wayne.print_score()
hedy.print_score()
  • 其中 from 是说从哪个文件引入的,即从foo.py引入; 而import是我们引入使用的。
  • wayne和hedy是我们通过实例化得到的,而Student接受了两个参数作为初始化的值。 

我们也可以写成下面的方式:

import foo

wayne = foo.Student('wayne', 100)
hedy = foo.Student('hedy', 99)

wayne.print_score()
hedy.print_score()

  

最终输出结果如下:

wayne: 100
hedy: 99

 

但目前的问题是: 我们不仅可以通过实例访问到方法,还可以访问它的属性,这是我们所不希望的,即wayne.name = 'another'这样的操作不是我们希望的。

 

为了不让实例直接访问name和score属性,我们可以设置为私有属性,在python中,类的数据变量前如果添加了__那么这个变量就成了私有变量,如下;

class Student(object):

  def __init__(self, name, score):
    self.__name = name
    self.__score = score

  def print_score(self):
    print('%s: %s' % (self.__name, self.__score))

这时如果我们希望访问:

# from foo import Student
import foo

wayne = foo.Student('wayne', 100)
hedy = foo.Student('hedy', 99)

wayne.print_score()
hedy.print_score()

print(wayne.__name)

最后一句代码就会报错:

    print(wayne.__name)
AttributeError: 'Student' object has no attribute '__name'

即我们不能通过实例直接访问到这个__name私有变量。 

 

而上述目的是为了不修改,但我们还是可以获取的,如给Student类添加一个get_name方法,如下:

  def get_name(self):
    print('%s' % self.__name)

这样,我们调用wayne.get_name()就可以获取到实例的__name值了。

 

但如果我们真的希望通过这种方式来修改变量,我们也可以给类添加一个set_name方法,如下:

  def set_name(self, new_name):
    self.__name = new_name

这样,我们就可以通过下面的方式进行修改了:

wayne.set_name('wayne-zhu')
wayne.get_name() # 'wayne-zhu'

需要注意的是:一定不要直接wayne.__name = 'wayne-zhu'进行修改,这样无法正确修改。   另外,也不是完全不可访问的,只是python解释器将__name修改为了_Student__name了,如下:

print(wayne._Student__name)

但是,我们还是不建议这样的访问方式,并且只是当前使用的python解释器在__name之前添加了_Student,而其他的解释器也许就不是这样添加的了。  

有时候,我们也可以看到_name这样一个下划线的变量,这个变量并不是实例不可以访问,只是不建议访问。 

 

 

继承和多态

  之前我们谈到class Student(object)中的object是基类,是没有继承的,所以这里就要说说继承是怎么样的! 

class Animal(object):
  def run(self):
    print('animal is running!')

class Dog(Animal):
  pass

class Cat(Animal):
  pass

animal = Animal()
animal.run()

dog = Dog()
dog.run()

cat = Cat()
cat.run()

  如上所示,Animal作为基类、父类,而Dog、Cat作为子类,子类自动继承了父类的方法run。最终结果是输出了三句: animal is running! 这就是面向对象编程的第一大特点:继承。

  而作为子类,也是可以有自己的方法的,并且如果和父类方法名相同,就会覆盖父类的方法:

class Animal(object):
  def run(self):
    print('animal is running!')

class Dog(Animal):
  def run(self):
    print('dog is running!')

class Cat(Animal):
  def run(self):
    print('cat is running!')

animal = Animal()
animal.run()

dog = Dog()
dog.run()

cat = Cat()
cat.run()

   这里最终结果如下:

animal is running!
dog is running!
cat is running!

    这样,我们就获得了多态的另外一个好处: 多态!要理解多态,我们还需要对数据类型做更多了解,如下:

animal = Animal()
dog = Dog()
cat = Cat()

print(isinstance(animal, Animal))
print(isinstance(dog, Dog))
print(isinstance(dog, Animal))
print(isinstance(cat, Cat))
print(isinstance(cat, Animal)) #全部是True

     我们可以看到,这里dog是Dog类型还是Animal类型,cat同样如此,这也很好理解,因为Dog和Cat都是从Animal中继承得到的。

    要更进一步理解多态,我们还需要写一个这样的函数:

def run_twice(animal):
  animal.run()
  animal.run()

    即run_twice函数,我们这样调用:

run_twice(animal)
run_twice(dog)
run_twice(cat)

   得到的结果如下:

animal is running!
animal is running!
dog is running!
dog is running!
cat is running!
cat is running!

  可见,对于同样的函数,参数不同,结果不同。

  并且我们还可以新增一种类型:

class Sheep(Animal):
  def run(self):
    print('sheep is running!')

  然后我们调用run_twice,即run_twice(Animal()),结果如下:

sheep is running!
sheep is running!

  于是可知,我们新增了一个Animal的子类,而对于run_twice()没有任何修改,任何依赖Animal作为参数的函数或方法都可以不加修改的正常运行,这就是多态。

  创建一种Animal的子类时,只要run()方法编写正确,不用管原来的代码是如何调用的,这就是注明的开闭原则 --- 对外开放类:允许增加Animal子类;对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

鸭子类型

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。

    这是不难理解的,run_twice()就是一个普通函数,这就是动态语言的鸭子类型,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那他就可以被看做是鸭子。

 

获取对象信息

  在python中,主要有type()、instanceof()、dir()等方法进行类型判断。JavaScript中也有相应的类型,比如typeof、isinstanceof等,所以编程语言之间还是比较相似的。

type()

  type对所有的类型都是可以判断的,基本类型如下:

>>> type(123)
<class 'int'>
>>> type('str')
<class 'str'>
>>> type(True)
<class 'bool'>
>>> type(None)
<class 'NoneType'>
>>> type([1, 2, 3])
<class 'list'>
>>> type((1, 2, 3))
<class 'tuple'>
>>> type({'name': 'wayne', 'age': 22})
<class 'dict'>

  可以看到:这里的基本类型都是class开头的内置类型。 

  另外,如果使用if语句进行判断,我们还可以使用下面的方式:

>>> type(123) == type(456)
True
>>> type(628) == int
True
>>> type('abc') == type('123')
True
>>> type('abc') == str
True
>>> type(True) == bool
True

 

instance()

  对于class的继承,使用instance会方便很多,比如之前的object、Animal、Dog,使用instance进行判断会方便很多。

dir()

  使用dir()可以获得一个对象的所有属性和方法,使用dir函数返回的是一个包含字符串的list,比如获得一个str对象的属性和方法:

>>> dir('wayne')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

  另外使用hadattr和getattr等方法是比较有用的。

 

 

 

实例属性和类属性

  类属性是属于这个类的,而实例属性是属于实例的,两者是完全不想干的,但是我们可以通过实例访问到类属性,但是如果实例属性和类型的名称相同时,会返回实例属性的值(即覆盖)。   

>>> class Student(object):
...     name = 'Student'
...
>>> s = Student() # 创建实例s
>>> print(s.name) # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name) # 打印类的name属性
Student
>>> s.name = 'Michael' # 给实例绑定name属性
>>> print(s.name) # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name) # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name # 如果删除实例的name属性
>>> print(s.name) # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student

 

 

 

面向对象高级编程

使用__slots__

   python作为一个动态编程语言,创建类之后,可以通过该类创建实例,然后还可以给实例绑定一些属性和方法,这都是不受限制的,而在静态编程语言中是受到限制的。

class Student(object):
  pass

s1 = Student()
s1.name = 'wayne'
print(s1.name)
s1.score = 100
print(s1.score)

  但是,如果我们不希望对这些实例添加一些属性,从而使得程序更规范,而不被随意改动,就可以像下面这样:

class Student(object):
  __slots__ = ('name', 'age')

s1 = Student()
s1.name = 'wayne'
print(s1.name)
s1.age = 22
print(s1.age)
s1.score = 100 #报错
print(s1.score)
  • 即这里使用了特殊变量__slots__,并且将一个tupple赋值给了__slots__,这样就限制了实例只能定义__slots__中的属性。
  • 但是这个限制是不能继承下去的,比如有一个anoStudent继承了Student类,那么这个继承的类可以定义任意属性,不受父类__slots__的限制。 
wayne
22
Traceback (most recent call last):
  File "foo.py", line 9, in <module>
    s1.score = 100
AttributeError: 'Student' object has no attribute 'score'

 

  

使用@property

  @property的主要作用是可以对属性在设置的过程中做类型检查,并且直接将方法作为属性方式进行调用的。

  首先,我们写一下传统的方式:

class Student(object):
  pass

s = Student()
s.score = 99
print(s.score) //99

  显然,这种方式是不合理的,因为如果s.score设置成了9999,显然不符合规定,而我们这里又没有处理,这是不合适的。我们可以设置一些方法修改:

class Student(object):
  def get_score(self):
    print(self._score)

  def set_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

s = Student()
s.set_score(99)
s.get_score()
s.set_score(9999)

  即通过get_score获取成绩,而在set_score的过程中,如果int不满足要求,则报错。 

  但是这样的方法又比较困难,所以,我们可以使用装饰器(decorator),这里我们使用@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

  如上所示:Student类中的两个方法名称相同而参数不同,一个是用于修改属性值并检查合法性的,另外一个是获取属性值的。使用方法如下:

>>> 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方法我们还可以定义只读属性,即不定义setter方法即可,如下:请利用@property给一个Screen对象加上widthheight属性,以及一个只读属性resolution。

  

class Screen(object):
  @property
  def width(self):
    return self._width

  @width.setter
  def width(self, value):
    self._width = value

  @property
  def height(self):
    return self._height

  @height.setter
  def height(self, value):
    self._height = value

  @property
  def resolution(self):
    return self._width * self._height


s = Screen()

s.width = 1024
print(s.width)
s.height = 768
print(s.height)

print(s.resolution) #786432
s.resolution = 100 #AttributeError: can't set attribute

如上所示:我们使用@property进行读取,使用@方法名.setter进行设置,如果一个属性同时具备两者,说明这是一个可读写属性; 而如果一个属性只有其中一个@property,那这个属性是一个只读属性。

 

 

多重继承

  python是支持多重集成的,如class Student(People, Chinese),这个新定义的Student类就同时继承了People类和Chinese类,并且这个Student类的实例也同时拥有了People和Chinese两个类的实例,并且还可以给Student定义新的方法,这就是多重继承,剁成继承使得复用性得到了很好的应用。

  

 

定制类

  看到类似__slots__和__init__的变量和函数名就要注意,这些在python中是有特殊用途的,前两者我们已经知道其用途,下面再介绍其他的一些特殊变量。

 

__str__  

class Student(object):
  def __init__(self, name):
    self.name = name

print(Student('wayne')) #<__main__.Student object at 0x0000022DB874C048>

 如上所示,我们将Sdudent('wayne')打印出来,但是这种形式不易查看,我们就可以定制一个特定的打印结果,如下:

class Student(object):
  def __init__(self, name):
    self.name = name
  def __str__(self):
    return 'Student object (name: %s)' % self.name

print(Student('wayne')) # Student object (name: wayne)

 上面我们使用了__str__这个特殊方法,这样打印出来的就比较易懂了。

 而如果我们是 s = Student('wayne'),然后输入s后回车,这是又回到原来的输出方式了,因为这时调用的时__repr__方法,我们在上面Student类下面添加一句 __repr__ = __str__ 即可。

 

 

使用枚举类

  python中提供了 Enum 这个类实现枚举的功能,如下所示:

from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
for name, member in Month.__members__.items():
  print(name, '=>', member, ',', member.value)

  最终结果如下:

Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12

  由此可见,通过Enum,可以方便的让我们对之进行遍历。即name获取名称、member获取完整名称、value获取其对应的数字(从1开始计数)。

  而如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique

@unique
class Weekday(Enum):
  Sun = 0
  Mon = 1
  Tue = 2
  Wed = 3
  Thu = 4
  Fri = 5
  Sat = 6

  这样,我们可以使用下面的方式进行访问:

>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
  ...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
...     print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat

 

  

使用元类

  即metaclass。我们知道,当我们定义出类之后,就可以根据这个类创建出实例,所以是先定义类然后创建实例;但是如果我们想创建出类呢?这时我们就必须根据metaclass创建出类。所以:先定义metaclass,然后创建类。

  链接起来就是 --- 先创建metaclass然后创建类,最后创建实例。因此我们可以将类看做由metaclass创建出来的实例。

 

 

 

 

错误、调试和测试

  错误:程序运行过程中会出现各种各样的错误。如程序本身的错误 --- 本该输出字符串却输出整数,这是bug; 还有用户输入的错误 --- 如用户应该输入整数却输入字符串导致程序无法正常执行,这可以通过检测用户输入而解决;另外还有一些异常情况,比如磁盘内存满了、网络断了等,这是异常,也要进行异常处理。 

  调试:而在找寻这些错误的过程中,我们需要进行debug,就是调试。

  测试:在程序编写完成之后,我们还要进一步的测试,以保证不出现问题。

  下面我们就一一讲解。

 

错误处理

  错误处理的目的是为了处理错误,给用户提示,并保证在这段出错代码之后的程序也可以正常执行。   

  python的异常处理和JavaScript是非常类似的,只是在语法上有一些差异,比如在js中使用的时try...catch...finally,try中的代码是可能出错的代码,出现错误之后,由catch捕捉,然后finally是无论代码有错与否都会执行的代码段,另外,对于一些错误我们还可以使用throw抛出自定义类型的错误,这样在捕捉的过程中也方便使用。而python的时try...except...finally,即try中的代码是可能出错的代码,except是处理错误的过程,而finally是必须执行的代码,在python中抛出错误使用的时raise关键词。

  同样地,在python中,错误也是会像JavaScript的作用域链一样层层向上传递,如果当前作用域有except就执行,而不再向山,否则一直向上,如果最顶层也没有错误处理函数,那么程序就将会终止。而如果在较底层的作用域中已经有错误处理函数处理了错误,并且继续raise(抛出),那么这个错误还会继续抛出到上面的作用域中寻找错误处理函数,原因很简单,我们可以认为当前错误处理函数是个小兵处理不好错误就继续把错误报告给了领导,让领导继续处理错误。

def foo(s):
  return 10 / int(s)

def bar(s):
  return foo(s) * 2

def main():
  try:
    bar('0')
  except Exception as e:
    print('Error:', e)
  finally:
    print('finally...')

main() 

#Error: division by zero
#finally...

  从上面的例子中我们可以看到,这样传递Exception的好处在于我们可以直接将错误处理函数写在最外层(main函数),这样可以使逻辑更加清楚、程序更加简洁,而不需要在每一层都写错误处理函数。

 

记录错误

  在python中,可以引入logging模块非常容易的记录错误,这样,处理完错误之后会继续执行后面的函数,如下:

import logging

def foo(s):
    return 10 / int(s)

def bar(s):
    return foo(s) * 2

def main():
    try:
        bar('0')
    except Exception as e:
        logging.exception(e)

main()
print('END')

  最终结果如下:

ERROR:root:division by zero
Traceback (most recent call last):
  File "foo.py", line 11, in main
    bar('0')
  File "foo.py", line 7, in bar
    return foo(s) * 2
  File "foo.py", line 4, in foo
    return 10 / int(s)
ZeroDivisionError: division by zero
END

  当然,我们也可以利用python的文件输入输出功能来将错误日志打印到本地文件中。 

 

调试

  大多数程序写完都是会有bug的,所以就需要进行调试了。

  第一种方法和js类似,js中通过alert或者console.log把中间过程的变量打印出来看结果,而python也是的,可以通过print()函数把中间的一些变量打印出来,观察其值是否有问题。但是这种方法的问题是麻烦,每次测试之后,还要将这些print()函数删去,这样才能继续。

  第二种方法是使用assert(断言)。即之前使用了print的位置都可以使用asser语句来代替,我们看下面的这样一个例子。

def foo(s):
  n = int(s)
  assert n != 0, 'n is zero'
  return 10 / n

def main():
  foo('1')

main()

  我们可以看到:这里使用了assert, n != 0是说只有当n!=0时才满足条件,如果n为0,那么就会报错: 'n is zero',这就是断言的好处了。

  而如果我们不希望assert的存在,我们可以这样运行: python -O foo.py,注意:这里的-O时大写英文字母O,这样,运行时就会将asser代码行当做pass进行处理。

  第三种方法是使用logging,即将print()替换成logging,但logging不会抛出错误,当前,前两者也是不会抛出错误的,而且可以输出到文件。如下:

import logging
logging.basicConfig(level=logging.INFO)

def foo(s):
  n = int(s)
  logging.info('n = %d' % n)
  return 10 / n

def main():
  foo('0')

main()

  结果如下:

INFO:root:n = 0
Traceback (most recent call last):
  File "foo.py", line 12, in <module>
    main()
  File "foo.py", line 10, in main
    foo('0')
  File "foo.py", line 7, in foo
    return 10 / n
ZeroDivisionError: division by zero

  这样,我们可以看到输出了n = 0,但是也不会抛出错误,这样的方法实际上和JavaScript中使用console.log是一样的。另外,logging允许你指定记录信息的级别,有debug、info、warning、error等几个级别,如果指定了level =INFO,那么logging.debug就不起作用了,同样,指定了level=WARNING后,debug和info也不起作用了,这样,你就可以放心输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。 

  

    第四种方法是启用Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。如以下程序:

s = '0'
n = int(s)
print(10 / n)

    然后,我们可以启动,在命令行输入python -m pdb foo.py即可,可以看到如下效果:

> c:\users\administrator\desktop\foo.py(1)<module>()
-> s = '0'

  即运行到了第一行 s = '0'。 

  输入小写英文字母l可以查看代码:

(Pdb) l
  1  -> s = '0'
  2     n = int(s)
  3     print(10 / n)
[EOF]

  输入命令n可以单步执行代码:执行到最后就会重复了。

(Pdb) n
> c:\users\administrator\desktop\foo.py(2)<module>()
-> n = int(s)

  任何时候我们都可以通过命令  p 变量名 来查看变量。

(Pdb) n
> c:\users\administrator\desktop\foo.py(3)<module>()
-> print(10 / n)
(Pdb) p n
0

  输入命令 q 结束调试,退出程序。

  这种通过pdb在命令行上调试的方法很好,但是很繁琐,我们可以使用下面一种方法。

  

  第五种方法也是要使用pdb,但是不需要单步执行,而是import pdb,然后在可能出错的地方放一个 pdb.set_trace(),这样我们就设置了一个断点

import pdb
s = '0'
n = int(s)
pdb.set_trace() #断点:程序运行到这里会暂停
print(10 / n)

  如上所示,因为我们已经import pdb了,所以,这时我们只需要python foo.py,而不需要使用 -m pdb了 --- 这里的 -m pdb 是运行pdb库模块作为该模块的脚本。如果这里继续使用python foo.py,那么就会逐句调试。  

C:\Users\Administrator\Desktop>python foo.py
> c:\users\administrator\desktop\foo.py(5)<module>()
-> print(10 / n)
(Pdb) p n
0
(Pdb) p s
'0'
(Pdb) q

  

  第六种方法就是使用IDE了,通过IDE我们可以自由地设置断点、单步执行,比较好的就是使用vscode然后安装Python插件。 

  

  

python单元测试

  TDD(Test-Driven Development)即单元驱动开发,用到的就是单元测试。  

  单元测试是用来对一个模块、一个函数或者是一个类来进行正确性检测的测试工作。而其中的单元我们就可以理解为一个模块、一个函数或者是一个类。

     比如对于函数abs(),我们可以编写一下几个测试用例:

  • 输入正数,比如2、6、5.5等,期待的返回值与输入相同。
  • 输入负数,比如-6、-6.5等,期待的返回值与输入相反。
  • 输入0,期待返回0。
  • 输入非数值类型,如'abc'、None等,期待抛出TypeError。

  我们把上面的测试用例放在一个测试模块里,就是一个完整的单元测试。如果单元测试通过,则说明我们测试的函数能正常工作;如果单元测试不通过,要么函数有bug、要么测试条件不正确,总之需要让这个单元测试通过。

 

那么单元测试有什么意义呢?  

比如我们对abs()函数代码做了修改,只需要再跑一遍单元测试即可,如果通过,说明我们修改的abs()函数不会对原有的行为造成影响;如果不通过,则说明出问题了,要么修改函数代码,要么修改单元测试。这种以测试为驱动的开发模式最大的好处就是保证一个程序模块的行为符合我们设计的测试用例,在将来修改的时候,可以极大程度保证该模块行为仍然是正确的。

  下面,我们编写一个Student类,并对之进行单元测试。 下面是student.py中的代码:

class Student(object):
  def __init__(self, name, score):
    self.name = name
    self.score = score

  def get_grade(self):
    if self.score > 100:
      raise ValueError
    elif self.score >= 80:
      return 'A'
    elif self.score >= 60:
      return 'B'
    elif self.score >= 0:
      return 'C'
    else:
      raise ValueError

  即定义一个学生类,根据输入成绩不同,获取不同等级。

  

  接下来 ,我们需要单独建立一个python文件来进行单元测试,以下是test_student.py文件的代码:

import unittest

from student import Student

class TestStudent(unittest.TestCase):

  def test_80_to_100(self):
    s1 = Student('Bart', 80)
    s2 = Student('Lisa', 100)
    self.assertEqual(s1.get_grade(), 'A')
    self.assertEqual(s2.get_grade(), 'A')

  def test_60_to_80(self):
    s1 = Student('Bart', 60)
    s2 = Student('Lisa', 79)
    self.assertEqual(s1.get_grade(), 'B')
    self.assertEqual(s2.get_grade(), 'B')

  def test_0_to_60(self):
    s1 = Student('Bart', 0)  
    s2 = Student('Lisa', 59)
    self.assertEqual(s1.get_grade(), 'C')
    self.assertEqual(s2.get_grade(), 'C')

  def test_invalid(self):
    s1 = Student('Bart', -1)
    s2 = Student('Lisa', 101)
    with self.assertRaises(ValueError):
      s1.get_grade()
    with self.assertRaises(ValueError):
      s2.get_grade()

if __name__ == '__main__':
  unittest.main()

  首先,我们引入 unittest模块进行单元测试,然后,从student.py文件中引入Student类,接下来,单元测试文件中需要新建一个单元测试类TestStudent,继承unittest.TestCase类, 在这个类中,我们再定义几个函数,对Student类中的几种情况进行测试即可。在测试文件的最后我们需要添加 if __name__ == '__main__'等两句。

  然后在cmd中运行,结果如下:

C:\Users\Administrator\Desktop>python student_test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

  这样,就证明对于单元测试的4个测试用例全部通过,这便是单元测试的基本例子了。

 

  

文件读写

  使用文件读写,主要用到的是open函数,这个函数接受四个参数,第一个是文件路径,第二个是使用方式,第三个是编码类型,第四个是对错误的处理方式。

  第一个参数就是路径名,一般用绝对路径。但如果有时读取会出现问题,我们可以使用try...except...finally的方式进行处理。

  第二个参数是使用方式,如r是读取、w是写入、a是追加等,也可以是rb读取二进制文件、wb写入二进制文件。 而在读的过程中,可以是read()读取所有内容,也可以是readline读取一行,还可以是readlines放在一个list中。

  第三个参数为编码类型,即encoding='utf-8'等。

  第四个参数是错误的处理方式,如errors='ignore',这是最为通用的处理方式。 

 

 

StringIO和ByteIO

  文件读写是python对于文件的读和写,而这里是python的读写在内存中进行。 一般需要先引入,如 from io import StringIO或者 from io import ByteIO,这样,我们可以利用StringIO和ByteIO类进行创建实例,创建的实例可以write以及read,在读取的时候我们一般使用的时getvalue(),注意,这里的v时小写的。

 

    

 操作文件和目录

  要使用python操作,需要引入os模块,即operate system操作系统模块,这个对象下有很多方法我们是可以进行调用的。这是因为操作系统提供了操作文件和目录的接口,python调用了这些接口,所以可以操作操作系统。

os.name # 'nt'表示的windows系统

  而操作系统中定义的环境变量可以用os.environ来获取,path(当前路径)可以直接os.path获取,对于系统内的环境变量我们可以是os.environ.get('path'),如下:

>>> os.environ.get('path')
'C:\\Users\\Administrator\\Anaconda3\\Library\\bin;C:\\Windows\\System32;C:\\Program Files\\nodejs;C:\\Program Files\\nodejs\\node_modules;C:\\Users\\Administrator\\Desktop\\shortcut;C:\\MinGW\\bin;D:\\matlab\\runtime\\win64;D:\\matlab\\bin;D:\\runtime\\win64;D:\\xampp\\mysql;C:\\Program Files\\MongoDB\\Server\\3.4\\bin;C:\\Windows\\System32\\curl-7.54.0-win64-mingw\\bin;C:\\Program Files (x86)\\Subversion\\bin;C:\\Program Files\\TortoiseSVN\\bin;D:\\programs\\java\\bin;C:\\Program Files\\MacType;C:\\Program Files (x86)\\Windows Kits\\8.1\\Windows Performance Toolkit\\;D:\\programs\\VC\\bin;D:\\bin;D:\\polyspace\\bin;D:\\matlab\\polyspace\\bin;C:\\Users\\Administrator\\Anaconda3;C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python36-32\\Scripts\\;C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python36-32\\;C:\\Users\\Administrator\\AppData\\Local\\Microsoft\\WindowsApps;C:\\Users\\Administrator\\AppData\\Roaming\\npm;C:\\Program Files (x86)\\Microsoft VS Code\\bin'

  如果我们希望获取当前的绝对路径,另外,使用os.path.join()os.path.split()又可以合并路径后者分离路径。

>>> os.path.abspath('.')
'C:\\Users\\Administrator\\Desktop'

  我们还可以使用python的os模块操作本地文件,如mkdirrmdir分别是创建文件夹删除文件夹

>>> os.mkdir('foo')
>>> os.rmdir('foo')

  另外,我们还可以使用os.rename给文件重命名,使用os.remove删除文件

>>> os.rename('foo.txt', 'wayne.txt')
>>> os.remove('fo.txt')

  即使用python可以实现对本地操作系统的完全操作,非常强大

  使用os.listdir()可以将当前路径下的所有文件名打印在一个list中:

>>> os.listdir()
['001.png', 'bbg', 'ce20180319.cpp', 'ConsoleApplication1', 'cpp项目文件.rar', 'desktop.ini', 'foo.py', 'hello.py', 'readConfig', 'recrystal', 'shortcut', 'vtk.cpp', 'wayne.txt', '__pycache__', '~$专业实习报告.docx', '~$操作系统1.doc', '大 四上课程', '宝宝相关', '实验相关', '火萤.lnk', '白子祥的文件', '简历', '简历投递.docx', '课题设计相关文件']

  

  在当前目录使用os.path表示的,而对于其中的文件,我们希望判断是否是文件夹,可以使用os.path.isdir()来进行判断,如下:

>>> os.path.isdir('白的文件')
True
>>> os.path.isdir('vtk.cpp')
False

   而使用os.path.isfile()可以判断是否是文件:

>>> os.path.isfile('vtk.cpp')
True
>>> os.path.isfile('白的文件')
False

  使用os.path.join()接受多个字符串,可以将他们合并。

  

   

序列化

  JavaScript中的JSON就存在序列化与反序列化之说,而python中同样存在,python中特定的序列化与反序列化使用的时pickle模块,即pickle.dumps()是序列化的过程,而pickle.loads()是反序列化的过程。 dumps理解为下饺子,将饺子馅包起来我们就可以得到具体可以存放的内容了,而loads是捞出来,保持原来的状态,暂时就可以理解记忆吧。。   

>>> import pickle
>>> d = dict(name='wayne', age= 22, male = 'f')
>>> d
{'name': 'wayne', 'age': 22, 'male': 'f'}
>>> r = pickle.dumps(d)
>>> r
b'\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x05\x00\x00\x00wayneq\x02X\x03\x00\x00\x00ageq\x03K\x16X\x04\x00\x00\x00maleq\x04X\x01\x00\x00\x00fq\x05u.'
>>> pickle.loads(r)
{'name': 'wayne', 'age': 22, 'male': 'f'}

   如上所示,先引入了pickle,然后我们建立了一个dict,接着序列化保存为r,接着使用pickle.loads进行反序列化。 当然,在序列化之后,就是字符串了,我们就可以将之存入文件了或者进行传输了,而如果不进行序列化,那么一个dict是不能进行传输,而前后端之间的合作也就没法进行了。

  

   另外一个方式是使用JavaScript中的JSON进行序列化,如下:

>>> import json
>>> d = dict(name='wayne', age=22, sex = 'f')
>>> d
{'name': 'wayne', 'age': 22, 'sex': 'f'}
>>> json.dumps(d)
'{"name": "wayne", "age": 22, "sex": "f"}'
>>> r = json.dumps(d)
>>> r
'{"name": "wayne", "age": 22, "sex": "f"}'
>>> json.loads(r)
{'name': 'wayne', 'age': 22, 'sex': 'f'}

  如上所示,使用JSON和使用pickle是非常类似的。

  

  接下来我们再看看JSON中的一个ensure_ascii参数,如下:

>>> obj = dict(name='小明', age=20)
>>> s = json.dumps(obj, ensure_ascii=True)
>>> s
'{"name": "\\u5c0f\\u660e", "age": 20}'
>>> s = json.dumps(obj, ensure_ascii=False)
>>> s
'{"name": "小明", "age": 20}'

  如上,我们可以看到,如果ensure_ascii为True,那么说明只允许ascii字符的出现,如果为False,则可以储存汉字。

 

 

 

进程和线程

  进程和线程是操作系统的知识,即进程代表一个任务,而线程是一个子任务,一个进程至少有一个线程,另外,进程是可以并行的,多核CPU可以更好的实现进程和线程的并行。

  python作为高级语言,是可以创建进程和线程的,而这一部分,就是为了介绍python创建进程和线程。

  

fork

  fork是创建进程的一种方法,但是这种方法在windows系统上是没有的,所以无法测试,而如果使用linux就可以使用fork函数创建进程了。

  注意使用fork的过程中需要引入os模块,通过os.fork()创建了一个自进程,而当前进程为父进程,其fork函数会返回两次,子进程返回0,父进程返回了子进程的ID。且os有获取进程的两种方式,os.getpid()是获取当前进程(这个进程可能是父进程也可以是子进程,且注意父进程和子进程也只是相对的),而os.getppid()是获取当前进程(os.getpid())的父进程。

   

multilprocessing

    因为python是跨平台的,所以,当然是有方法可以在windows下创建多进程的,这就是multiprocessing了。multiprocessing模块提供了Process类来创建一个子进程。

from multiprocessing import Process #引入Process类是为了创建子进程
import os #os模块即操作系统模块,可以获取当前进程的ID或者父进程的ID

#子进程要执行的代码
def run_proc(name):
  print('Run child process %s (%s)...' % (name, os.getpid()))

if __name__ == '__main__':
  print('Parent process %s.' % os.getpid())
  p = Process(target=run_proc, args=('test',))
  print('Child process will start.')
  p.start()
  p.join()
  print('Child process end')

  如上所示:我们引入了multiprocessing模块的Process类用于创建子进程,提供两个参数,一个是子进程的运行函数,另外一个是我们希望给这个函数传入的参数,Process返回这个子进程,然后我们直接调用start()函数就可以执行这个子进程,使用join()函数是等待子进程结束后继续往下执行。 

  

 

Pool

  如果我们希望创建大量的子进程,就可以使用线程池的方式创建子进程了。如下:

from multiprocessing import Pool
import os, time, random

#进程运行函数 def long_time_task(name): print(
'Run task %s (%s)...' % (name, os.getpid())) start = time.time() time.sleep(random.random() * 3) end = time.time() print('Task %s runs %0.2f seconds.' % (name, (end - start))) if __name__ == '__main__': print('Parent process %s.' % os.getpid()) p = Pool(4) for i in range(6): p.apply_async(long_time_task, args=(i,)) print('Waiting for all subprocesses done...') p.close() p.join() print('All subprocesses done.')

  最终结果如下所示:

Parent process 11008.
Waiting for all subprocesses done...
Run task 0 (864)...
Run task 1 (9440)...
Run task 2 (10976)...
Run task 3 (10220)...
Task 3 runs 0.00 seconds.
Run task 4 (10220)...
Task 2 runs 0.07 seconds.
Run task 5 (10976)...
Task 0 runs 0.60 seconds.
Task 1 runs 1.74 seconds.
Task 4 runs 2.41 seconds.
Task 5 runs 2.42 seconds.
All subprocesses done.

  我们可以看到:因为直接执行的时这个程序,所以,if  __name__ == '__main__' 返回True,继续执行下面的步骤。

  • 打印出当前进程的进程ID。
  • 创建一个每次最多容纳4个进程的进程池。当然这个大小可以由我们自行设定。
  • 接着,开始设定6个子进程,让子进程执行。
  • 6个子进程执行的过程中,会打印出子进程的id,然后利用time.time()函数继续子进程运行时间,而实际上其运行依靠的是time.sleep(),最后打印出其运行时间。
  • 注意:因为进程池的个数限定为4,所以,最多一次运行4个子进程。等待其中的某个运行结束,才会继续让下一个进程开始运行
  • 最终所有进程运行完毕,就可以结束了。

  

  

  

 子进程 ???

  子进程可以可以由subprocess模块启动,然后控制器输入输出:

import subprocess

print('$ nslookup www.python.org')
r = subprocess.call(['nslookup', 'www.python.org'])
print('Exit code:', r)

  如上所示:引入了subprocess,然后结果如下:

$ nslookup www.python.org
Server:  UnKnown
Address:  192.168.43.1

Non-authoritative answer:
Name:    python.map.fastly.net
Addresses:  2a04:4e42:6::223
          151.101.24.223
Aliases:  www.python.org

Exit code: 0

  

 

进程间通信

   Process之间一定要通信的,multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。 

  我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

#进程间通信
from multiprocessing import Process, Queue # Process用于创建进程,Queue用于进程间通信
import time, os, random # time用于计时, os用于获取进程ID,random用于进程任意时间的sleep

#写数据进程执行的代码,其中q是用于进程通信的Queue
def write(q):
  print('Process to write: %s' % os.getpid())
  for value in ['A', 'B', 'C']:
    print('Put %s to queue...' % value)
    q.put(value)
    time.sleep(random.random())


#读数据进程执行的代码,同样q是用于进程通信的Queue
def read(q):
  print('Process to read: %s' % os.getpid())
  while True:
    value = q.get(True)
    print('Get %s from queue.' % value)

if __name__ == '__main__':
  #在父进程中创建Queue,然后传给各个子进程
  q = Queue()
  pw = Process(target=write, args=(q,)) #创建写进程
  pr = Process(target=read, args=(q,)) #创建读进程
  # 创建之后必须要启动才能执行:
  pw.start()
  pr.start()
  # 等待写进程结束
  pw.join()
  pr.terminate() 

  如上所示,我们在父进程中创建q,然后将q传递给两个子进程,这样,一个子进程实现写入,另外 一个实现读取,就可以完成进程间的通信了。

   而从上面的几个例子我们可以看出:Process创建子进程一般可以接受两个参数,第一个是target=fun,fun是子进程需要执行的函数,而第二个参数是args=(),后面是一个tupple,这里的参数是fun中需要接受的参数。

   且对于Process创建的子进程,需要通过strart()来开始执行,通过terminate()强制结束(注意:这个不是必须的)。 

 

 

python多线程

  一个进程由至少一个线程组成,多任务可以由多进程完成,也可由一个进程的多线程完成。

  另外,线程是操作系统执行的最小执行单元,高级语言一般都可以创建线程,python也不例外,并且线程不是模拟的,而是真正的线程。在python中提供了两个封装了底层的模块_thread和threading,而前者是较低级的,后者是较高级的,所以我们一般使用threading就可以了。

>>> import threading
>>> threading.current_thread().name
'MainThread'

  我们可以看到,引入threading模块之后,调用current_thread方法,然后获取name就可以得到MainThread,因为当前进程一定是有一个线程的,就是主线程。

  

import threading, time

# 新线程执行的代码
def loop():
  print('thread %s is running' % threading.current_thread().name)
  n = 0
  while n < 5:
    n = n + 1
    print('thread %s >>> %s' % (threading.current_thread().name, n))
    time.sleep(1)
  print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

  首先,引入threading模块,然后定义了loop函数,这个函数就是用于执行线程的,最后,通过threading.Thread()函数创建一个线程,接着使用start()使得线程开始执行,再等线程执行完毕之后执行下面的函数。我们可以看通过threading.current_thread().name可以获取名称。结果:

thread MainThread is running...
thread LoopThread is running
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.

 

 

Lock

  多进程和多线程最大的区别就是数据的共享问题。如多进程中,每个进程都会拷贝一份自己的数据,互不影响;而多线程中,这些线程会共享这个进程中的数据,某个线程修改了数据,其他线程拿到的就是修改之后的数据。我们看看线程之间是如果修改的:

import time, threading

#银行存款
balance = 0

def change_it(n):
  #先存后取,结果应该为0:
  global balance
  balance = balance + n
  balance = balance - n

def run_thread(n):
  for i in range(100):
    change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

  如上所示:我们创建了一个balance变量,然后定义了一个change_it函数,这个函数中global balance说明这里面balance再改变的时候会改变外部的balance,即全局变量;接着又定义了一个线程执行函数,这个函数再不断地调用change_it函数。接下来定义了两个线程,这两个线程都会运行过程中调用run_thread函数,最后就是执行线程,完毕之后停止。

  • run_thread函数中range里数字比较小,如100,则最终结果为0,这是我们所期望的。
  • 但是当数字大一些,如100万时,最终的结果一般就得不到0了,而是在随机变化,这是为什么呢?

  因为在执行时,一个简单的语句是分两步进行的,如 banlance = banlance + n是先将等式右边的值计算出来之后复制给一个中间变量,然后再将中间变量赋值给 banlance,如下:

x = balance + n 
balance = x

  如上所示, x就是中间变量了。

  

  显然,中间变量x是局部变量,两个线程都有自己的x,当代码正常执行时:

初始值 balance = 0

t1: x1 = balance + 5 # x1 = 0 + 5 = 5
t1: balance = x1     # balance = 5
t1: x1 = balance - 5 # x1 = 5 - 5 = 0
t1: balance = x1     # balance = 0

t2: x2 = balance + 8 # x2 = 0 + 8 = 8
t2: balance = x2     # balance = 8
t2: x2 = balance - 8 # x2 = 8 - 8 = 0
t2: balance = x2     # balance = 0

结果 balance = 0

  然而,由于多线程是并发执行的,即一个线程执行一段时间之后又会把cpu给另外一个线程使用进行执行,这样,两个线程执行的情况可能如下所示:

初始值 balance = 0

t1: x1 = balance + 5  # x1 = 0 + 5 = 5

t2: x2 = balance + 8  # x2 = 0 + 8 = 8
t2: balance = x2      # balance = 8

t1: balance = x1      # balance = 5
t1: x1 = balance - 5  # x1 = 5 - 5 = 0
t1: balance = x1      # balance = 0

t2: x2 = balance - 8  # x2 = 0 - 8 = -8
t2: balance = x2   # balance = -8

结果 balance = -8

  因为虽然x1和x2是各自的,但是balance对于两个线程来说是共享的,这样就导致了最后的结果偏离了我们的期望,并且不是确定的。


  显然,我们不希望自己的存款再几次正常交易之后变成负数,所以,我们希望一个线程在修改balance的时候另外一个线程不要修改,至少是一个赋值语句会分成两个,在这两个之间不要插入其他的语句,这样就能保证数据的准确了。

  为了保证balance计算正确,我们需要给change_it()上一把锁,这样,在某个线程执行change_it()时,由于它上了一把锁,其他的线程就不能执行这个change_it()函数了, 只能等待,等到锁被释放之后,其他线程获得了这个锁才能修改。注意:由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以不会造成修改的冲突,创建一把锁,就是要通过threading.lock()来实现,如下所示:

import time, threading

#银行存款
balance = 0

#锁
lock = threading.Lock()

def change_it(n):
  #先存后取,结果应该为0:
  global balance
  balance = balance + n
  balance = balance - n

def run_thread(n):
  for i in range(10000000):
    #首先要获取锁:
    lock.acquire()
    try:
      # 一旦获取锁,就可以放心change其中的balance了
      change_it(n)
    finally:
      # 最终一定要释放锁
      lock.release()

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)

  如上所示,我们可以看到: 引入了threading之后,通过Lock()函数获取到lock,然后在run_thread函数中线通过lock.acquire()获取到锁,接着就可以change了,最后改变了之后再释放就可以了。 这样,无论我们执行的时间多长,在lock的保护下都不会出现问题。但其实也是存在问题的,锁的存在,就不能保证很好的并发执行了,并发程度不够高,并且如果存在多个锁,也会出现一些问题。

  

  在python中,我们还可以通过multiprocessing.cpu_count()获取到cpu核心数,然后我们可以根据核心数写上对应的几个死循环线程,观察是否会导致所有的cpu内存占用率都达到100%,如下所示: 

import multiprocessing, threading

def loop():
  x = 2
  while True:
    x = x * x
    print(x)

for i in range(multiprocessing.cpu_count()):
  t = threading.Thread(target=loop)
  t.start()

  但实际上占用的cpu并不是很多,也就在60%左右。而如果是c、c++、java等,会有多少核就使用多少,但为什么python不是呢?这是因为python在解释执行时,有一个GIL锁,这个锁导致了即使电脑有100个核,也只能用到一个核,所以,在python中,可以使用多线程,但是不要指望有效利用多核,如果一定要使用多核,就需要使用c扩展,但这样也就使得python失去了其自身优点。  

 

    

 

进程 vs 线程

  在实现多任务时,我们通常会设计Master-Worker模式,Master负责分配任务,而Worker负责执行任务,因此,在一般环境下,都是有一个Master和若干个Worker。 

  • 对于多进程实现Master-Worker,那么主进程就是Master,其他进程为Worker; 对于多线程实现的Master-Worker,那么主线程是Master,其他线程为Worker。
  • 多进程的好处在于一个进程挂了,不会影响其他的进程;而多线程中因为线程是共享同样的变量的,所以一般一个线程挂了,就会导致所属的这个进程全部崩溃。
  • 多线程一般比多进程要快一些,因为多线程在切换上的开销比较小。

  但是,无论是多进程还是多线程,只要,只要数量一多,效率肯定是上不去的!因为切换任务也是需要时间成本的,数量越多,切换的时间也就越多,效率就自然降低了。 

  那什么是计算密集型、什么又是IO密集型呢? 

  计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写

   IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少。因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

 

 

正则表达式

  JavaScript、java等高级语言都是有正则表达式的,这是用于处理字符串的利器。  

  比如 \d 匹配数字、\w匹配一个字母、.可以匹配任意字符、*可以匹配任意数量的字符、+表示匹配至少一个字符、?表示匹配0或1个字符、{n}表示n个字符、{n, m}表示n到m个字符。另外,对于特殊字符,我们可以使用 \ 进行转义;[0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线,[]就是匹配任意一个的意思;A|B表示匹配A或者B;^表示匹配行的开头; $表示匹配行的结尾。

       上面回顾了通用的基础知识,而如果要在python中使用正则表达式,我们需要用到python中的re模块。另外,因为普通的字符串也是需要使用\作为转义的,所以,我们建议在正则字符串前添加 r。

  math用于匹配后返回一个对象,如果没有匹配成功则返回None。

  更多可以查看官网。

 

 

 

python常见模块

  python很大的一个优点就是自带了很多有用的模块方便开发者使用,而无需额外的配置。

  

 

 

 

 ,

 

 

 

 

 

 

 

 

 

 

 

 

 

   
  

 

      

 

 

 

 

 

 

 

参考文章: 廖雪峰博客

posted @ 2018-03-24 18:21  Wayne-Zhu  阅读(515)  评论(0编辑  收藏  举报

一分耕耘,一分收获。