python学习笔记

我在学习完C++后开始学习的python,因此本文主要记录了在学习python时的笔记,以及python语法与c++的一些区别。

数据类型

  1. 字符串可以用''或者""引起来,当使用三个单引号/双引号时,字符串可以换行。
  2. r''表示’’内部的字符串默认不转义.
  3. None表示空值
  4. and表示&& ,or表示|| , not 表示!
  5. 变量可以是任何数据类型,使用时不需要先声明。=可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。这种变量本身类型不固定的语言称之为动态语言
  6. 在Python中,通常用全部大写的变量名表示常量,如PI = 3.14159265359。但事实上它仍然是一个变量,Python根本没有任何机制保证其不会被改变。
  7. /除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数。//称为地板除,是两个整数的除法仍然是整数。
  8. python中,字符串是以Unicode编码的,b'abc'表示bytes类型的数据,ord(str)将获取字符的整数表示,char(num)把编码转换为对应的字符.字符串.encode('类型')用于编码,字符串.decode('类型')用于解码
  9. 字符格式化:%d 整数 %f 浮点数 %s 字符串 %x 十六进制整数,还可以指定是否补0和整数与小数的位数:print('%2d-%05d' % (3, 1))->3-00001,print('%.2f' % 3.1415926)->3.14,
  10. %%为转义%
  11. 使用format也可以格式化。'Hello, {0}, 成绩提升了 {1:.1f}%'.format('小明', 17.125)
  12. set是一组没有重复key的集合,不存储value,不可以放入可变对象。要创建一个set,需要提供一个list作为输入集合.通过add(key)添加元素,remove(key)删除元素。s1 & s2做交集,s1 | s2做并集
  13. 对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回。

    1
    2
    3
    4
    5
    6
    >>> a = 'abc'
    >>> b = a.replace('a', 'A')
    >>> b
    'Abc'
    >>> a
    'abc'
  14. [l.lower() for l in L1 if isinstance(l ,str)] [m + n for m in 'ABC' for n in 'XYZ']列表生成式。前面写要生成的元素,后面写条件,中的的循环可以有多个。

  15. 生成器的两种方法:(1)按列表生成式的方法,把中括号换为小括号。(2)函数中使用yield,此时要注意,函数在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。此外,但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中:
    1
    2
    3
    4
    5
    6
    7
    8
    g = fib(6)
    while True:
    try:
    x = next(g)
    print('g:', x)
    except StopIteration as e:
    print('Generator return value:', e.value)
    break

语句

  1. if语句

    1
    2
    3
    4
    5
    6
    7
    8
    if <条件判断1>:
    <执行1>
    elif <条件判断2>:
    <执行2>
    elif <条件判断3>:
    <执行3>
    else:
    <执行4>
  2. 循环语句(只有for语句和while语句)

    1
    2
    (1) for x in ...:
    (2) while ...:
  3. 默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()

  4. 可以通过collections模块的Iterable类型判断对象是否可迭代(是否是可迭代对象):

    1
    2
    from collections import Iterable
    isinstance('abc', Iterable) # str是否可迭代

    可以使用isinstance()判断一个对象是否是Iterator(迭代器)对象(可以被next()函数调用并不断返回下一个值的对象):

    1
    2
    from collections import Iterator
    isinstance((x for x in range(10)), Iterator)

    生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。把list、dict、str等Iterable变成Iterator可以使用iter()函数

  5. 可以内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身。
    1
    2
    for i, value in enumerate(['A', 'B', 'C']):
    print(i, value)

