Python descriptor(描述符)的实现


Posted in Python onNovember 15, 2020

问题

问题1

Python是一种动态语言,不支持类型检查。当需要对一个对象执行类型检查时,可能会采用下面的方式:

class Foo(object):
 def __init__(self,a):
 if isinstance(a,int):
  self.__a = a
 else:
  raise TypeError("Must be an int")

 def set_a(self,val):
 if isinstance(val,int):
  self.__a = val
 else:
  raise TypeError("Must be an int")

 def get_a(self):
 return self.__a

上述是一种类型检查的方法,但是如果需要类型检查的参数非常多的话就会变得非常繁琐,重复代码太多,Python这么简洁,优雅,肯定有更好的解决方法。另外上述方法也有缺陷。

f = Foo(1)
print f.get_a() #1
print f._Foo__a #1,还是能访问到= =
f._Foo__a = 'test'
print f.get_a() #test,还是改变了__a的值,而且不是int型
print f._Foo__a #test

问题2

在一个对象中,创建一个只读属性。

问题3

class Foo(object):
 a = 1
f = Foo()
print f.a #1,实例属性中没有a属性,所以到Foo.__dict__中查找
print Foo.a #1
print f.__dict__ #{}
f.a = 2  #增加一个名为a的实例属性
print f.a #2,搜索属性时先在实例字典中查找,然后再去类的字典中查找,在实例的__dict__中找到了..
print Foo.a #1
print f.__dict__ #{'a': 2}

如果不想给实例f增加实例属性,而是想对类属性操作怎么办呢。解决方案:

1) 使用class.attr改变值;

Foo.a = 2就不会给实例增加实例属性了。

2) 自定义属性访问,描述符;

class descriptor(object):
 def __init__(self,val):
 self.val = val
 def __get__(self,obj,type = None):
 print 'get',
 return self.val
 def __set__(self,obj,val):
 print 'set',val
 self.val = val
 def __delete__(self,obj):
 raise AttributeError("Can't delete attribute")
class Foo(object):
 a = descriptor(0)

f = Foo()
print f.a #get 0
print Foo.a #get 0
print f.__dict__ #{}
f.a = 2  #set 2,并没有增加实例属性
print f.a #get 2
print Foo.a #get 2
print f.__dict__ #{}

问题总结

上述三个问题均与属性访问有关,如果能够自定义属性访问,上述问题就能解决啦= =。其实问题3已经给出了解决方法,就是描述符…

描述符的定义和介绍

​ 描述符(Descriptor)是Python中非常重要的一部分,它广泛应用于Python的内核。

​ 一般来说,描述符就是一个带有绑定方法的对象,只不过照比其他对象多了几个特殊的描述符方法,即 __get__(),__set__(),__delete__()对描述符对象的属性访问主要通过描述符方法。

定义:一个对象如果定义了__get__(),__set__(),__delete__()方法中的任何一个,它就可以被称为描述符。

​ 对属性的访问默认是从对象的字典中获取(get),设置(set)和删除(delete)属性。

假设有实例a,a有属性x,获取a.x的值。

一般来说,a.x会按以下顺序查找属性,查找链: a.__dict__['x']type(a).__dict__['x']根据mro顺序在type(a)的父类中查找(不包括元类)

​ 但是,如果被查找的属性是一个描述符,并且为类属性,那么就会覆盖其默认行为,转而去调用描述符方法。注意,描述符仅适用于新式类和新式对象。

class descriptor(object):
 def __init__(self,val):
  self.val = val

 def __get__(self, obj, objtype):
  print 'get',
  return self.val

 def __set__(self, obj, val):
  print 'set'
  self.val = val

class Foo(object):
 x = descriptor(0)
 y = 0

f = Foo()
'''描述符覆盖默认的访问行为'''
print f.x   #get 0,调用的是描述符的__get__函数
print f.__dict__ #{}
f.x = 1    #set,调用的是描述符的__set__函数
print f.x   #get 1
print f.__dict__ #{},即使赋值也没有增加实例属性...,是不是覆盖了默认行为- -
f.y = 2    
print f.__dict__ #{'y': 2},因为没有与y同名的描述符对象...

