本篇是python的面向对象高级篇,主要描述反射、内置属性与方法、元类;为下一篇的ORM框架做好准备。废话不多说,咱就从反射开始吧。

反射

第一次听说反射这个术语,还是在自学Java的时候看到,知道了什么是类的类,怎么通过这个类的类来反射到对象,貌似搞得很复杂,不过基本的思想还是搞明白了。不想,在学python的时候又碰见反射了,个人觉得python的反射比Java要简洁一点。

python面向对象中的反射:通过字符串的形式,导入模块;通过字符串的形式,去模块寻找指定函数,并执行。利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,一种基于字符串的事件驱动!

python的反射主要靠4个内置函数实现:hasattr、getattr、setattr、delattr

hasattr

hasattr(object, name)

判断对象object是否包含名为name的特性(hasattr是通过调用getattr(ojbect, name)是否抛出异常来实现的)

class Person(object): def __init__(self,name): self.name = name def talk(self): print('%s正在交谈'%self.name)p = Person('laowang') print(hasattr(p,'talk')) # True。因为存在talk方法print(hasattr(p,'name')) # True。因为存在name变量print(hasattr(p,'abc')) # False。因为不存在abc方法或变量1

2

3

4

5

6

7

8

9

101

2

3

4

5

6

7

8

9

10

getattr

hasattr(object, name, default=none)

获取对象中的方法或变量的内存地址 class Person(object): def __init__(self,name): self.name = name def talk(self): print('%s正在交谈'%self.name)p = Person('laowang')n = getattr(p,'name') # 获取name变量的内存地址print(n) # 此时打印的是:laowangf = getattr(p,'talk') # 获取talk方法的内存地址f() # 调用talk方法#我们发现getattr有三个参数,那么第三个参数是做什么用的呢?s = getattr(p,'abc','not find')print(s) # 打印结果:not find。因为abc在对象p中找不到,本应该报错,属性找不到,但因为修改了找不到就输出not find1

2

3

4

5

6

7

8

9

10

11

12

13

141

2

3

4

5

6

7

8

9

10

11

12

13

14

setattr

setattr(object, name, value)

为对象添加变量或方法

def abc(self): print('%s正在交谈'%self.name)class Person(object): def __init__(self,name): self.name = namep = Person('laowang')setattr(p,'talk',abc) # 将abc函数添加到对象中p中,并命名为talkp.talk(p) # 调用talk方法,因为这是额外添加的方法,需手动传入对象setattr(p,'age',30) # 添加一个变量age,复制为30print(p.age) # 打印结果:301

2

3

4

5

6

7

8

9

10

11

121

2

3

4

5

6

7

8

9

10

11

12

delattr

删除对象中的变量。注意:不能用于删除方法

delattr(object, name) class Foo(object): country = 'china' def __init__(self, name): self.name = name def f(self): print 'this is function f.name = ', self.nameobj = Foo('abc')print getattr(obj, 'name')delattr(obj, 'name') #删除掉了对象的普通字段nameprint getattr(obj, 'name')print getattr(Foo, 'country')delattr(Foo, 'country') #删除掉类的静态字段print getattr(Foo, 'country') #打印时说找不到些成员,报错1

2

3

4

5

6

7

8

9

10

11

12

13

14

151

2

3

4

5

6

7

8

9

10

11

12

13

14

15

模块与函数反射

由于Python中一切皆对象,因此模块与函数也可以使用反射机制。

模块反射示例:

#!/usr/bin/env python# -*- coding:utf-8 -*-def test(): print('from the test')#!/usr/bin/env python# -*- coding:utf-8 -*-'''程序目录: module_test.py index.py当前文件: index.py'''import module_test as obj#obj.test()print(hasattr(obj,'test'))getattr(obj,'test')()1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

181

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

注:getattr,hasattr,setattr,delattr对模块的修改都在内存中进行,并不会影响文件中真实内容。

对象魔术方法