函数

  1. 函数名其实就是指向一个函数对象的引用,即函数名是一个变量!完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”。f=abs。也可以abs=10,注意,由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10
  2. 如果想定义一个什么事也不做的空函数,可以用pass语句:

    1
    2
    def nop():
    pass
  3. 数据类型检查可以用内置函数isinstance()实现:

    1
    2
    3
    def my_abs(x):
    if not isinstance(x, (int, float)):
    raise TypeError('bad operand type')
  4. 可以返回多个值!但事实上,返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

    1
    2
    3
    >>> x, y = move(100, 100, 60, math.pi / 6)
    >>> print(x, y)
    151.96152422706632 70.0
  5. def power(x, n=2)::默认参数,!!默认参数必须指向不变对象!!调用时如果不按顺序提供部分默认参数时,需要把参数名写上。
    def calc(*numbers)::可变参数,此时,参数numbers接收到的是一个tuple。如果实参为list或tuple,可以在list或tuple实参前加*,把它变为可变参数传进去。
    def person(name, age, **kw):: 关键字参数,允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。可以扩展函数的功能。
    def person(name, age, *, city, job)::命名关键字参数,用分隔,调用方式同上,但此时被限制了关键字参数的名字。命名关键字参数可以有缺省值。如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符了:def person(name, age, *args, city, job):
    参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

  6. 解决递归调用栈溢出的方法是通过尾递归优化,尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
  7. 一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
  8. map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回
  9. reduce把一个函数作用在一个序列[x1, x2, x3, ...]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4),要导入包from functools import reduce
  10. filter()接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。
  11. sorted()函数也是一个高阶函数,它可以接收一个key函数来实现自定义的排序,可以接收reverse=True进行反向排序.sorted([36, 5, -12, 9, -21], key=abs,reverse=True)
  12. 函数可以作为返回值
  13. 内部函数可以引用外部函数的参数和局部变量,当外部函回函数内部函数时,相关参数和变量都保存在返回的函数中,这种称为“闭包(Closure)”的程序结构拥有极大的威力。返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量。
  14. 关键字lambda表示匿名函数,冒号前面的x表示函数参数。匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果。
  15. 函数对象有一个name属性,可以拿到函数的名字
  16. 在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)

    1
    2
    3
    4
    5
    6
    7
    import functools
    def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
    print('call %s():' % func.__name__)
    return func(*args, **kw)
    return wrapper
    1
    2
    3
    4
    5
    6
    7
    8
    9
    import functools
    def log(text):
    def decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
    print('%s %s():' % (text, func.__name__))
    return func(*args, **kw)
    return wrapper
    return decorator
  17. functools.partial可以帮助我们创建偏函数。int2 = functools.partial(int, base=2)max2 = functools.partial(max, 10)

模块

  1. 每一个包目录下面都会有一个init.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。init.py可以是空文件,也可以有Python代码,因为init.py本身就是一个模块,而它的模块名就是mycompany。
  2. 当我们在命令行运行一个模块文件时,Python解释器把一个特殊变量name置为main,而如果在其他地方导入该模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
  3. 正常的函数和变量名是公开的(public),可以被直接引用,比如:abc,x123,PI等。
    类似xxx这样的变量是特殊变量,可以被直接引用,但是有特殊用途。
    类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用(不是不能!)。
  4. import xxx 是从 Python 系统库,或者项目的最外层导入 xxx。
    from . import xxx 是从同一文件夹下导入 xxx。
    from .yyy import xxx 是从当前目录的子文件夹 yyy 中导入 xxx。
    此外,
    import Module #引入模块
    from Module import Other #引入模块中的类、函数或变量
    from Module import * #引入模块中的所有公开成员
  5. 默认情况下,Python解释器会搜索当前目录、所有已安装的内置模块和第三方模块,搜索路径存放在sys模块的path变量中。

