01-日志的全局配置

日志在 Python 中专门有一个库可以使用——logging

import logging

# 控制台输出日志
print("我是日志")

日志有五个级别:

logging.debug("我是debug")
logging.info("我是info")
logging.warning("我是warning")
logging.error("我是error")
logging.critical("我是critical")

out:
WARNING:root:我是warning
ERROR:root:我是error
CRITICAL:root:我是critical

root 是默认日志的名称,root 是一个对象。logging 就是在调用 root。

输出的时候会前面带上日志的级别和对象。

root 是默认日志的名称,只会输出 warning 级别以上的日志。

1.1 配置日志(修改配置项)

配置默认的 root 日志

配置项意义
format格式化输出
level0、10、20、30、40、50
分别对应日志的五个级别。
直接使用常量 logging.DEBUG
Handler指定日志输出位置
datefmt修改日期输出格式
filename日志文件名称
import logging

logging.basicConfig(format="%(name)s - %(asctime)s - %(filename)s - %(lineno)s -%(message)s")


logging.debug("我是debug")
logging.info("我是info")
logging.warning("我是warning")
logging.error("我是error")
logging.critical("我是critical")

out:
root - 2019-08-07 14:26:04,058 - 01-日志的全局配置.py - 7 -我是warning
root - 2019-08-07 14:26:04,059 - 01-日志的全局配置.py - 8 -我是error
root - 2019-08-07 14:26:04,059 - 01-日志的全局配置.py - 9 -我是critical

1.2Formatter 参数

参数含义
%(message)s用户自定义要输出的信息
%(asctime)s当前的日期时间
%(name)slogger 实例的名称
%(module)s使用 logger 实例的模块名
%(filename)s使用 logger 实例的模块的文件名
%(funcName)s使用 logger 实例的函数名
%(lineno)d使用 logger 实例的代码行号
%(levelname)s日志级别名称
%(levelno)s表示日志级别的数字形式
%(threadName)s使用 logger 实例的线程名称(测试多线程时有用)
%(thread)d使用 logger 实例的线程号(测试多线程时有用)
%(process)d使用 logger 实例的进程号(测试多进程时有用)

创建 Formatter:

formatter = logging.Formatter
	('%(asctime)s - %(filename)s[line:%(lineno)d] - <%(threadName)s %(thread)d>' + '- <Process %(process)d> - %(levelname)s: %(message)s')

1.3Handler 参数

1.3.1 常用的 Handler

参数意义
logging.StreamHandler输出到控制台
logging.FileHandler输出到指定的日志文件中
logging.handlers.RotatingFileHandler也是输出到日志文件中,还可以指定日志文件的最大大小和副本数,当日志文件增长到设置的大小 后,会先将原日志文件 test.log 重命名,如 test.log.1,然后再创建一个 test.log 继续写入日志。如 果设置了副本数 N,则最多只能存在 N 个重命名的日志文件
logging.handlers.TimedRotatingFileHandler按日期时间保存日志文件,如果设置了滚动周期,则只存在这个周期内的日志文件。比如,只保留 一周内的日志
logging.handlers.SMTPHandler捕获到指定级别的日志后,给相应的邮箱发送邮件

1.3.2 创建 Handler

# 创建StreamHandler,输出日志到控制台
stream_handler = logging.StreamHandler()

1.3.2Handler 常用方法

设置日志的格式,调用 formatter

stream_handler.setFormatter(formatter)

设置日志级别

stream_handler.setLevel(logging.INFO)

1.4Logger 实例

如前面所述,直接使用 logging.basicConfig() 默认使用 root 这个 logger 实例。

basicConfig 默认输出是 warning 级别的。

我们也可以使用 logging.getLogger()创建一个自定义命令的 logger 实例:

import logging

# 1. 创建一个叫aiotest的logger实例,如果参数为空则返回root
logger = logging.getLogger('aiotest')

# 2. 设置总日志级别, 也可以给不同的handler设置不同的日志级别
logger.setLevel(logging.DEBUG)

# 3. 设置Formatter
formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - <%(threadName)s %(thread)d>' + '- <Process %(process)d> - %(levelname)s: %(message)s')