在Python中,所有以双下划线包起来的方法,都统称为”魔术方法”。比如我们接触最多的对象初始化方法__init__。下面主要介绍控制对象属性访问的魔术方法。 class Foo: x=1 def __init__(self,y): self.y=y def __getattr__(self, item): print('----> from getattr:你找的属性不存在') def __setattr__(self, key, value): print('----> from setattr') # self.key=value #这就无限递归了,你好好想想 # self.__dict__[key]=value #应该使用它 def __delattr__(self, item): print('----> from delattr') # del self.item #无限递归了 self.__dict__.pop(item)#__setattr__添加/修改属性会触发它的执行f1=Foo(10)print(f1.__dict__) # 因为你重写了__setattr__,凡是赋值操作都会触发它的运行,你啥都没写,就是根本没赋值,除非你直接操作属性字典,否则永远无法赋值f1.z=3print(f1.__dict__)#__delattr__删除属性的时候会触发f1.__dict__['a']=3#我们可以直接修改属性字典,来完成添加/修改属性的操作del f1.aprint(f1.__dict__)#__getattr__只有在使用点调用属性且属性不存在的时候才会触发f1.xxxxxx1

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

311

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

__getattr__(self, name)

该方法定义,试图访问一个不存在的属性时触发调用。因此重载该方法可以实现捕获错误拼写然后进行重定向, 或者对一些废弃的属性进行警告。

class Foo: x=1 def __init__(self,y): self.y=y def __getattr__(self, item): print('----> from getattr:你找的属性不存在')#__getattr__只有在使用点调用属性且属性不存在的时候才会触发f1.xxxxxx1

2

3

4

5

6

7

81

2

3

4

5

6

7

8

__setattr__(self, name)

它允许你自定义某个属性的赋值行为,不管这个属性存在与否,也就是说你可以对任意属性的任何变化都定义自己的规则。然后,一定要小心使用 __setattr__不管对象的某个属性是否存在,它都允许你为该属性进行赋值,因此你可以为属性的值进行自定义操作。有一点需要注意,实现__setattr__时要避免”无限递归”的错误,下面的代码示例中会提到。 def __setattr__(self, name. value): self.name = value # 因为每次属性幅值都要调用 __setattr__(),所以这里的实现会导致递归 # 这里的调用实际上是 self.__setattr('name', value)。因为这个方法一直 # 在调用自己,因此递归将持续进行,直到程序崩溃def __setattr__(self, name, value): self.__dict__[name] = value # 使用 __dict__ 进行赋值 # 定义自定义行为1

2

3

4

5

6

7

8

91

2

3

4

5

6

7

8

9

__delattr__(self, name)

这个魔法方法和__setattr__ 几乎相同,只不过它是用于处理删除属性时的行为。和 __setattr__ 一样,使用它时也需要多加小心,防止产生无限递归(在 __delattr__的实现中调用 del self.name 会导致无限递归)

def __delattr__(self, item): print('----> from delattr') # del self.item #无限递归了 self.__dict__.pop(item)#__delattr__删除属性的时候会触发f1.__dict__['a']=3#我们可以直接修改属性字典,来完成添加/修改属性的操作del f1.aprint(f1.__dict__)1

2

3

4

5

6

7

81

2

3

4

5

6

7

8

__getattribute__(self, name)

__getattribute__ 看起来和上面那些方法很合得来,但是最好不要使用它。__getattribute__ 只能用于新式类。在最新版的Python中所有的类都是新式类,在老版Python中你可以通过继承 object 来创建新式类。 __getattribute__ 允许你自定义属性被访问时的行为,它也同样可能遇到无限递归问题(通过调用基类的 __getattribute__ 来避免)。 __getattribute__ 基本上可以替代 __getattr__ 。只有当它被实现,并且显式地被调用,或者产生 AttributeError 时它才被使用。 这个魔法方法可以被使用(毕竟,选择权在你自己),我不推荐你使用它,因为它的使用范围相对有限(通常我们想要在赋值时进行特殊操作,而不是取值时),而且实现这个方法很容易出现Bug。 class Foo: def __init__(self,x): self.x=x def __getattr__(self, item): print('执行的是我') # return self.__dict__[item] def __getattribute__(self, item): print('不管是否存在,我都会执行') raise AttributeError('哈哈')f1=Foo(10)f1.xf1.xxxxxx#当__getattribute__与__getattr__同时存在,只会执行__getattrbute__#除非__getattribute__在执行过程中抛出异常AttributeError1

2

3

4

5

6

7

8

9

10

11

12

13

141

2

3

4

5

6

7

8

9

10

11

12

13

14

__str__()与__repr__()

>>> class Student(object):... def __init__(self, name):... self.name = name...>>> print(Student('Michael'))<__main__.student object at>1

2

3

4

5

61

2

3

4

5

6

打印出一堆<__main__.student object at>,不好看。