面向对象编程

  1. 创建类:class Student(object):
  2. 可以自由地给一个实例变量绑定任何类型的属性。bart.name='jerry',在init中绑定的属性则一定要有。也可以给实例绑定方法s.set_age = MethodType(set_age, s)
    可以使用__slots__变量来限制实例能添加的属性:__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的,除非在子类中也定义slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots

    1
    2
    class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
  3. 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

  4. 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线,在Python中,实例的变量名如果以开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问。因为此时它已经改名为_类名__属性名
  5. 动态语言的“鸭子类型”决定了它并不要求严格的继承体系。
  6. type()可以用来判断对象类型。 type(123)==int True type(fn)==types.FunctionType True
    isinstance也可用来判断 isinstance([1, 2, 3], (list, tuple)) 判断是否是其中一种
    dir()可以获得一个对象的所有属性和方法。
  7. 我们自己写的类,如果也想用len(myObj)的话,就自己写一个len()方法;
    getattr()、setattr()以及hasattr(),我们可以直接操作一个对象的状态。
  8. 可以直接在class中定义属性,这种属性是类属性,归Student类所有。相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

    1
    2
    class Student(object):
    name = 'Student'

    类方法内调用时,可以使用类名.变量名调用。

  9. Python内置的@property装饰器就是负责把一个方法变成属性调用的。
    把一个getter方法变成属性,只需要加上@property@property本身又创建了另一个装饰器`@score.setter`,负责把一个setter方法变成属性赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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

    #调用时:
    s.score
    s.score=10
  10. python可以实现多重继承,为了看清关系,建议将除主继承以外的继承的类命名结尾为MixIn。MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

  11. __str__()用于print()类对象时,返回用户看到的字符串;__repr__()用于直接调用类对象时,为调试服务的;因此,可以在定义了一个后,直接__repr__ = __str__
  12. 如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个iter()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的next()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Fib(object):
    def __init__(self):
    self.a, self.b = 0, 1 # 初始化两个计数器a,b

    def __iter__(self):
    return self # 实例本身就是迭代对象,故返回自己

    def __next__(self):
    self.a, self.b = self.b, self.a + self.b # 计算下一个值
    if self.a > 100000: # 退出循环的条件
    raise StopIteration()
    return self.a # 返回下一个值
  13. __getitem__可以将类实例表现的像list那样按照下标取出元素。注意!!__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断。

    1
    2
    3
    4
    5
    6
    class Fib(object):
    def __getitem__(self, n):
    if isinstance(n, int): # n是索引
    pass
    if isinstance(n, slice): # n是切片
    pass

    与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。

  14. __getattr__()方法可以动态返回一个属性(此时调用方式为s.age)。也可以返回一个函数(此时调用方式为s.age())。
    注意,只有在没有找到属性的情况下,才调用getattr,已有的属性不会在getattr中查找。

    1
    2
    3
    def __getattr__(self, attr):
    if attr=='score':
    return 99

    如果要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误。

  15. __call__()方法,就可以直接对实例进行调用。此外,他也可以定义参数,对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

    1
    2
    s = Student('Michael')
    s() # self参数不要传入

    需要判断一个对象是否能被调用时,可以通过callable()函数判断一个对象是否是“可调用”对象。

    1
    callable(Student())

    一个经典的定制类的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Chain():
    def __init__(self,path=''):
    self._path = path
    def __getattr__(self, item):
    return Chain('%s/%s' % (self._path,item))
    def __str__(self):
    return self._path
    __repr__ = __str__
    __call__ = __getattr__

    print ( Chain().users('michael').repos )

    #分析:
    Chain()先调用了init;
    然后调用getattr,此时相当于生成了一个新的Chain的实例,然后通过call,把实例当成一个函数进行调用;
    接着再调用getattr;
    最后调用str,输出当前的路径。
    ------
    换句话说,. 就相当于调用 __getattr__,(...) 就相当于调用 __call__。
  16. 定义枚举类实例:

    1
    2
    from enum import Enum
    Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

    调用枚举类成员Month.Jan,枚举所有成员:

    1
    2
    3
    4
    for name, member in Month.__members__.items():
    print(name, '=>', member, ',', member.value)

    #value属性则是自动赋给成员的int常量,默认从1开始计数。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    from enum import Enum, unique

    @unique #帮助我们检查保证没有重复值
    class Weekday(Enum):
    Sun = 0 # Sun的value被设定为0
    Mon = 1
    Tue = 2
    Wed = 3
    Thu = 4
    Fri = 5
    Sat = 6

    调用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    >>> 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
  17. class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。要创建一个class对象,type()函数依次传入3个参数:
    class的名称继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上

    1
    2
    3
    def fn(self, name='world'): # 先定义函数
    print('Hello, %s.' % name)
    Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class
  18. 要控制类的创建行为,还可以使用metaclass(元类)。可以理解为:先定义metaclass,就可以创建类,最后创建实例。

    1
    2
    3
    4
    5
    # metaclass是类的模板,所以必须从`type`类型派生:
    class ListMetaclass(type):
    def __new__(cls, name, bases, attrs):
    attrs['add'] = lambda self, value: self.append(value)
    return type.__new__(cls, name, bases, attrs)
    1
    2
    3
    #创建类
    class MyList(list, metaclass=ListMetaclass):
    pass

    当我们传入关键字参数metaclass时,魔术就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.new()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后,返回修改后的定义。new()方法接收到的参数依次是:
    当前准备创建的类的对象类的名字类继承的父类集合类的方法集合