描述符十分强大。properties,methods, static methods,class methods, and super()都是基于描述符实现的。从Python2.2开始,描述符就在新式类中出现了。描述符是一个灵活的工具,使程序开发更加便利。感觉描述符很吊啊…

Descriptor Protocol(描述符协议)

descriptor.__get__(self, obj, type=None) --> value

descriptor.__set__(self, obj, value) --> None

descriptor.__delete__(self, obj) --> None

上述三个方法就是描述符方法,如果一个对象定义了描述符方法中的任何一个,那么这个对象就会成为描述符。

假设有一个对象t,t.a是一个定义了三个描述符方法的描述符,并且a是类属性,调用情况如下:

print t.a → a.__get__(t, type(t))

t.a = v → a.__set__(t, v)

del t.a → a.__delete__(t)

注意:当描述符作为类属性时,才会自动调用描述符方法,描述符作为实例属性时,不会自动调用描述符方法。

数据和非数据描述符

data descriptor(数据描述符):定义了__get__()__set__()方法的对象;

non-data descriptor(非数据描述符):定义了__get__()的对象;非数据描述符典型应用是class methods。

数据和非数据描述符的区别

如果一个实例的字典(__dict__)中有一个和数据描述符对象相同名字的实例属性,则优先访问数据描述符;

class descriptor(object):
 def __init__(self,val = 0):
  self.val = val

 def __get__(self, obj, objtype):
  print '__get__',
  return self.val

 def __set__(self, obj, val):
  self.val = val

class Foo(object):
 a = descriptor()#数据描述符
f = Foo()
f.a = 1    #不会增加实例属性,会调用descriptor.__set__方法,数据描述符的优先级比实例属性高
print f.a   #__get__ 1,调用的是descriptor.__get__方法
print f.__dict__ #{}

如果一个实例中存在和数据描述符名字相同的实例属性,利用下述方法就可以访问实例属性…

f.__dict__['a'] = 2   #增加一个与数据描述符同名的实例属性
print f.__dict__   #{'a': 2}
print f.a     #__get__ 1,正常使用,调用的仍是描述符的__get__方法
print f.__dict__['a']  #2,这个时候就是实例属性了...

如果一个实例的字典中有一个和非数据描述符对象相同名字的实例属性,则优先访问实例属性;

class descriptor(object):
 def __init__(self,val = 0):
  self.val = val

 def __get__(self, obj, objtype):
  print '__get__',
  return self.val

class Foo(object):
 a = descriptor()#非数据描述符,没有__set__
f = Foo()
print f.a   #__get__ 0,调用descriptor.__get__方法
f.a = 1    #增加实例属性,因为实例属性的优先级比非数据描述符高
print f.a   #1,不会调用descriptor.__get__方法,实例属性的优先级比非数据描述符高
print f.__dict___ #{'a': 1},增加了实例属性a

数据和非数据描述符测试code

class data_descriptor(object):#数据描述符
 def __init__(self,val):
  self.val = val

 def __get__(self, obj,type = None):
  print 'data get'
  return self.val

 def __set__(self, obj, value):
  if not isinstance(value,int):#赋值时可以类型检查啊
   raise ValueError('value must be int')
  self.val = value

class non_data_descriptor(object):#非数据描述符
 def __init__(self,val):
  self.val = val

 def __get__(self,obj,type = None):
  print 'non data get'
  return self.val

class Foo(object):
 data = data_descriptor(0)
 non_data = non_data_descriptor(1)

f = Foo()
print f.__dict__ #{}
print f.data  #data get,0
print f.non_data #non data get,1
f.data = 2   #数据描述符优先级较高,不会创建实例属性,而是调用描述符的__set__方法
f.non_data = 3  #增加了与非数据描述符同名的实例属性

print f.__dict__ #{'non_data': 3}
print f.data  #data get,2 调用数据描述符的__get__()方法
print f.non_data #3,非数据描述符优先级比实例属性低
print Foo.non_data #non data get 1,利用类属性查找还是可以访问非数据描述符的,非数据描述符值未改变

属性访问,__getattribute__()

​ 在Python中,__getattribute__()就是属性解析机制,当调用一个属性时,不管是成员还是方法,都会触发 __getattribute__()来调用属性。