# 4. 创建Handler
# 文件Handler
file_handler = logging.FileHandler("logger.log", encoding="utf8")
# 控制台输出Handler
stream_handler = logging.StreamHandler()

# 5. 给Handler设置属性
stream_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
stream_handler.setLevel(logging.DEBUG)
file_handler.setLevel(logging.WARNING)

# 6. 将Handler添加到logger实例上
logger.addHandler(stream_handler)
logger.addHandler(file_handler)


# 输出日志
logger.debug("我是debug")
logger.info("我是info")
logger.warning("我是warning")
logger.error("我是error")
logger.critical("我是critical")


out:
2019-08-07 15:22:55,429 - 01-日志的全局配置.py[line:30] - <MainThread 15616>- <Process 15732> - DEBUG: 我是debug
2019-08-07 15:22:55,429 - 01-日志的全局配置.py[line:31] - <MainThread 15616>- <Process 15732> - INFO: 我是info
2019-08-07 15:22:55,430 - 01-日志的全局配置.py[line:32] - <MainThread 15616>- <Process 15732> - WARNING: 我是warning
2019-08-07 15:22:55,430 - 01-日志的全局配置.py[line:33] - <MainThread 15616>- <Process 15732> - ERROR: 我是error
2019-08-07 15:22:55,430 - 01-日志的全局配置.py[line:34] - <MainThread 15616>- <Process 15732> - CRITICAL: 我是critical

日志文件:
2019-08-07 15:23:37,297 - 01-日志的全局配置.py[line:32] - <MainThread 74852>- <Process 74856> - WARNING: 我是warning
2019-08-07 15:23:37,297 - 01-日志的全局配置.py[line:33] - <MainThread 74852>- <Process 74856> - ERROR: 我是error
2019-08-07 15:23:37,298 - 01-日志的全局配置.py[line:34] - <MainThread 74852>- <Process 74856> - CRITICAL: 我是critical

给 logger 设置最低的全局级别,优先级最高,最低的就是 logger.setLevel(logging.DEBUG)的级别,不会低于这个级别。

在其他地方使用自定义 logger,将上面的代码保存为 demo 文件。

from demo import logger

try:
    print(1/0)
except:
    logger.error("错误001")

1.4.1 配合 os 和 time 模块使用:

import os
import time
import logging
# 1. 创建logger实例,如果参数为空则返回 root logger
logger = logging.getLogger('aiotest')

# 设置总日志级别, 也可以给不同的handler设置不同的日志级别
logger.setLevel(logging.DEBUG)

# 2. 创建Handler, 输出日志到控制台和文件
# 控制台日志和日志文件使用同一个Formatter
formatter = logging.Formatter('%(asctime)s - %(filename)s[line:%(lineno)d] - <%(threadName)s %(thread)d>' + '- <Process %(process)d> - %(levelname)s: %(message)s' )

# 日志文件FileHandler
basedir = os.path.abspath(os.path.dirname(__file__))
log_dest = os.path.join(basedir, 'logs')    # 日志文件所在目录
if not os.path.isdir(log_dest):
    os.mkdir(log_dest)

filename = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) + '.log'

# 日志文件名,以当前时间命名
file_handler = logging.FileHandler(os.path.join(log_dest, filename), encoding='utf-8')

file_handler.setFormatter(formatter)
# 设置Formatter
file_handler.setLevel(logging.WARNING)
# 单独设置日志文件的日志级别

# 控制台日志StreamHandler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
# stream_handler.setLevel(logging.INFO)
# 单独设置控制台日志的日志级别,注释掉则使用总日志 级别
# 3. 将handler添加到logger中
logger.addHandler(file_handler)
logger.addHandler(stream_handler)

02-深拷贝和浅拷贝

Python 赋值操作或函数参数传递,传递的永远是对象引用(即内存地址),而不是对象内容。在 Python 中一切皆对象,对象又分为可变(mutable)和不可变(immutable)两种类型。对象拷贝是指在内存中创建新的对象,产生新的内存地址。当顶层对象和它的子元素对象全都是 immutable 不可变对象时,不存在被拷贝,因为没有产生新对象。浅拷贝(Shallow Copy),拷贝顶层对象,但不会拷贝内部的子元素对象。深拷贝(Deep Copy),递归拷贝顶层对象,以及它内部的子元素对象。