错误、调试、测试

  1. 当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try:
    print('try...')
    r = 10 / int('2')
    print('result:', r)
    except ValueError as e:
    print('ValueError:', e)
    except ZeroDivisionError as e:
    print('ZeroDivisionError:', e)
    else:
    print('no error!')
    finally:
    print('finally...')
    print('END')

    Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。常见的错误类型和继承关系
    不需要在每个可能出错的地方去捕获错误,只要在合适的层次去捕获错误就可以了。这样一来,就大大减少了写try…except…finally的麻烦。

  2. Python内置的logging模块可以非常容易地记录错误信息,同时,让程序继续执行下去。通过配置,logging还可以把错误记录到日志文件里,方便事后排查。

    1
    2
    3
    4
    5
    6
    7
    import logging

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

    logging允许你指定记录信息的级别,有debug,info,warning,error等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debug和info就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

    1
    2
    3
    4
    5
    6
    import logging
    logging.basicConfig(level=logging.INFO)
    s = '0'
    n = int(s)
    logging.info('n = %d' % n)
    print(10 / n)
  3. 用raise语句抛出一个错误的实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class FooError(ValueError):
    pass

    def foo(s):
    n = int(s)
    if n==0:
    raise FooError('invalid value: %s' % s)
    return 10 / n

    foo('0')
  4. 断言:凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:assert n != 0, 'n is zero!'。启动Python解释器时可以用-O参数来关闭assert。

  5. python -m pdb 文件名.py启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。
    输入命令l来查看代码。
    输入命令n可以单步执行代码
    输入命令p 变量名来查看变量
    输入命令q结束调试
    此外,可以在程序中使用pdb.set_trace()(要导入包import pdb)来使程序会自动在pdb.set_trace()暂停并进入pdb调试环境,
    命令p 变量名查看变量
    命令c继续运行
  6. 编写单元测试时,我们需要编写一个测试类,从unittest.TestCase继承。以test开头的方法就是测试方法,不以test开头的方法不被认为是测试方法,测试的时候不会被执行。
    self.assertEqual(abs(-1), 1):断言函数返回的结果与1相等
    with self.assertRaises(KeyError): value = d['empty']:访问不存在的key时,断言会抛出KeyError
    一旦编写好单元测试,我们就可以运行单元测试。最简单的运行方式是在文件名.py的最后加上两行代码。然后直接执行python 文件名.py;另一种方法是在命令行通过参数-m unittest直接运行单元测试

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

    可以在单元测试中编写两个特殊的setUp()和tearDown()方法。这两个方法会分别在每调用一个测试方法的前后分别被执行。

  7. 可以编写文档测试,这些代码与其他说明可以写在注释中,然后,由一些工具来自动生成文档。既然这些代码本身就可以粘贴出来直接运行。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    def fact(n):
    '''
    Calculate 1*2*...*n
    >>> fact(1)
    1
    >>> fact(10)
    3628800
    >>> fact(-1)
    Traceback (most recent call last):
    ...
    ValueError
    '''
    if n < 1:
    raise ValueError()
    if n == 1:
    return 1
    return n * fact(n - 1)

    if __name__ == '__main__':
    import doctest
    doctest.testmod()

    注意到最后3行代码。当模块正常导入时,doctest不会被执行。只有在命令行直接运行时,才执行doctest。所以,不必担心doctest会在非测试环境下执行。

IO处理

  1. f = open('/Users/michael/test.txt', 'r',【encoding='gbk', errors='ignore'】)打开文件,如果文件不存在,open()函数就会抛出一个IOError的错误,并且给出错误码和详细的信息告诉你文件不存在。
    其中,r表示读,rb表示按二进制读。encoding参数表示编码类型,errors参数表示如果遇到编码错误后如何处理。最简单的方式是直接忽略
    w表示写,wb表示按二进制写,如果文件已存在,会直接覆盖(相当于删掉后新写入一个文件)
    a以追加模式写入。
    f.read()可以一次读取文件的全部内容,Python把内容读到内存,用一个str对象表示;read(size)方法,每次最多读取size个字节的内容。readline()可以每次读取一行内容,调用readlines()一次读取所有内容并按行返回list
    f.write('Hello, world!'),通过反复调用write()来写入文件。
    f.close()关闭文件。
    可以使用with语句简化书写。(只能在支持了上下文管理器的对象使用,会自动调用close语句)

    1
    2
    3
    with open('/path/to/file', 'r') as f:
    for line in f.readlines():
    print(line.strip()) # 把末尾的'\n'删掉
  2. 要把str写入StringIO,我们需要先创建一个StringIO,然后,f.write('hello')像文件一样写入即可。使用f.getvalue()获得写入后的str.

    1
    2
    3
    4
    from io import StringIO
    f = StringIO()
    f.write('world!')
    f.getvalue()

    如果要操作二进制数据,就需要使用BytesIO.函数与StringIO相同。

  3. os模块下的基本功能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import os
    os.name #操作系统类型
    os.uname() #详细的系统信息(windows没有)
    os.environ #环境变量
    os.environ.get('key') #获取某个环境变量的值
    os.path.abspath('.') #查看当前目录的绝对路径
    os.mkdir('/Users/michael/testdir') #创建一个目录
    os.rmdir('/Users/michael/testdir') # 删掉一个目录
    os.path.join('/Users/michael', 'testdir') #把两个路径合成一个时,要通过该函数,不要求目录和文件要真实存在
    os.path.split('/Users/michael/testdir/file.txt') #拆分路径,不要求目录和文件要真实存在
    os.path.splitext('/path/to/file.txt') #直接让你得到文件扩展名,不要求目录和文件要真实存在
    os.rename('test.txt', 'test.py') # 对文件重命名
    os.remove('test.py') # 删掉文件

    shutil模块可以作为os模块的补充:

    1
    copyfile()  #复制文件

    我们可以利用Python的特性来过滤文件,例如:

    1
    2
    [x for x in os.listdir('.') if os.path.isdir(x)]  #列出当前目录下的所有目录
    [x for x in os.listdir('.') if os.path.isfile(x) and os.path.splitext(x)[1]=='.py'] #列出所有的.py文件
  4. 变量从内存中变成可存储或传输的过程称之为序列化.在Python中叫pickling.反过来,把变量内容从序列化的对象重新读到内存里称之为反序列化,即unpickling。Python提供了pickle模块来实现序列化。
    pickle.dumps()方法把任意对象序列化成一个bytes;
    pickle.dump()直接把对象序列化后写入一个file-like Object;
    pickle.loads()方法反序列化出对象;
    pickle.load()方法从一个file-like Object中直接反序列化出对象.
    注意:Pickle的问题和所有其他编程语言特有的序列化问题一样,就是它只能用于Python,并且可能不同版本的Python彼此都不兼容,因此,只能用Pickle保存那些不重要的数据,不能成功地反序列化也没关系。

  5. Python内置的json模块提供了非常完善的Python对象到JSON格式的转换。函数与上述相同。
    如果需要定制JSON序列化,则传参时,default参数填写对应的转换函数。

    1
    2
    3
    4
    5
    6
    7
    def student2dict(std):
    return {
    'name': std.name,
    'age': std.age,
    'score': std.score
    }
    json.dumps(s, default=student2dict)

    如果偷懒,可以使用json.dumps(s, default=lambda obj: obj.__dict__),因为通常class的实例都有一个dict属性,它就是一个dict,用来存储实例变量。也有少数例外,比如定义了slots的class。
    同理,反序列化,也可以使用这种方法,同时传参时,object_hook参数填写对应的函数。

    1
    2
    3
    def dict2student(d):
    return Student(d['name'], d['age'], d['score'])
    json.loads(json_str, object_hook=dict2student)

    此外,在出现中文时,ensure_ascii参数可以确定中文是否需要转换为ASCii码。