属性解析机制按照优先级链搜索属性。在优先级链中,类字典中的数据描述符的优先级高于实例变量,实例属性的优先级高于非数据描述符,如果定义了__getattr()__,优先级链会为__getattr()__分配最低优先级。可以通过自定义__getattribute__方法来重写优先级链。

优先级链 : 数据描述符 > 实例属性 > (非数据描述符,非描述符的类属性) > __getattr()__

上述所说的描述符均要为类属性,当描述符作为实例属性出现时,不会自动调用描述符方法。

class Foo(object):
 def __init__(self):
  self.x = 1
  self.y = 2

 def __getattribute__(self,keys = None):#这样优先级链,描述符什么的就都没用了。。。。
  return 'test'

f = Foo()
print f.x,f.y,f.z #test,test,test,优先级链什么的都没用啦= =

__getattribute__()的Python实现大致如下,

def __getattribute__(self, key):
 "Emulate type_getattro() in Objects/typeobject.c"
 v = object.__getattribute__(self, key)
 if hasattr(v, '__get__'):#如果v定义了__get__方法的话,优先调用v.__get__ 
  return v.__get__(None, self)
 return v

调用描述符

​ 描述符调用的细节取决于obj是一个对象还是一个类。不管是哪种,描述符只对新式对象和新式类起作用。继承了 object 的类是新式类。

​ 描述符也是一个对象,可以通过方法名直接调用描述符方法,如描述符d,d.__get__(object)。另外,在访问描述符时会自动调用相应的描述符方法(只有为类属性时才会自动调用)。描述符的自动调用机制基于__getattribute__()__getattribute__()确保了descriptor的机制,所以,如果重写了__getattribute__, 就可以消除descriptor机制。

​ 对于对象来说,obj.__getattribute__()会将b.x转化为 type(b).__dict__['x'].__get__(b,type(b)) ,按照下述顺序搜索属性:

类属性中的数据描述符 > 实例变量 > 类属性的非数据描述符 > __getattr__()

​ 对于类来说,class.__getattribute__()会将B.x转化为B.__dict__['x'].__get__(None,B)

记住以下几点:

1.描述符的调用基于__getattribute__();
2.重写__getattribute__()会阻止描述符的正常调用;
3.object.__getattribute__()和type.__getattribute__()会调用不同的__get__();
4.数据描述符一直覆盖实例属性,即同时存在同名的数据描述符和实例属性,优先调用数据描述符
5.非数据描述符一直被实例属性覆盖,即同时存在同名的非数据描述符和实例属性,优先调用实例属性;

​ 描述符的机制在 object, type, 和 super__getattribute__()方法中实现。由 object 派生出的类自动继承这个机制,或者它们有个有类似机制的元类。如果想让描述符失效的话,可以重写 __getattribute__()

class descriptor(object):
 def __init__(self, initval=None, name='var'):
  self.val = initval
  self.name = name

 def __get__(self, obj, objtype):
  print 'get', self.name,
  return self.val

 def __set__(self, obj, val):
  print 'set', self.name,val
  self.val = val

class Foo(object):
 x = descriptor(10, 'var "x"')
 y = 5

m = Foo()
'''访问m.x的三种方法'''
print m.x       #get var "x",10
print type(m).__dict__['x'].__get__(m,type(m)) #m.x会转化为这种形式,等价于m.x
print m.__getattribute__('x')  #等价于m.x,因为x定义了__get__方法,调用x的__get__方法,上面已经给出了__getattribute__的实现原理
'''设置m.x的值'''
m.x = 20        #set var "x" 20
type(m).__dict__['x'].__set__(m,20) #m.x = 20会转化为此种形式,等价于m.x = 20

print m.x       #get var "x",20
print m.y       #5
#print type(m).__dict__['y'].__get__(m,type(m)) #error,AttributeError: 'int' object has no attribute '__get__'
print m.__getattribute__('y')  #5,等价于m.y

描述符的陷阱

描述符应放在类层次上,即作为类属性(class level)

说了N多次了,要谨记…

class Foo(object):
 y = descriptor(0)
 def __init__(self):
  self.x = descriptor(1)  #实例属性的描述符是不会自动调用对应的描述符方法的...