可变对象与不可变对象

Python 中一切皆对象,对象就像一个塑料盒子,里面装的是数据。对象有不同类型,例如布尔型和整型,类型决定了可以对它进行的操作。现实生活中的"陶器"会暗含一些信息(例如它可能很重且易碎,注意不要掉到地上)。 对象的类型还决定了它装着的数据是允许被修改的变量(可变的 mutable)还是不可被修改的常量(不可变的 immutable)。你可以把不可变对象想象成一个透明但封闭的盒子:你可以看到里面装的数据,但是无法改变它。类似地,可变对象就像一个开着口的盒子,你不仅可以看到里面的数据,还可以拿出来修改它,但你无法改变这个盒子本身,即你无法改变对象的类型。

  • mutable:可变对象,如 List、Dict 和 Set
  • immutable:不可变对象,如 Number、String、Tuple、Frozenset

注意:

Python 赋值操作或函数参数传递,传递的永远是对象引用(即内存地址),而不是对象内容。

In [1]: a = 1
In [2]: b = a

In [3]: id(a)
Out[3]: 9164864
In [4]: id(b)
Out[4]: 9164864

In [5]: b += 1
In [6]: a
Out[6]: 1
In [7]: b
Out[7]: 2
In [8]: id(a)  # 对象引用a还是指向Number对象1
Out[8]: 9164864
In [9]: id(b)  # 对象引用b指向了Number对象2
Out[9]: 9164896

Python 会缓存使用非常频繁的小整数-5 至 256 、 ISO/IEC 8859-1 单字符 、 只包含大小写英文字 母的字符串 ,以对其复用,不会创建新的对象:

1. 不会创建新对象 In [1]: a = 10
In [2]: b = 10
In [3]: id(a)
Out[3]: 9165152
In [4]: id(b)
Out[4]: 9165152
In [5]: a = '@'
In [6]: b = '@'
In [7]: id(a)
Out[7]: 139812844740424
In [8]: id(b)
Out[8]: 139812844740424
In [9]: a = 'HELLOWORLDhelloworld'
In [10]: b = 'HELLOWORLDhelloworld'
In [11]: id(a)
Out[11]: 139812785036792
In [12]: id(b)
Out[12]: 139812785036792
2. 会创建新的对象
In [1]: a = 1000
In [2]: b = 1000
In [3]: id(a)
Out[3]: 140528314730384
In [4]: id(b)
Out[4]: 140528314731824
In [5]: a = 'x*y'
In [6]: b = 'x*y'
In [7]: id(a)
Out[7]: 139897777405880
In [8]: id(b)
Out[8]: 139897777403808
In [9]: a = 'Hello World'
In [10]: b = 'Hello World'
In [11]: id(a)
Out[11]: 139897789146096
In [12]: id(b)
Out[12]: 139897789179568

copy 是浅拷贝 (只拷贝内存地址)

deepcopy 是深拷贝 (内容重新分配)

03-对象属性管理

3.1__dict__方法

__dict__方法可以获取类或者对象的所有属性和方法。

类.__dict__可以直接获取到类定义时所有的方法和属性

实例对象.__dict__可以直接获取到实例的所有的方法和属性,不能获取到类中的

但是实例对象的方法指向了类对象的方法,所以实例对象能调用类方法。

(对象添加属性或方法不影响类)

对象.__dict__['key']可以直接获取到 value

不存在的 key 会报错 KeyError

class Person(object):
    name = 'python'
    age = 18

    def __init__(self):
        self.sex = "boy"
        self.like = "papapa"

    @staticmethod
    def stat_func():
        print('this is stat_func')

    @classmethod
    def class_func(cls):
        print('class_func')

person = Person()
print('Person.__dict__: ', Person.__dict__)
print('person.__dict__: ', person.__dict__)