正则表达式

【注意】如果没有标明字符长度,只要从头开始完全匹配即可,后面是否匹配都算匹配。re.match('abc\d','abc22345ad')是匹配的。

  1. 精确匹配

    1
    2
    3
    4
    5
    \d          匹配数字
    \w 匹配一个字母、数字
    \s 匹配空格(也包括Tab等空白符)
    . 匹配任意字符
    \转义字符 匹配转义字符,如 - _
  2. 变长字符

    1
    2
    3
    4
    5
    * 表示任意个字符(包括0个)
    + 表示至少一个字符
    ? 表示0个或1个字符
    {n} 表示n个字符,
    {n,m} 表示n-m个字符
  3. 范围。可以用[]表示范围,比如:

    1
    2
    3
    4
    [0-9a-zA-Z\_]       可以匹配一个数字、字母或者下划线
    [0-9a-zA-Z\_]+ 可以匹配至少由一个数字、字母或者下划线组成的字符串
    [a-zA-Z\_][0-9a-zA-Z\_]* 可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量
    [a-zA-Z\_][0-9a-zA-Z\_]{0, 19} 更精确地限制了变量的长度是1-20个字符

    python|java可以匹配python或java
    ^表示行的开头,^\d表示必须以数字开头。
    $表示行的结束,\d$表示必须以数字结束。

  4. Python提供re模块,包含所有正则表达式的功能。
    由于Python的字符串本身也用\转义,所以要特别注意,建议使用r前缀,这样就不需要考虑转义了。
    match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。如:re.match(r'^\d{3}\-\d{3,8}$', '010-12345')
    split()用来切分字符串。 如re.split(r'[\s\,]+', 'a,b, c d')
    group()用来提取分组,group(0)表示原始字符串,之后表示第n个子串。groups()表示分组后的元组。re.match(r'^(\d{3})-(\d{3,8})$', '010-12345') ==> m.group(1)
    compile()用于预编译正则表达式,为了效率。
    1
    2
    re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
    re_telephone.match('010-12345').groups()

内建模块

datetime模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from datetime import datetime   #导入datetime类

now = datetime.now() #获取当前时间,返回类型是datetime
dt = datetime(2015, 4, 19, 12, 20) # 用指定日期时间创建datetime

dt.timestamp() # 把datetime转换为timestamp,是一个浮点数,小数位表示毫秒数
datetime.fromtimestamp(t) #把timestamp转换为本地时间的datetime
datetime.utcfromtimestamp(t) #把timestamp转换为UTC时间的datetime

datetime.strptime('2015-6-1 18:19:59', '%Y-%m-%d %H:%M:%S') #把str转换为datetime
datetime.now().strftime('%a, %b %d %H:%M') #时间转换为str

now + timedelta(days=2, hours=12) #时间的加减

now.replace(tzinfo=timezone(timedelta(hours=8))) # 强制设置为UTC+8:00
utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc) #拿到UTC时间,并强制设置时区为UTC+0:00
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8))) #astimezone()将转换时区为北京时间:

注意:datetime表示的时间需要时区信息才能确定一个特定的时间,否则只能视为本地时间。

collections模块

namedtuple可以很方便地定义一种数据类型,它具备tuple的不变性,又可以根据属性来引用。创建对象是tuple的一种子类

1
2
Point = namedtuple('Point', ['x', 'y'])  #创建一个自定义的tuple对象并且规定了tuple元素的个数,
p = Point(1, 2) #可以用属性而不是索引来引用tuple的某个元素 p.x