b = Foo()
print "X is %s, Y is %s" % (b.x, b.y)#X is <__main__.descriptor object at 0x10432c250>, Y is 0
print "Y is %s"%b.y.__get__(b)  #需要自己调用__get__方法,解释器不会自己调用的

从上述代码可知,

  1. 访问类层次上的描述符会自动调用相应的描述符方法;
  2. 访问实例层次上的描述符只会返回描述符对象自身,并不会调用相应的描述符方法;

(实例属性的描述符不会自动调用描述符方法,这么做肯定是有原因的吧…有知道的大神求指导…)

**描述符是所有实例共享的,让不同实例保存的值互不影响

class descriptor(object):
 def __init__(self, default):
  self.value = default

 def __get__(self, instance, owner):
  return self.value

 def __set__(self, instance, value):
  self.value = value

class Foo(object):
 bar = descriptor(5)

bar是类属性,所有Foo的实例共享bar。

f = Foo()
g = Foo()
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 5 g.bar is 5
f.bar = 10          #调用__set__函数
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 10 g.bar is 10

当实例修改了描述符以后,会影响到其他实例,有没有一种方法可以让实例之间互不影响呢?

数据字典法

​ 在descriptor中使用数据字典,由__get____set__的第一个参数来确定是哪个实例,使用实例作为字典的key,为每一个实例单独保存一份数据,修改代码如下…

from weakref import WeakKeyDictionary
class descriptor(object):
 def __init__(self, default):
  self.default = default
  self.data = WeakKeyDictionary()

 def __get__(self, instance, owner):# instance = x,owner = type(x)
  # we get here when someone calls x.d, and d is a descriptor instance
  return self.data.get(instance, self.default)

 def __set__(self, instance, value):
  # we get here when someone calls x.d = val, and d is a descriptor instance
  self.data[instance] = value

class Foo(object):
 bar = descriptor(5)

f = Foo()
g = Foo()
print "f.bar is %s g.bar is %s" % (f.bar, g.bar) #f.bar is 5 g.bar is 5
print "Setting f.bar to 10"
f.bar = 10
print "f.bar is %s\ng.bar is %s" % (f.bar, g.bar) ##f.bar is 10 g.bar is 5

上述方法虽然可行,但是存在缺陷。

descriptor使用了一个字典来保存不同实例的数据。一般来说是不会出现问题,但是如果实例为不可哈希对象,如list,上述方法就会出现问题,因为不可哈希对象不能作为键值。

标签法

说白了就是给实例增加一个与描述符同名的实例属性,利用该实例属性来保存该实例描述符的值,描述符相当于一个中间操作,描述符的__get__()返回实例属性,__set__也是对实例属性操作。

此方法的实现原理: 数据描述符的访问优先级比实例属性高…

还是见图和代码吧,代码最直观…

Python descriptor(描述符)的实现

class descriptor(object):
 def __init__(self, label):#label为给实例增加的实例属性名
  self.label = label
 def __get__(self, instance, owner):
  #dict.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.
  return instance.__dict__.get(self.label) #获取与描述符同名的实例属性的值 

 def __set__(self, instance, value):
  #注意这里,要这么写,不能写instance.x = val这种形式,这样会形成自身的循环调用
  instance.__dict__[self.label] = value  #修改与描述符同名的实例属性的值 

class Foo(list):
 x = descriptor('x') #注意这个初始化值为要给实例增加的实例属性名,要和描述符对象同名。
 y = descriptor('y')

f1 = Foo()
f2 = Foo()
print f1.__dict__ #{}
print f1.x,f2.x,f1.y,f2.y#None None None None,此时尚未增加实例属性,需要调用__set__方法建立一个与描述符同名的实例属性
#print Foo.__dict__
f1.x = 1
f1.y = 2
f2.x = 3
f2.y = 4   
print f1.__dict__ #{'y': 2, 'x': 1} #增加了的实例属性
print f1.x,f1.y,f2.x,f2.y #1 2 3 4

因为只有调用了__set__函数才会建立一个与描述符同名的实例属性,所以可以在__init__()函数中对描述符赋值。

class Foo(list):
 x = descriptor('x')
 y = descriptor('y')
 def __init__(self):
  self.x = 1 #调用的是描述符的__set__方法,与描述符同名的实例属性增加完毕....
  self.y = 2
f = Foo()
print f.x,f.y