怎么才能打印得好看呢?只需要定义好__str__()方法,返回一个好看的字符串就可以了: >>> class Student(object):... def __init__(self, name):... self.name = name... def __str__(self):... return 'Student object (name: %s)' % self.name...>>> print(Student('Michael'))Student object (name: Michael)>>> s = Student('Michael')>>> s<__main__.student object at>1

2

3

4

5

6

7

8

9

10

11

121

2

3

4

5

6

7

8

9

10

11

12

嗯,怎么直接调用 s 答应出来的也是对象地址信息呢?这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以,有个偷懒的写法:

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

2

3

4

5

61

2

3

4

5

6

__next__和__iter__实现迭代器协议

如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。我们以斐波那契数列为例,写一个Fib类,可以作用于for循环: 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 # 返回下一个值>>>for n in Fib():... print(n)...11235...46368750251

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

231

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

__getitem__()

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

>>> Fib()[5]Traceback (most recent call last): File '', line 1, in TypeError: 'Fib' object does not support indexing1

2

3

41

2

3

4

要表现得像list那样按照下标取出元素,需要实现__getitem__()方法: class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 start = n.start stop = n.stop if start is None: start = 0 a, b = 1, 1 L = [] for x in range(stop): if x >= start: L.append(a) a, b = b, a + b return L1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

191

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

以上实现没有对step参数作处理,也没有对负数作处理,所以,要正确实现一个__getitem__()还是有很多工作要做的。此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object,例如str。

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

__del__()

析构方法,当对象在内存中被释放时,自动触发执行。

注:如果产生的对象仅仅只是python程序级别的(用户级),那么无需定义del,如果产生的对象的同时还会向操作系统发起系统调用,即一个对象有用户级与内核级两种资源,比如(打开一个文件,创建一个数据库链接),则必须在清除对象的同时回收系统资源,这就用到了__del__()

class Foo: def__del__(self): print('执行我啦')f1=Foo()del f1print('------->')#输出结果执行我啦------->1

2

3

4

5

6

7

8

9

101

2

3

4

5

6

7

8

9

10

典型的应用场景:

创建数据库类,用该类实例化出数据库链接对象,对象本身是存放于用户空间内存中,而链接则是由操作系统管理的,存放于内核空间内存中;当程序结束时,python只会回收自己的内存空间,即用户态内存,而操作系统的资源则没有被回收,这就需要我们定制__del__(),在对象被删除前向操作系统发起关闭数据库链接的系统调用,回收资源

__call__()

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。

任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用。请看示例: class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s.' % self.name)>>>s = Student('Michael')>>>s() # self参数不要传入My name is Michael.1

2

3

4

5

6

7

8

91

2

3

4

5

6

7

8

9

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

如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。

那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:

>>>callable(Student())True>>>callable(max)True>>>callable([1, 2, 3])False>>>callable(None)False>>>callable('str')False1

2

3

4

5

6

7

8

9

101

2

3

4

5

6

7

8

9

10

__slots__ __slots__是什么:是一个类变量,变量值可以是列表,元祖,或者可迭代对象,也可以是一个字符串(意味着所有实例只有一个数据属性)

引子:使用点来访问属性本质就是在访问类或者对象的__dict__属性字典(类的字典是共享的,而每个实例的是独立的)

为何使用__slots__:字典会占用大量内存,如果你有一个属性很少的类,但是有很多实例,为了节省内存可以使用__slots__取代实例的__dict__当你定义__slots__后,__slots__就会为实例使用一种更加紧凑的内部表示。实例通过一个很小的固定大小的数组来构建,而不是为每个实例定义一个字典,这跟元组或列表很类似。在__slots__中列出的属性名在内部被映射到这个数组的指定小标上。使用__slots__一个不好的地方就是我们不能再给实例添加新的属性了,只能使用在__slots__中定义的那些属性名。

注意事项:__slots__的很多特性都依赖于普通的基于字典的实现。另外,定义了__slots__后的类不再 支持一些普通类特性了,比如多继承。大多数情况下,你应该只在那些经常被使用到 的用作数据结构的类上定义__slots__比如在程序中需要创建某个类的几百万个实例对象 。

关于__slots__的一个常见误区是它可以作为一个封装工具来防止用户给实例增加新的属性。尽管使用__slots__可以达到这样的目的,但是这个并不是它的初衷;更多的是用来作为一个内存优化工具。 class Foo: __slots__='x'f1=Foo()f1.x=1f1.y=2#报错print(f1.__slots__) #f1不再有__dict__class Bar: __slots__=['x','y'] n=Bar()n.x,n.y=1,2n.z=3#报错1