deque可以高效实现插入和删除操作的双向列表,适合用于队列和栈。支持append()、pop()、appendleft()、popleft(),可以非常高效地往头部添加或删除元素。

1
2
3
4
from collections import deque
q = deque(['a', 'b', 'c'])
q.append('x')
q.appendleft('y')

使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict,其他行为与dict完全一样。注意默认值是调用函数返回的,而函数在创建defaultdict对象时传入。

1
2
from collections import defaultdict
dd = defaultdict(lambda: 'N/A')

使用dict时,Key是无序的。在对dict做迭代时,我们无法确定Key的顺序。如果要保持Key按插入的顺序,可以用OrderedDict

1
2
od = OrderedDict([('a', 1), ('b', 2), ('c', 3)])
list(od.keys()) # 按照插入的Key的顺序返回

Counter是一个简单的计数器,实际上也是dict的一个子类。例如,统计字符出现的个数。

1
2
3
4
from collections import Counter
c = Counter()
for ch in 'programming':
... c[ch] = c[ch] + 1

base64模块

Base64是一种用64个字符 {‘A’, ‘B’, ‘C’, … ‘a’, ‘b’, ‘c’, … ‘0’, ‘1’, … ‘+’, ‘/‘} 来表示任意二进制数据的方法。由于在URL中+和/不能直接作为参数,所以又有一种”url safe”的base64编码,其实就是把字符+和/分别变成-和_:
它的原理是:每3个字节一组,按照6bit划分为4组。如果编码不是3的倍数,就用\x00字节补足,并添加对应数量的=号。由于=字符也可能出现在Base64编码中,但=用在URL、Cookie里面会造成歧义,所以,很多Base64编码后会把=去掉.
Base64是一种通过查表的编码方法,不能用于加密,即使使用自定义的编码表也不行。Base64适用于小段内容的编码,比如数字证书签名、Cookie的内容等。

1
2
3
4
5
import base64
base64.b64encode(b'binary\x00string')
base64.b64decode(b'YmluYXJ5AHN0cmluZw==')
base64.urlsafe_b64encode(b'i\xb7\x1d\xfb\xef\xff')
base64.urlsafe_b64decode('abcd--__')

struct模块

解决bytes和其他二进制数据类型的转换。参数说明

1
2
3
import struct
struct.pack('>I', 10240099) #转换为二进制,>表示字节顺序是big-endian,也就是网络序,I表示4字节无符号整数
struct.unpack('>IH', b'\xf0\xf0\xf0\xf0\x80\x80') #后面的bytes依次变为I:4字节无符号整数和H:2字节无符号整数

hashlib模块

1
2
3
4
5
6
7
8
9
10
11
12
13
import hashlib

#MD5
md5 = hashlib.md5()
md5.update('how to use md5 in '.encode('utf-8'))
md5.update('python hashlib?'.encode('utf-8'))
print(md5.hexdigest())

#SHA1
sha1 = hashlib.sha1()
sha1.update('how to use sha1 in '.encode('utf-8'))
sha1.update('python hashlib?'.encode('utf-8'))
print(sha1.hexdigest())

hmac模块

Hmac算法通过一个标准算法,在计算哈希的过程中,把key混入计算过程中。要注意传入的key和message都是bytes类型,str类型需要首先编码为bytes

1
2
3
4
5
6
import hmac
message = b'Hello, world!'
key = b'secret'
h = hmac.new(key, message, digestmod='MD5')
# 如果消息很长,可以多次调用h.update(msg)
h.hexdigest()

itertools模块

itertools提供了非常有用的用于操作迭代对象的函数。

1
2
3
4
5
6
7
8
9
natuals = itertools.count(1,2)  #count()会创建一个无限的迭代器,从1开始,间隔为2
cs = itertools.cycle('ABC') #cycle()会把传入的一个序列无限重复下去
ns = itertools.repeat('A', 3) #repeat()负责把一个元素无限重复下去,不过如果提供第二个参数就可以限定重复次数

无限序列在for迭代时才会无限地迭代下去;但通过takewhile()等函数根据条件判断来截取出一个有限的序列
ns = itertools.takewhile(lambda x: x <= 10, natuals)

itertools.chain('ABC', 'XYZ') #把一组迭代对象串联起来,形成一个更大的迭代器
itertools.groupby('AaaBBbcCAAa', lambda c: c.upper()) #把迭代器中相邻的重复元素挑出来放在一起

contexlib模块

任何对象,只要正确实现了上下文管理,就可以用于with语句。
实现上下文管理是通过__enter____exit__这两个方法实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#正常写法:
class Query(object):

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

def __enter__(self):
print('Begin')
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print('Error')
else:
print('End')

def query(self):
print('Query info about %s...' % self.name)

with Query('Bob') as q:
q.query()