注意事项:

给描述符添加标签时,初始化值要和描述符的变量名相同,比如name = descriptor(‘name'),因为这个初始化值是给实例增加的实例属性名,必须要和描述符对象同名。

下面为错误示范,初始化值和描述符不同名的情况。

class descriptor(object):
 def __init__(self, label):
  self.label = label
 def __get__(self, instance, owner):
  return instance.__dict__.get(self.label) 

 def __set__(self, instance, value):
  if not isinstance(value,int):
   raise ValueError('must be int')
  instance.__dict__[self.label] = value 

class Foo(object):
 x = descriptor('y') #应该改为Descriptor('x'),与实例同名
 def __init__(self,val = 0):
  self.x = val

f = Foo()
print f.x #0
f.y = 'a' #绕过了描述符的__set__方法...未进行类型检查,此时x为非法值啊,是不是很坑...
print f.x #a

潜在坑…正常使用时也会带来坑。

class Foo(object):
 x = descriptor('x')
 def __init__(self,val = 0):
  self.x = val

f = Foo()
f.x = 'a'#ValueError: must be int

好像没毛病啊…接着往下看。

f.__dict__['x'] = 'a'
print f.x #a,还是绕过了__set__方法,未进行类型检查,还是非法值啊...哈哈- -

小结

​ 查了很多资料,标签法用的较多,但是标签法也有一定的缺陷,目前没有找到更好的方法解决上述问题,如有更好的方法,求指导,谢谢…

描述符的应用…

​ 描述符也是类,可以为其增加方法。比如增加回调方法,描述符是一个用来回调的很好的手段。比如想要一个类的某个状态发生变化就立刻通知我们,可以自定义回调函数用来响应类中的状态变化。如以下代码,

from weakref import WeakKeyDictionary
class CallbackProperty(object):
  def __init__(self, default=None):
    self.data = WeakKeyDictionary()
    self.default = default
    self.callbacks = WeakKeyDictionary()

  def __get__(self, instance, owner):
    if instance is None:
      return self    
    return self.data.get(instance, self.default)

  def __set__(self, instance, value):#每次改变值的时候都会调用low_balance_warning函数
    for callback in self.callbacks.get(instance, []):
      # alert callback function of new value
      callback(value)
    self.data[instance] = value

  def add_callback(self, instance, callback):
    """Add a new function to call everytime the descriptor within instance updates"""
    if instance not in self.callbacks:
      self.callbacks[instance] = [] #实例->[方法,]
    self.callbacks[instance].append(callback)

class BankAccount(object):
  balance = CallbackProperty(0)

def low_balance_warning(value):
  if value < 100:
    print "You are now poor"
  else:
    print 'You are now rich!!!'

def check(value):
  print 'You have %s money, Good Luck!!!'%value

ba = BankAccount()
BankAccount.balance.add_callback(ba, low_balance_warning)
BankAccount.balance.add_callback(ba, check)

ba.balance = 5000          # You are now rich!!! You have 5000 money, Good Luck!!!
print "Balance is %s" % ba.balance  # Balance is 5000
ba.balance = 99           # You are now poor  You have 99 money, Good Luck!!!
print "Balance is %s" % ba.balance  # Balance is 99

有木有感觉很厉害…__set__()方法感觉就像一个监督人员,监视属性的一举一动。

​ 描述符还有其他用处,如格式检查,类型检查,设置只读变量等。设置一个只读变量的话,只要不让变量再赋值就好了,即调用__set__()函数时触发异常即可,这也是问题2的答案。

class descriptor(object):
  def __init__(self,val):
    self.val = val

  def __get__(self, obj,type = None):
    return self.val

  def __set__(self, obj, value):
    raise Exception('read only')

class Foo(object):
  d = descriptor(1)

d = Foo()
print d.d #1
d.d = 2  #触发异常,read only

参考网址

1.https://docs.python.org/2/howto/descriptor.html#definition-and-introduction
2.https://hg.python.org/cpython/file/2.7/Objects/object.c
3.http://svn.python.org/view/python/trunk/Objects/classobject.c?view=markup
4.http://www.geekfan.net/7862/
5.http://pyzh.readthedocs.io/en/latest/Descriptor-HOW-TO-Guide.html
6.//3water.com/article/62987.htm
7.//3water.com/article/97741.htm
8.http://www.tuicool.com/articles/yYJbqun

到此这篇关于Python descriptor(描述符)的实现的文章就介绍到这了,更多相关Python descriptor内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Python 相关文章推荐
Pyramid将models.py文件的内容分布到多个文件的方法
Nov 27 Python
在Python中使用PIL模块对图片进行高斯模糊处理的教程
May 05 Python
python获取当前时间对应unix时间戳的方法
May 15 Python
python 制作自定义包并安装到系统目录的方法
Oct 27 Python
Django中在xadmin中集成DjangoUeditor过程详解
Jul 24 Python
解决django-xadmin列表页filter关联对象搜索问题
Nov 15 Python
python实现图片插入文字
Nov 26 Python
Python 实现opencv所使用的图片格式与 base64 转换
Jan 09 Python
python中spy++的使用超详细教程
Jan 29 Python
python pyhs2 的安装操作
Apr 07 Python
python读取并查看npz/npy文件数据以及数据显示方法
Apr 14 Python
Python如何让字典保持有序排列
Apr 29 Python
基于OpenCV的网络实时视频流传输的实现
Nov 15 #Python
彻底解决Python包下载慢问题
Nov 15 #Python
Python eval函数原理及用法解析
Nov 14 #Python
Django怎么在admin后台注册数据库表
Nov 14 #Python
通过实例解析python and和or使用方法
Nov 14 #Python
Python高并发和多线程有什么关系
Nov 14 #Python
Django跨域请求原理及实现代码
Nov 14 #Python
You might like
《PHP边学边教》(02.Apache+PHP环境配置――下篇)
2006/12/13 PHP
ThinkPHP 404页面的设置方法
2015/01/14 PHP
PHP 数组黑名单/白名单实例代码详解
2019/06/04 PHP
基于Jquery的实现回车键Enter切换焦点
2010/09/14 Javascript
JavaScript控制Session操作方法
2013/01/17 Javascript
css transform 3D幻灯片特效实现步骤解读
2013/03/27 Javascript
Jquery使用Firefox FireBug插件调试Ajax步骤讲解
2013/12/02 Javascript
JavaScript实现随机替换图片的方法
2015/04/16 Javascript
在JavaScript的jQuery库中操作AJAX的方法讲解
2015/08/15 Javascript
jQuery插件实现无缝滚动特效
2015/11/24 Javascript
javascript 继承学习心得总结
2016/03/17 Javascript
js事件驱动机制 浏览器兼容处理方法
2016/07/23 Javascript
用nodejs的实现原理和搭建服务器(动态)
2016/08/10 NodeJs
vue+vuex+axios+echarts画一个动态更新的中国地图的方法
2017/12/19 Javascript
jQuery+SpringMVC中的复选框选择与传值实例
2018/01/08 jQuery
解决小程序无法触发SESSION问题
2020/02/03 Javascript
vue中使用腾讯云Im的示例
2020/10/23 Javascript
[42:32]Secret vs Optic 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
python函数参数*args**kwargs用法实例
2013/12/04 Python
python使用matplotlib绘图时图例显示问题的解决
2017/04/27 Python
Selenium元素的常用操作方法分析
2018/08/10 Python
Python2.7实现多进程下开发多线程示例
2019/05/31 Python
python multiprocessing多进程变量共享与加锁的实现
2019/10/02 Python
Django通用类视图实现忘记密码重置密码功能示例
2019/12/17 Python
在Anaconda3下使用清华镜像源安装TensorFlow(CPU版)
2020/04/19 Python
python 实现socket服务端并发的四种方式
2020/12/14 Python
旅游与酒店管理的自我评价分享
2013/11/03 职场文书
总监职责范文
2013/11/09 职场文书
高等教育学自荐书范文
2014/02/10 职场文书
初中生期末评语大全
2014/04/24 职场文书
幼儿教师求职信
2014/05/24 职场文书
廉洁校园实施方案
2014/05/25 职场文书
公司离职证明标准格式
2014/11/18 职场文书
故意杀人案辩护词
2015/05/21 职场文书
女性励志书籍推荐
2019/08/19 职场文书
python b站视频下载的五种版本
2021/05/27 Python