2

3

4

5

6

7

8

9

10

11

121

2

3

4

5

6

7

8

9

10

11

12

class Foo: __slots__=['name','age']f1=Foo()f1.name='alex'f1.age=18print(f1.__slots__)f2=Foo()f2.name='egon'f2.age=19print(f2.__slots__)print(Foo.__dict__)#f1与f2都没有属性字典__dict__了,统一归__slots__管,节省内存1

2

3

4

5

6

7

8

9

10

11

12

13

14

151

2

3

4

5

6

7

8

9

10

11

12

13

14

15

__doc__

类的描述信息,该属性无法继承给子类 class Foo: '我是描述信息' passprint(Foo.__doc__)class Foo: '我是描述信息' passclass Bar(Foo): passprint(Bar.__doc__) #该属性无法继承给子类1

2

3

4

5

6

7

8

9

10

11

12

131

2

3

4

5

6

7

8

9

10

11

12

13

__module__和__class__

__module__表示当前操作的对象在那个模块

__class__ 表示当前操作的对象的类是什么

#!/usr/bin/env python# -*- coding:utf-8 -*-class C: def __init__(self): self.name = ‘SB'1

2

3

4

5

61

2

3

4

5

6 from lib.aa import Cobj = C()print obj.__module__ # 输出 lib.aa,即:输出模块print obj.__class__ # 输出 lib.aa.C,即:输出类1

2

3

4

51

2

3

4

5

__enter__和__exit__

在操作文件对象的时候可以这么写:

with open('a.txt') as f:  '代码块'1

21

2

上述叫做上下文管理协议,即with语句,为了让一个对象兼容with语句,必须在这个对象的类中实现__enter__和__exit__方法 class Open: def __init__(self,name): self.name=name def __enter__(self): print('出现with语句,对象的__enter__被触发,有返回值则赋值给as声明的变量') # return self def __exit__(self, exc_type, exc_val, exc_tb): print('with中代码块执行完毕时执行我啊')with Open('a.txt') as f: print('=====>执行代码块') # print(f,f.name)1

2

3

4

5

6

7

8

9

10

11

12

131

2

3

4

5

6

7

8

9

10

11

12

13

__exit__()中的三个参数分别代表异常类型,异常值和追溯信息,with语句中代码块出现异常,则with后的代码都无法执行

class Open: def __init__(self,name): self.name=name def __enter__(self): print('出现with语句,对象的__enter__被触发,有返回值则赋值给as声明的变量') def __exit__(self, exc_type, exc_val, exc_tb): print('with中代码块执行完毕时执行我啊') print(exc_type) print(exc_val) print(exc_tb)with Open('a.txt') as f: print('=====>执行代码块') raise AttributeError('***着火啦,救火啊***')print('0'*100) #------------------------------->不会执行1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

171

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

如果__exit__()返回值为True,那么异常会被清空,就好像啥都没发生一样,with后的语句正常执行 class Open: def __init__(self,name): self.name=name def __enter__(self): print('出现with语句,对象的__enter__被触发,有返回值则赋值给as声明的变量') def __exit__(self, exc_type, exc_val, exc_tb): print('with中代码块执行完毕时执行我啊') print(exc_type) print(exc_val) print(exc_tb) return Truewith Open('a.txt') as f: print('=====>执行代码块') raise AttributeError('***着火啦,救火啊***')print('0'*100) #------------------------------->会执行1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

181

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

class Open: def __init__(self,name): self.name=name def __enter__(self): print('出现with语句,对象的__enter__被触发,有返回值则赋值给as声明的变量') def __exit__(self, exc_type, exc_val, exc_tb): print('with中代码块执行完毕时执行我啊') print(exc_type) print(exc_val) print(exc_tb) return Truewith Open('a.txt') as f: print('=====>执行代码块') raise AttributeError('***着火啦,救火啊***')print('0'*100) #------------------------------->会执行1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

181

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18 使用with语句的目的就是把代码块放入with中执行,with结束后,自动完成清理工作,无须手动干预

在需要管理一些资源比如文件,网络连接和锁的编程环境中,可以在__exit__中定制自动释放资源的机制,无须再去关注这个问题