#使用装饰器
from contextlib import contextmanager

class Query(object):

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

def query(self):
print('Query info about %s...' % self.name)

@contextmanager
def create_query(name):
print('Begin')
q = Query(name)
yield q #用yield语句把with ... as var把变量输出出去,然后,with语句就可以正常地工作了
print('End')

with create_query('Bob') as q:
q.query()

1
2
3
4
5
6
7
8
9
@contextmanager
def tag(name):
print("<%s>" % name)
yield
print("</%s>" % name)

with tag("h1"):
print("hello")
print("world")

如果一个对象没有实现上下文,我们就不能把它用于with语句。这个时候,可以用closing()来把该对象变为上下文对象。
closing也是一个经过@contextmanager装饰的generator

1
2
3
4
5
6
from contextlib import closing
from urllib.request import urlopen

with closing(urlopen('https://www.python.org')) as page:
for line in page:
print(line)

urllib模块

urllib的request模块可以非常方便地抓取URL内容,也就是发送一个GET请求到指定的页面,然后返回HTTP的响应

1
2
3
4
5
6
7
8
from urllib import request

with request.urlopen('http://madj.xin') as f:
data = f.read()
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', data.decode('utf-8'))

如果我们要想模拟浏览器发送GET请求,就需要使用Request对象,通过往Request对象添加HTTP头,我们就可以把请求伪装成浏览器。

1
2
3
4
5
6
7
req = request.Request('http://www.douban.com/')
req.add_header('User-Agent', 'Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 Mobile/10A5376e Safari/8536.25')
with request.urlopen(req) as f:
print('Status:', f.status, f.reason)
for k, v in f.getheaders():
print('%s: %s' % (k, v))
print('Data:', f.read().decode('utf-8'))

如果要以POST发送一个请求,只需要把参数data以bytes形式传入。

1
2
3
4
5
6
7
8
9
10
11
12
login_data = parse.urlencode([
('username', email),
('password', passwd),
('entry', 'mweibo'),
('client_id', ''),
('savestate', '1'),
('ec', ''),
('pagerefer', 'https://passport.weibo.cn/signin/welcome?entry=mweibo&r=http%3A%2F%2Fm.weibo.cn%2F')
])

with request.urlopen(req, data=login_data.encode('utf-8')) as f:
.......

如果还需要更复杂的控制,比如通过一个Proxy去访问网站,我们需要利用ProxyHandler来处理,示例代码如下:

1
2
3
4
5
6
proxy_handler = urllib.request.ProxyHandler({'http': 'http://www.example.com:3128/'})
proxy_auth_handler = urllib.request.ProxyBasicAuthHandler()
proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
opener = urllib.request.build_opener(proxy_handler, proxy_auth_handler)
with opener.open('http://www.example.com/login.html') as f:
pass

XML模块

操作XML有两种方法:DOM和SAX。DOM会把整个XML读入内存,解析为树,因此占用内存大,解析慢,优点是可以任意遍历树的节点。SAX是流模式,边读边解析,占用内存小,解析快,缺点是我们需要自己处理事件。
正常情况下,优先考虑SAX,因为DOM实在太占内存。
在Python中使用SAX解析XML非常简洁,通常我们关心的事件是start_elementend_elementchar_data,准备好这3个函数,然后就可以解析xml了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from xml.parsers.expat import ParserCreate

class DefaultSaxHandler(object):
def start_element(self, name, attrs):
print('sax:start_element: %s, attrs: %s' % (name, str(attrs)))

def end_element(self, name):
print('sax:end_element: %s' % name)

def char_data(self, text):
print('sax:char_data: %s' % text)

xml = r'''<?xml version="1.0"?>
<ol>
<li><a href="/python">Python</a></li>
<li><a href="/ruby">Ruby</a></li>
</ol>
'''

handler = DefaultSaxHandler()
parser = ParserCreate()
parser.StartElementHandler = handler.start_element
parser.EndElementHandler = handler.end_element
parser.CharacterDataHandler = handler.char_data
parser.Parse(xml)

最有效的生成XML的方法是拼接字符串

1
2
3
4
5
6
L = []
L.append(r'<?xml version="1.0"?>')
L.append(r'<root>')
L.append(encode('some & data'))
L.append(r'</root>')
return ''.join(L)

HTMLParser模块

HTML本质上是XML的子集,但是HTML的语法没有XML那么严格,所以不能用标准的DOM或SAX来解析HTML。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from html.parser import HTMLParser
from html.entities import name2codepoint

class MyHTMLParser(HTMLParser):

def handle_starttag(self, tag, attrs):
print('<%s>' % tag)

def handle_endtag(self, tag):
print('</%s>' % tag)

def handle_startendtag(self, tag, attrs):
print('<%s/>' % tag)

def handle_data(self, data):
print(data)

def handle_comment(self, data):
print('<!--', data, '-->')