out:
Person.__dict__:  {'__module__': '__main__', 'name': 'python', 'age': 18, '__init__': <function Person.__init__ at 0x000002C518993950>, 'stat_func': <staticmethod object at 0x000002C518996978>, 'class_func': <classmethod object at 0x000002C5189969B0>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

person.__dict__:  {'sex': 'boy', 'like': 'papapa'}

由此可见, 类的普通方法、类方法、静态方法、全局变量以及一些内置的属性都是放在类对 象 dict 里 而实例对象中存储了一些 self.xxx 的一些东西

3.2 继承

在类的继承中,子类有自己的 dict, 父类也有自己的 dict,子类的全局变量和方法放在子类的 dict 中, 父类的放在父类 dict 中。

3.3 动态语言限制属性的修改

现在我们终于明白了,动态语言与静态语言的不同

  • 动态语言:可以在运行的过程中,修改代码

  • 静态语言:编译时已经确定好代码,运行过程中不能修改

如果我们想要限制实例的属性怎么办?比如,只允许对 Person 实例添加 name 和 age 属性。

class Person:
    __slots__ = ("name", "age")
    def __init__(self,name,age):
        self.name = name
        self.age = age

p = Person("老王",20)
p.score = 100

out:
Traceback (most recent call last):  File "C:/Users/Administrator/PycharmProjects/test/app.py", line 8, in <module>
    p.score = 100

AttributeError: 'Person' object has no attribute 'score'

使用 __slots__ 要注意, __slots__ 定义的属性仅对当前类实例起作用,对继承的子类是不起作用

当你定义__slots__后,Python 就会为实例使用一种更加紧凑的内部表示。 实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典。所以__slots__是创建大量对象时节省内存的方法。

__slots__的副作用是作为一个封装工具来防止用户给实例增加新的属性。 尽管使用__slots__可以达到这样的目的,但是这个并不是它的初衷。

3.4 一些方法

hasattr()函数用于判断对象是否包含对应的属性

getattr()函数用于返回一个对象属性值

setattr 函数,用于设置属性值,该属性必须存在

delattr 函数用于删除属性,delattr(x,'foobar)相当于 del x.foobar

04-__call__魔法方法

可以使得函数可以直接被调用

class Person(object):

    def __call__(self, *args, **kwargs):
        return "ToString()"

person = Person()
print(person())

out:
ToString()

05-闭包

闭包就是,内层函数调用了外层函数的方法或变量,并返回内层方法。

闭包会保存局部作用域的变量。

def outer(num):
    num = num

    def inner():
        nonlocal num
        num += 1
        print(num)

    return inner

func = outer(100)
func()
func()
func()
func()

out:
101
102
103
104

闭包的内部函数不可以使用外部循环的变量,或者会变化的变量

def count():
    fs = []
    for i in range(1, 4):
        def f():
            return i*i
        fs.append(f)
    return fs

f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())

out:
9
9
9

原因是,调用的时候,i 已经变为 3 了,def 定义的函数不会立即执行。

进行一些修改:

def count():
    def f(j):
        return lambda: j * j
    fs = []
    for i in range(1, 4):
        fs.append(f(i))
        # f(i)立刻被执行,因此i的当前值被传入闭包lambda: j * j
    return fs


f1, f2, f3 = count()
print(f1())
print(f2())
print(f3())

out:
1
4
9

f(i)立刻被执行,因此 i 的当前值被传入闭包 lambda: j * j,调用的时候只是在调用 lambda:j*j

也可以这么写:

def count():
    def f(j):
        def double():
            return j*j
        return double
    fs = []
    for i in range(1, 4):
        fs.append(f(i))
    return fs

06-装饰器

装饰器(decorator)接受一个 callable 对象 (可以是函数或者实现了 call 方法的类)作为参数,并返回一个 callable 对象 它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。 举个实例,假设你写好了 100 个 Flask 中的路由函数,现在要在访问这些路由函数前,先判断用户 是否有权限,你不可能去这 100 个路由函数中都添加一遍权限判断的代码(如果权限判断代码为 5 行,你得加 500 行)。那么,你可能想把权限认证的代码抽离为一个函数,然后去那 100 个路由函 数中调用这个权限认证函数,这样只要加 100 行。但是,根据开放封闭原则,对于已经实现的功能代码建议不能修改, 但可以扩展,因为可能你在这些路由函数中直接添加代码会导致原函数出 现问题,那么最佳实践是使用装饰器

def my_func():
    print("step 2 : my_func")


def outer(my_func):
    print("step 1 : outer")
    def inner():
        my_func()
        print("step 3 : inner")
    return inner


# 传入my_func的引用
# 方法名即变量名,是引用,也就是改变了my_func的指向。即可在不改变原有功能下,增加新功能
my_func = outer(my_func)
my_func()

out:
step 1 : outer
step 2 : my_func
step 3 : inner

传入 my_func 的引用,方法名即变量名,是引用,也就是改变了 my_func 的指向。即可在不改变原有功能下,增加新功能

使用 Python 语法糖@

@也就是封装了一行语句:my_func = outer(my_func),将这行代码简化

def outer(my_func):
    print("step 1 : outer")
    def inner():
        my_func()
        print("step 3 : inner")
    return inner

@outer
def my_func():
    print("step 2 : my_func")

my_func()#等价于inner()

out:
step 1 : outer
step 2 : my_func
step 3 : inner

被装饰器修饰的代码块一定是在下面的,是原来的功能。

新的方法应当写在老方法的上面,然后在老方法用@新方法名修饰,然后老代码无需修改,即可调用新的功能

6.1 装饰器的几种

对原函数而言

6.1.1 没有参数、没有返回值

同上述的例子

6.1.2 有参数、没有返回值

def outer(my_func):
    print("step 1 : outer")
    def inner(num):
        my_func(num)
        print("step 3 : inner")
    return inner

@outer
def my_func(num):
    print("step 2 : my_func      传入的参数",num)

my_func(100) #等价于inner(100)

out:
step 1 : outer
step 2 : my_func      传入的参数 100
step 3 : inner

6.1.3 没有参数、有返回值

def outer(my_func):
    print("step 1 : outer")
    def inner():
        print("step 2 : 进入内部函数")
        return my_func()
    return inner


@outer
def my_func():
    print("step 3 : my_func")
    return "初始函数返回值"


print(my_func())

out:
step 1 : outer
step 2 : 进入内部函数
step 3 : my_func
初始函数返回值

6.1.4 有参数、有返回值

def outer(my_func):
    print("step 1 : outer")
    def inner(num):
        print("step 2 : 进入内部函数")
        return my_func(num)
    return inner


@outer
def my_func(num):
    print("step 3 : my_func,传入的参数:", num)
    num += 1
    return "加1之后:" + str(num)


print(my_func(num=100))


out:
step 1 : outer
step 2 : 进入内部函数
step 3 : my_func,传入的参数: 100
加1之后:101

6.2 万能装饰器

利用*args*kwargs传参

需要注意的是,传入到 inner 内部调用的函数的时候,需要解包

即还是写成*args,*kwargs

6.3 多装饰器

def outer1(func):
    print("outer1")
    def inner():
        print("inner1")
        return func()
    return inner

def outer2(func):
    print("outer2")
    def inner():
        print("inner2")
        return func()
    return inner


@outer1
@outer2
def func():
    print("func")


func()


out:
outer2
outer1
inner1
inner2
func

6.4 带参数的装饰器

def outer(str):
    print("outer")
    def outer1(func):
        print("outer1")
        def inner():
            print(str)
            print("inner")
            return func()
        return inner
    return outer1

@outer("哈哈哈")
def func():
    print("func")


func()

out:
outer
outer1   # 到此处都是装饰的时候生成的,下面才是调用的时候生成的
哈哈哈
inner
func

@函数名是装饰器

@函数名()是在调用函数后装饰

则在此处,outer()应当返回一个函数引用。

三层嵌套的函数可以为装饰器接受参数。

二层嵌套的函数不可以接受参数。

6.5 还原函数名称

def outer(func):
    print("outer")
    def inner():
        print("inner")
        print(func.__name__)
        return func()
    return inner

@outer
def func():
    print("func in")

func()  # 就是innner()

# 获取当前函数的名称
print(func.__name__)

out:
outer
inner
func
func in
inner

被装饰器修饰过的方法,打印的方法名称都是 inner,在打印日志的时候就无法追踪了。

在此需要还原方法名称

使用 Python 的@wraps帮助还原被装饰器修饰的函数名

from functools import wraps

def outer(func):
    print("outer")
    # 将方法名传入wraps
    @wraps(func)
    def inner():
        print("inner")
        print(func.__name__)
        return func()
    return inner

@outer
def func():
    print("func in")

@outer
def func1():
    print("func1 in")

func()  # 就是innner()
func1()

# 获取当前函数的名称
print(func.__name__)
print(func1.__name__)


out:
outer
outer
inner
func
func in
inner
func1
func1 in
func
func1

@wraps(函数名)修饰 inner 的内部方法。即可还原函数名

6.6 类装饰器

用类装饰的函数

class A:
    # outer
    def __init__(self,func):
        print("开始装饰")
        self.func = func

    # inner
    def __call__(self, *args, **kwargs):
        print("类装饰器")
        return self.func()


@A       # func = A(func) 创建一个对象,func-->A类的实例对象。
def func():
    print("this is func")

func()

out:
开始装饰
类装饰器
this is func

7-案例

1.函数执行时间的统计

import time
from functools import wraps

def outer(func):
    @wraps(func)
    def inner(num):
        start = time.time()
        ret = func(num)
        end = time.time()
        print("执行时间为:", end-start)
        return ret
    return inner

@outer
def func(num):
    ret = 0
    for i in range(num+1):
        ret += i
    return ret

print(func(100000000))


out:
执行时间为: 7.979628324508667
5000000050000000

2.日志记录

首先是之前的日志模块,这里重新贴一遍,略作修改

import logging

# 1. 创建一个叫aiotest的logger实例,如果参数为空则返回root
logger = logging.getLogger('aiotest')

# 2. 设置总日志级别, 也可以给不同的handler设置不同的日志级别
logger.setLevel(logging.DEBUG)

# 3. 设置Formatter
formatter = logging.Formatter('%(asctime)s - %(filename)s [line:%(lineno)d] - %(func)s - <%(threadName)s %(thread)d>' +
                              ' - <Process %(process)d> - %(levelname)s: %(message)s')

# 4. 创建Handler
# 文件Handler
file_handler = logging.FileHandler("logger.log", encoding="utf8")
# 控制台输出Handler
stream_handler = logging.StreamHandler()

# 5. 给Handler设置属性
stream_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
stream_handler.setLevel(logging.DEBUG)
file_handler.setLevel(logging.WARNING)

# 6. 将Handler添加到logger实例上
logger.addHandler(stream_handler)
logger.addHandler(file_handler)

因为%(funcName)s永远获取的是调用函数的 inner 的名字,所以这里传入新的自定义变量,将正确的函数名通过@wraps(函数引用)来传入,并通过.__name__方法获取正确的函数名称。

from functools import wraps
from myLog import logger


def outer(func):
    @wraps(func)
    def inner(*args, **kwargs):
        try:
            logger.info("当前执行方法为:" + func.__name__, extra={'func': func.__name__})
            return func(*args, **kwargs)
        except Exception as e:
            logger.error(e.__repr__(), extra={'func': func.__name__})

    return inner


@outer
def func1(*args, **kwargs):
    return 1 / 1


@outer
def func2(*args, **kwargs):
    return 1 / 0


print(func1())
print(func2())

out:
2019-08-08 23:37:10,982 - 02-对象.py [line:8] - func1 - <MainThread 14556> - <Process 18380> - INFO: 当前执行方法为:func1
1.0
2019-08-08 23:37:10,983 - 02-对象.py [line:8] - func2 - <MainThread 14556> - <Process 18380> - INFO: 当前执行方法为:func2
None
2019-08-08 23:37:10,983 - 02-对象.py [line:11] - func2 - <MainThread 14556> - <Process 18380> - ERROR: ZeroDivisionError('division by zero',)

日志文件中的记录:
2019-08-08 23:41:24,756 - 02-对象.py [line:12] - func2 - <MainThread 16856> - <Process 8564> - ERROR: ZeroDivisionError('division by zero',)

疑问:

执行顺序问题。多次执行打印的顺序不一样?