本来还想把对象描述符也写在这里的,但是发现对象描述符我理解的还不够深刻,不知道是怎么一回事,决定再花点时间去研究下描述符,然后在专门写一篇关于描述符的文章,然后再写元类,再用元类写一个简易版本的ORM框架,算是对面向对象部分的总结。

python getattrribute_Python学习——面向对象高级之反射相关推荐

  1. Python基础之六面向对象高级编程

    '''面向对象高级编程 ''' from enum import Enum'''__slots__限制实例属性定义的属性只对当前类实例起作用,对于继承的子类不起作用''' class Student( ...

  2. Python基础学习——面向对象编程(第一讲:面向对象概述、面向对象三个基本特征(封装性、继承性、多态性)、类和对象(定义类、创建和使用对象、实例变量、类变量、构造方法、实例方法、类方法、静态方法))

    面向对象是Python最重要的特性,在Python中一切数据类型都是面向对象的. 1.面向对象概述 面向对象的编程思想是,按照真实世界客观事物的自然规律进行分析,客观世界中存在什么样的实体,构建软件系 ...

  3. python是一门面向对象的高级_9.Python笔记之面向对象高级部分

    类的成员 类的成员可以分为三大类:字段.方法和属性 注:所有成员中,只有普通字段的内容保存对象中,即:根据此类创建了多少对象,在内存中就有多少个普通字段.而其他的成员,则都是保存在类中,即:无论对象的 ...

  4. python继承语法_python语法学习面向对象之继承

    python语法学习面向对象之继承 只要涉及到面向对象,"类"是必须出现的一个代名词. 类和对象是面向对象编程的两个主要方面.类创建一个新类型,而对象是这个类的实例. 类的一些概念 ...

  5. Python学习笔记:面向对象高级编程(完)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  6. Python学习笔记:面向对象高级编程(中下)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  7. Python学习笔记:面向对象高级编程(中上)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  8. Python学习笔记:面向对象高级编程(上)

    前言 最近在学习深度学习,已经跑出了几个模型,但Pyhton的基础不够扎实,因此,开始补习Python了,大家都推荐廖雪峰的课程,因此,开始了学习,但光学有没有用,还要和大家讨论一下,因此,写下这些帖 ...

  9. Python学习之面向对象高级编程

    Python学习目录 1. 在Mac下使用Python3 2. Python学习之数据类型 3. Python学习之函数 4. Python学习之高级特性 5. Python学习之函数式编程 6. P ...

最新文章

  1. 第二篇:salt-api使用填坑指南
  2. java.util.ConcurrentModificationException异常分析
  3. poj 1039 Pipe (判断 直线和 线段 是否相交 并 求交点)
  4. 17.立体匹配——更好的效果与挑战,总结_5
  5. PHP开发中涉及到emoji表情的几种处理方法
  6. 实践教程 | Pytorch 模型的保存与迁移
  7. 问题 I: Sequence Problem : Array Practice
  8. matlab2c使用c++实现matlab函数系列教程-expstat函数
  9. manual 离线手册 韩顺平php_PHP - Manual: 手册的格式 (官方文档)
  10. 关于ioncube的license破解
  11. 「手绘控笔技巧」最有效的控笔练习,让你少走很多弯路
  12. transition过渡的基本使用
  13. 老年手机英文改中文_老年手机设置成英文怎么办
  14. AtCoder Beginner Contest 162 D RGB Triplets 前缀和
  15. Swap(交换函数)
  16. 猿辅导python助教面试两次都有什么内容_猿辅导面试信息第一手揭秘,小伙伴们看过来!...
  17. html的选项卡切换,纯css选项卡切换
  18. Groovy(Java笨狗)系列-Working with closures(1)
  19. 星特朗望远镜怎么样_内幕解析星特朗80500评测如何?怎么样呢?优缺点吐槽揭秘...
  20. 视频截取动图怎么做?分享在线视频转gif小窍门

热门文章

  1. Shifting More Attention to Video Salient Object Detection (CVPR 2019)
  2. GlusterFS元数据机制分析
  3. Notepad++添加读取十六进制插件HexEditor
  4. java 健身会所_基于jsp的健身俱乐部会员-JavaEE实现健身俱乐部会员 - java项目源码...
  5. Steaming SQL for Apache Kafka 学习
  6. Android客户端与PC服务器实现Socket通信
  7. 2022.6.14日新selenium写法
  8. 决策引擎EngineX平台实践
  9. 数据防泄密·工控安全保障方案
  10. python监听多个udp端口_尝试实现非阻塞python-udp多端口获取wierd异常