def handle_entityref(self, name):
print('&%s;' % name)

def handle_charref(self, name):
print('&#%s;' % name)

parser = MyHTMLParser()
parser.feed('''<html>
<head></head>
<body>
<!-- test html parser -->
<p>Some <a href=\"#\">html</a> HTML&nbsp;tutorial...<br>END</p>
</body></html>''')

feed()方法可以多次调用,也就是不一定一次把整个HTML字符串都塞进去,可以一部分一部分塞进去。特殊字符有两种,一种是英文表示的 ,一种是数字表示的Ӓ,这两种字符都可以通过Parser解析出来。

常用第三方模块

Pillow模块

图像处理库。
缩放图像:

1
2
3
4
5
6
7
8
9
10
11
12
from PIL import Image

# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('test.jpg')
# 获得图像尺寸:
w, h = im.size
print('Original image size: %sx%s' % (w, h))
# 缩放到50%:
im.thumbnail((w//2, h//2))
print('Resize image to: %sx%s' % (w//2, h//2))
# 把缩放后的图像用jpeg格式保存:
im.save('thumbnail.jpg', 'jpeg')

模糊图像:

1
2
3
4
5
6
7
from PIL import Image, ImageFilter

# 打开一个jpg图像文件,注意是当前路径:
im = Image.open('test.jpg')
# 应用模糊滤镜:
im2 = im.filter(ImageFilter.BLUR)
im2.save('blur.jpg', 'jpeg')

绘制验证码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from PIL import Image, ImageDraw, ImageFont, ImageFilter

import random

# 随机字母:
def rndChar():
return chr(random.randint(65, 90))

# 随机颜色1:
def rndColor():
return (random.randint(64, 255), random.randint(64, 255), random.randint(64, 255))

# 随机颜色2:
def rndColor2():
return (random.randint(32, 127), random.randint(32, 127), random.randint(32, 127))

# 240 x 60:
width = 60 * 4
height = 60
image = Image.new('RGB', (width, height), (255, 255, 255))
# 创建Font对象:
font = ImageFont.truetype('/Library/Fonts/Arial.ttf', 36)
# 创建Draw对象:
draw = ImageDraw.Draw(image)
# 填充每个像素:
for x in range(width):
for y in range(height):
draw.point((x, y), fill=rndColor())
# 输出文字:
for t in range(4):
draw.text((60 * t + 10, 10), rndChar(), font=font, fill=rndColor2())
# 模糊:
image = image.filter(ImageFilter.BLUR)
image.save('code.jpg', 'jpeg')

多进程和线程

  1. Unix/Linux下,os.fork()调用一次,然后,分别在父进程和子进程内返回。
    此外,multiprocessing模块提供了一个Process类来代表一个进程对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    from multiprocessing import Process
    import os

    # 子进程要执行的代码
    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() #join()方法可以等待子进程结束后再继续往下运行
    print('Child process end.')
  2. 如果要启动大量的子进程,可以用进程池的方式批量创建子进程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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(5):
    p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

    对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process了。请注意输出的结果,task 0,1,2,3是立刻执行的,而task 4要等待前面某个task完成后才执行,这是因为Pool的默认大小在我的电脑上是4,因此,最多同时执行4个进程。这是Pool有意设计的限制,并不是操作系统的限制。如果改成:p = Pool(5)就可以同时跑5个进程。

  3. 调用外部进程,可以使用subprocess模块。subprocess.call(['nslookup','www.python.org']),如果子进程还需要输入,则可以通过communicate()方法输入。
  4. Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
  5. 使用threading模块使用线程

    1
    2
    3
    t = threading.Thread(target=loop, name='LoopThread')
    t.start()
    t.join()
  6. 可以使用同步锁:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    balance = 0
    lock = threading.Lock()

    def run_thread(n):
    for i in range(100000):
    # 先要获取锁:
    lock.acquire()
    try:
    # 放心地改吧:
    change_it(n)
    finally:
    # 改完了一定要释放锁:
    lock.release()
  7. 全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import threading

    # 创建全局ThreadLocal对象:
    local_school = threading.local()

    def process_student():
    # 获取当前线程关联的student:
    std = local_school.student
    print('Hello, %s (in %s)' % (std, threading.current_thread().name))

    def process_thread(name):
    # 绑定ThreadLocal的student:
    local_school.student = name
    process_student()

    t1 = threading.Thread(target= process_thread, args=('Alice',), name='Thread-A')
    t2 = threading.Thread(target= process_thread, args=('Bob',), name='Thread-B')
    t1.start()
    t2.start()
    t1.join()
    t2.join()

本文标题:python学习笔记

文章作者:Jerry

发布时间:2018年04月03日 - 19:59:43

最后更新:2019年09月14日 - 23:54:46

原始链接:https://jerryma0912.github.io/2018/04/03/16-python-note/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。