一等公民指的是 Python 的函数可以动态建立,能赋值给别的变量,能做为参传给函数,也能做为函数的返回值。总而言之,函数和普通变量并无什么区别。python
函数是一等公民,这是函数式编程的基础,然而 Python 中基本上不会使用 lambda 表达式,由于在 lambda 表达式的中仅能使用单纯的表达式,不能赋值,不能使用 while、try 等语句,所以 lambda 表达式要么难以阅读,要么根本没法写出。这极大的限制了 lambda 表达式的使用场景。程序员
上文说过,函数和普通变量没什么区别,但普通变量并非函数,由于这些变量没法调用。但若是某个类实现了 __call__
这个魔术方法,这个类的实例就均可以像函数同样被调用:编程
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' def __call__(self): print(self) def __str__(self): return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}'.format(user=self) p = Person() p() # 等价于 print(p)
对于熟悉 C 系列语言的人来讲,函数传参的方式一目了然。默认是拷贝传值,若是传指针是引用传值。咱们先来看一段简单的 Python 代码:swift
def foo(arg): arg = 5 print(arg) a = 1 foo(a) print(a) # 输出 5 和 1
这段代码的结果符合咱们的预期,从这段代码来看,Python 也属于拷贝传值。但若是再看这段代码:设计模式
def foo(arg): arg.append(1) print(arg) a = [1] foo(a) print(a) # 输出两个 [1, 1]
你会发现参数数组在函数内部被改变了。就像是 C 语言中传递了变量的指针同样。因此 Python 究竟是拷贝传值仍是引用传值呢?答案都是否认的!
Python 的传值方式能够被理解为混合传值。对于那些不可变的对象(好比 1.1.2 节中介绍过的元组,还有数字、字符串类型),传值方式是拷贝传值;对于那些可变对象(好比数组和字典)则是引用传值。数组
Python 的函数能够有默认值,这个功能很好用:缓存
def foo(a, l=[]): l.append(a) return l foo(2,[1]) # 给数组 [1] 添加一个元素 2,获得 [1,2] foo(2) # 没有传入数组,使用默认的空数组,获得 [2]
然而若是这样调用:闭包
foo(2) # 利用默认参数,获得 [2] foo(3) # 居然获得了 [2, 3]
函数调用了两次之后,默认参数被改变了,也就是说函数调用产生了反作用。这是由于默认参数的存储并不像函数里的临时变量同样存储在栈上、随着函数调用结束而释放,而是存储在函数这个对象的内部:app
foo.__defaults__ # 一开始确实是空数组 foo(2) # 利用默认参数,获得 [2] foo.__defaults__ # 若是打印出来看,已经变成 [2] 了 foo(3) # 再添加一个元素就获得了 [2, 3]
由于函数 foo
做为一个对象,不会被释放,所以这个对象内部的属性也不会随着屡次调用而自动重置,会一直保持上次发生的变化。基于这个前提,咱们得出一个结论:函数的默认参数不容许是可变对象,好比这里的 foo
函数须要这么写:框架
def foo(a, l=None): if l is None: l = [] l.append(a) return l print(foo(2)) # 获得 [2] print(foo(3)) # 获得 [3]
如今,给参数添加默认值的行为在函数体中完成,不会随着函数的屡次调用而累积。
对于 Python 的默认参数来讲:
若是默认值是不可变的,能够直接设置默认值,不然要设置为 None 并在函数体中设置默认值。
当参数个数不肯定时,能够在参数名前加一个 *: def foo(*args): print(args) foo(1, 2, 3) # 输出 [1, 2, 3] 若是直接把数组做为参数传入,它实际上是单个参数,若是要把数组中全部元素都做为单独的参数传入,则在数组前面加上 *: a = [1, 2, 3] foo(a) # 会输出 ([1,2,3], ) 由于只传了一个数组做为参数 foo(*a) # 输出 [1, 2, 3] 这里的单个 * 只能接收非关键字参数,也就是仅有参数值的哪些参数。若是想接受关键字参数,须要用 ** 来表示: def foo(*args, **kwargs): print(args) print(kwargs) foo(1,2,3, a=61, b=62) # 第一行输出:[1, 2, 3] # 第二行输出:{'a': 61, 'b': 62} 相似的,字典变量传入函数只能做为单个参数,若是要想展开并被 **kwargs 识别,须要在字典前面加上两个星号 **: a = [1, 2, 3] d = {'a': 61, 'b': 62} foo(*a, **d)
2.2.4 参数分类
Python 中函数的参数能够分为两大类:
定位参数(Positional):表示参数的位置是固定的。好比对于函数 foo(a, b) 来讲,foo(1, 2) 和 foo(2, 1) 就是大相径庭的,a 和
b 的位置是固定的,不可随意调换。 关键词参数(Keyword):表示参数的位置不重要,可是参数名称很重要。好比 foo(a = 1, b = 2) 和 foo(b = 2, a = 1) 的含义相同。
有一种参数叫作仅限关键字(Keyword-Only)参数,好比考虑这个函数:
def foo(*args, n=1, **kwargs): print(n)
这个函数在调用时,若是参数 n 不指定名字,就会被前面的 *args
处理掉,若是指定的名字不是 n,又会被后面的 **kwargs
处理掉,因此参数 n 必须精确的以 (n = xxx)
的形式出现,也就是 Keyworld-Only。
在 2.2.2 节中,咱们查看了函数变量的 __defaults__
属性,其实这就是一种内省,也就是在运行时动态的查看变量的信息。
前文说过,函数也是对象,所以函数的变量个数,变量类型都应该有办法获取到,若是你须要开发一个框架,也许会对函数有各类奇葩的检查和校验。
如下面这个函数为例: g = 1 def foo(m, *args, n, **kwargs): a = 1 b = 2 首先能够获取函数名,函数所在模块的全局变量等: foo.__globals__ # 全局变量,包含了 g = 1 foo.__name__ # foo 咱们还能够看到函数的参数,函数内部的局部变量: foo.__code__.co_varnames # ('m', 'n', 'args', 'kwargs', 'a', 'b') foo.__code__.co_argcount # 只计算参数个数,不考虑可变参数,因此获得 2 或者用 inspect 模块来查看更详细的信息: import inspect sig = inspect.signature(foo) # 获取函数签名 sig.parameters['m'].kind # POSITIONAL_OR_KEYWORD 表示能够是定位参数或关键字参数 sig.parameters['args'].kind # VAR_POSITIONAL 定位参数构成的数组 sig.parameters['n'].kind # KEYWORD_ONLY 仅限关键字参数 sig.parameters['kwargs'].kind # VAR_KEYWORD 关键字参数构成的字典 inspect.getfullargspec(foo) # 获得:ArgSpec(args=['m', 'n'], varargs='args', keywords='kwargs', defaults=None)
本节的新 API 比较多,但并不要求记住这些 API 的用法。再次强调,本文的写做目的是为了创建读者对 Python 的整体
认知,了解 Python 能作什么,至于怎么作,那是文档该作的事。
经典的设计模式有 23 个,虽然设计模式都是经常使用代码的总结,理论上来讲与语法无关。但不得不认可的是,标准的设计模
式在不一样的语言中,有的由于语法的限制根本没法轻易实现(好比在 C 语言中实现组合模式),有的则由于语言的特定功能
,变得冗余啰嗦。
以策略模式为例,有一个抽象的策略类,定义了策略的接口,而后使用者选择一个具体的策略类,构造他们的实例而且调
用策略方法。具体代码能够参考:策略模式在百度百科的定义。
然而这些对象自己并无做用,它们仅仅是能够调用相同的方法而已,只不过在 Java 中,全部的任务都须要由对象来完成。
即便策略自己就是一个函数,但也必须把它包裹在一个策略对象中。因此在 Python 中更优雅写法是直接把策略函数做为变量使用。
不过这就引入一个问题,如何判断某个函数是个策略呢,毕竟在面向对象的写法中,只要检查它的父类是不是抽象的策略类便可。
也许你已经见过相似的写法:
strategy def strategyA(n): print(n * 2)
下面就开始介绍装饰器。
首先,装饰器是个函数,它的参数是被装饰的函数,返回值也是一个函数: def decorate(origin_func): # 这个参数是被装饰的函数 print(1) # 先输出点东西 return origin_func # 把原函数直接返回 @decorate # 注意这里不是函数调用,因此不用加括号,也不用加被修饰的函数名 def sayHello(): print('Hello') sayHello() # 若是没有装饰器,只会打印 'Hello',实际结果是打印 1 再打印 'Hello' 所以,使用装饰器的这种写法: @decorate def foo(): pass 和下面这种写法是彻底等价的, 初学者能够把装饰器在心中默默的转换成下一种写法,以方便理解: def foo(): pass foo = decorate(foo)
须要注意的是,装饰器函数 decorate
在模块被导入时就会执行,而被装饰的函数只在被调用时才会执行,也就是说即便不调用 sayHello
函数也会输出 1,但这样就不会输出 Hello 了。
有了装饰器,配合前面介绍的函数对象,函数内省,咱们能够作不少有意思的事,至少判断上一节中某个函数是不是策
略是很是容易的。在装饰器中,咱们还能够把策略函数都保存到数组中, 而后提供一个“推荐最佳策略”的功能, 其实就
是遍历执行全部的策略,而后选择最好的结果。
上一节中的装饰器主要是为了介绍工做原理,它的功能很是简单,并不会改变被装饰函数的运行结果,仅仅是在导入时
装饰函数,而后输出一些内容。换句话说,即便不执行函数,也要执行装饰器中的 print
语句,并且由于直接返回函数
的缘故,其实没有真正的起到装饰的效果。
如何作到装饰时不输出任何内容,仅在函数执行最初输出一些东西呢?这是常见的 AOP(面向切片编程) 的需求。这就
要求咱们不能再直接返回被装饰的函数,而是应该返回一个新的函数,因此新的装饰器须要这么写:
def decorate(origin_func): def new_func(): print(1) origin_func() return new_func @decorate def sayHello(): print('Hello') sayHello() # 运行结果不变,可是仅在调用函数 sayHello 时才会输出 1
这个例子的工做原理是,sayHello
函数做为参数 origin_func
被传到装饰器中,通过装饰之后,它实际上变成了 new_func
,会先输出 1 再执行原来的函数,也就是 sayHello
。
这个例子很简陋,由于咱们知道了 sayHello
函数没有参数,因此才能定义一个一样没有参数的替代者:nwe_func
。若是咱们在开发一个框架,要求装饰器能对任意函数生效,就须要用到 2.2.3 中介绍的 *
和 **
这种不定参数语法了。
若是查看 sayHello 函数的名字,获得的结果将是 new_func: sayHello.__name__ # new_func 这是很天然的,由于本质上其实执行的是: new_func = decorate(sayHello) 而装饰器的返回结果是另外一个函数 new_func,二者仅仅是运行结果相似,但两个对象并无什么关联。 因此为了处理不定参数,而且不改变被装饰函数的外观(好比函数名),咱们须要作一些细微的修补工做。这些工做都是模板代码,因此 Python 早就提供了封装: import functools def decorate(origin_func): @functools.wraps(origin_func) # 这是 Python 内置的装饰器 def new_func(*args, **kwargs): print(1) origin_func(*args, **kwargs) return new_func
在 2.4.2 节的代码注释中我解释过,装饰器后面不要加括号,被装饰的函数自动做为参数,传递到装饰器函数中。若是加了
括号和参数,就变成手动调用装饰器函数了,大多数时候这与预期不符(由于装饰器的参数通常都是被装饰的函数)。
不过装饰器能够接受自定义的参数,而后返回另外一个装饰器,这样外面的装饰器实际上就是一个装饰器工厂,能够根据用户
的参数,生成不一样的装饰器。仍是以上面的装饰器为例,我但愿输出的内容不是固定的 1,而是用户能够指定的,代码就应该这么写:
import functools def decorate(content): # 这实际上是一个装饰器工厂 def real_decorator(origin_func): # 这才是刚刚的装饰器 @functools.wraps(origin_func) def new_func(): print('You said ' + str(content)) # 如今输出内容能够由用户指定 origin_func() return new_func # 在装饰器里,返回的是新的函数 return real_decorator # 装饰器工厂返回的是装饰器 装饰器工厂和装饰器的区别在于它能够接受参数,返回一个装饰器: @decorate(2017) def sayHello(): print('Hello') sayHello() 其实等价于: real_decorator = decorate(2017) # 经过装饰器工厂生成装饰器 new_func = real_decorator(sayHello) # 正常的装饰器工做逻辑 new_func() # 调用的是装饰过的函数
C 语言中咱们定义变量用到的语法是:
int a = 1;
这背后的含义是定义了一个 int
类型的变量 a
,至关于申请了一个名为 a
的盒子(存储空间),里面装了数字 1。
而后咱们改变 a
的值:a = 2;
,能够打印 a
的地址来证实它并无发生变化。因此只是盒子里装的内容(指针指向的位置)
发生了改变:
可是在 Python 中,变量不是盒子。好比一样的定义变量:
a = 1
这里就不能把 a
理解为 int
类型的变量了。由于在 Python 中,变量没有类型,值才有,或者说只有对象才有类型。
由于即便是数字 1,也是 int
类的实例,而变量 a
更像是给这个对象贴的一个标签。
若是执行赋值语句 a = 2
,至关于把标签 a 贴在另外一个对象上:
基于这个认知,咱们如今应该更容易理解 2.2.1 节中所说的函数传参规则了。若是传入的是不可变类型,好比 int
,改变它的值实际上就是把标签挂在新的对象上,天然不会改变原来的参数。若是是可变类型,而且作了修改,那么函数中的变量和外面的变量都是指向同一个对象的标签,因此会共享变化。
根据上一节的描述,直接把变量赋值给另外一个变量, 还算不上复制: a = [1, 2, 3] b = a b == a # True,等同性校验,会调用 __eq__ 函数,这里只判断内容是否相等 b is a # True,一致性校验,会检查是不是同一个对象,调用 hash() 函数,能够理解为比较指针 可见不只仅数组相同,就连变量也是相同的,能够把 b 理解为 a 的别名。 若是用切片,或者数组的构造函数来建立新的数组,获得的是原数组的浅拷贝: a = [1, 2, 3] b = list(a) b == a # True,由于数组内容相同 b is a # False,如今 a 和 b 是两个变量,刚好指向同一个数组对象 但若是数组中的元素是可变的,能够看到这些元素并无被彻底拷贝: a = [[1], [2], [3]] b = list(a) b[0].append(2) a # 获得 [[1, 2], [2], [3]],由于 a[0] 和 b[0] 其实仍是挂在相同对象上的不一样标签 若是想要深拷贝,须要使用 copy 模块的 deepcopy 函数: import copy b = copy.deepcopy(a) b[0].append(2) a # 变成了 [[1, 2], [2], [3]] a # 仍是 [[1], [2], [3]]
此时,不只仅是每一个元素的引用被拷贝,就连每一个元素本身也被拷贝。因此如今的 a[0]
和 b[0]
是指向两个不一样对象的两个不一样变量(标签),天然就互不干扰了。
若是要实现自定义对象的深复制,只要实现 __deepcopy__
函数便可。这个概念在几乎全部面向对象的语言中都会存在,就不详细介绍了。
Python 内存管理使用垃圾回收的方式,当没有指向对象的引用时,对象就会被回收。然而对象一直被持有也并不是什
么好事,好比咱们要实现一个缓存,预期目标是缓存中的内容随着真正对象的存在而存在,随着真正对象的消失而
消失。若是由于缓存的存在,致使被缓存的对象没法释放,就会致使内存泄漏。
Python 提供了语言级别的支持,咱们可使用 weakref
模块,它提供了 weakref.WeakValueDictionary
这
个弱引用字典来确保字典中的值不会被引用。若是想要获取某个对象的弱引用,可使用 weakref.ref(obj)
函数。
静态函数其实和类的方法没什么关系,它只是刚好定义在类的内部而已,因此这里我用函数(function) 来形容它。它能够没有参数: class Person: @staticmethod # 用 staticmethod 这个修饰器来代表函数是静态的 def sayHello(): print('Hello') Person.sayHello() # 输出 'Hello` 静态函数的调用方式是类名加上函数名。类方法的调用方式也是这样,惟一的不一样是须要用 @staticmethod 修饰器,并且方法的第一个参数必须是类: class Person: @classmethod # 用 classmethod 这个修饰器来代表这是一个类方法 def sayHi(cls): print('Hi: ' + cls.__name__) Person.sayHi() # 输出 'Hi: Person`
类方法和静态函数的调用方法一致,在定义时除了修饰器不同,惟一的区别就是类方法须要多声明一个参数。
这样看起来比较麻烦,但静态函数没法引用到类对象,天然就没法访问类的任何属性。
因而问题来了,静态函数有何意义呢?有的人说类名能够提供命名空间的概念,但在我看来这种解释并不成立,
由于每一个 Python 文件均可以做为模块被别的模块引用,把静态函数从类里抽取出来,定义成全局函数,也是有命名空间的:
# 在 module1.py 文件中: def global(): pass class Util: @staticmethod def helper(): pass # 在 module2.py 文件中: import module1 module1.global() # 调用全局函数 module1.Util.helper() # 调用静态函数
从这个角度看,定义在类中的静态函数不只不具有命名空间的优势,甚至调用语法还更加啰嗦。对此,个人理解是:静
态函数能够被继承、重写,但全局函数不行,因为 Python 中的函数是一等公民,所以不少时候用函数替代类都会使代码
更加简洁,但缺点就是没法继承,后面还会有更多这样的例子。
Python (等多数动态语言)中的类并不像 C/OC/Java 这些静态语言同样,须要预先定义属性。咱们能够直接在初始化
函数中建立属性:
class Person: def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 值是 'bestswifter' 因为 __init__ 函数是运行时调用的,因此咱们能够直接给对象添加属性: bs.age = 22 bs.age # 由于刚刚赋值了,因此如今取到的值是 22
若是访问一个不存在的属性,将会抛出异常。从以上特性来看,对象其实和字典很是类似,但这种过于灵活的特性其实蕴含了潜在的
风险。好比某个封装好的父类中定义了许多属性, 可是子类的使用者并不必定清楚这一点,他们极可能会不当心就重写了父类的属性
。一种隐藏并保护属性的方式是在属性前面加上两个下划线:
class Person: def __init__(self): self.__name = 'bestswifter' bs = Person() bs.__name # 这样是没法获取属性的 bs._Person__name # 这样仍是能够读取属性 这是由于 Python 会自动处理以双下划线开头的属性,把他们重名为 _Classname__attrname 的格式。因为 Python
对象的全部属性都保存在实例的 __dict__ 属性中,咱们能够验证一下: bs = Person() bs.__dict__ # 获得 {'_Person__name': 'bestswifter'}
但不少人并不承认经过名称改写(name mangling) 的方式来存储私有属性,缘由很简单,只要知道改写规则,依然很容易的就能读写
私有属性。与其自欺欺人,不如采用更简单,更通用的方法,好比给私有属性前面加上单个下划线 _
。
注意,以单个下划线开头的属性不会触发任何操做,彻底靠自觉与共识。任何稍有追求的 Python 程序员,都不该该读写这些属性。
使用过别的面向对象语言的读者应该都清楚属性的 getter
和 setter
函数的重要性。它们封装了属性的读写操做,
能够添加一些额外的逻辑,好比校验新值,返回属性前作一些修饰等等。最简陋的 getter
和 setter
就是两个普通函数:
class Person: def get_name(self): return self.name.upper() def set_name(self, new_name): if isinstance(new_name, str): self.name = new_name.lower() def __init__(self, name): self.name = name bs = Person('bestswifter') bs.get_name() # 获得大写的名字: 'BESTSWIFTER' bs.set_name(1) # 因为新的名字不是字符串,因此没法赋值 bs.get_name() # 仍是老的名字: 'BESTSWIFTER' 工做虽然完成了,但方法并不高明。在 1.2.3 节中咱们就见识到了 Python 的一个特色:“内部高度封装,彻底对外透明”。
这里手动调用 getter 和 setter 方法显得有些愚蠢、啰嗦,好比对比下面的两种写法,在变量名和函数名很长的状况下,差距会更大: bs.name += '1995' bs.set_name(bs.get_name() + '1995') Python 提供了 @property 关键字来装饰 getter 和 setter 方法,这样的好处是能够直接使用点语法,了解 Objective-C
的读者对这一特性必定倍感亲切: class Person: @property # 定义 getter def name(self): # 函数名就是点语法访问的属性名 return self._name.upper() # 如今真正的属性是 _name 了 @name.setter # 定义 setter def name(self, new_name): # 函数名不变 if isinstance(new_name, str): self._name = new_name.lower() # 把值存到私有属性 _name 里 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 其实调用了 name 函数,获得大写的名字: 'BESTSWIFTER' bs.name = 1 # 其实调用了 name 函数,由于类型不符,没法赋值 bs.name # 仍是老的名字: 'BESTSWIFTER' 咱们已经在 2.4 节详细学习了装饰器,应该能意识到这里的 @property 和 @xxx.setter 都是装饰器。所以上述写法实际上等价于: class Person: def get_name(self): return self._name.upper() def set_name(self, new_name): if isinstance(new_name, str): self._name = new_name.lower() # 以上是老旧的 getter 和 setter 定义 # 若是不用 @property,能够定义一个 property 类的实例 name = property(get_name, set_name) 可见,特性的本质是给类建立了一个类属性,它是 property 类的实例,构造方法中须要把 getter、setter 等函数传入,
咱们能够打印一下类的 name 属性来证实: Person.name # <property object at 0x107c99868> 理解特性的工做原理相当重要。以这里的 name 特性为例,咱们访问了对象的 name 属性,可是它并不存在,因此会尝试访问类
的 name 属性,这个属性是 property 类的实例,会对读写操做作特殊处理。这也意味着,若是咱们重写了类的 name 属性,
那么对象的读写方法就不会生效了: bs = Person() Person.name = 'hello' bs.name # 实例并无 name 属性,所以会访问到类的属性 name,如今的值是 'hello` 了 若是访问不存在的属性,默认会抛出异常,但若是实现了 __getattr__ 函数,还有一次挽救的机会: class Person: def __getattr__(self, attr): return 0 def __init__(self, name): self.name = name bs = Person('bestswifter') bs.name # 直接访问属性 bs.age # 获得 0,这是 __getattr__ 方法提供的默认值 bs.age = 1 # 动态给属性赋值 bs.age # 获得 1,注意!!!这时候就不会再调用 __getattr__ 方法了 因为 __getattr__ 只是兜底策略,处理一些异常状况,并不是每次都能被调用,因此不能把重要的业务逻辑写在这个方法中。
在上一节中,咱们利用特性来封装 getter
和 setter
,对外暴露统一的读写接口。但有些 getter
和 setter
的逻辑实际上是能够复用的,好比商品的价格和剩余数量在赋值时,都必须是大于 0 的数字。这时候若是每次都要写一遍 setter
,代码就显得很冗余,因此咱们须要一个能批量生产特性的函数。因为咱们已经知道了特性是 property
类的实例,并且是类的属性,因此代码能够这样写:
def quantity(storage_name): # 定义 getter 和 setter def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: # 把值保存在实例的 __dict__ 字典中 instance.__dict__[storage_name] = value else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter) # 返回 property 的实例
有了这个特性工厂,咱们能够这样来定义特性:
class Item: price = quantity('price') number = quantity('number') def __init__(self): pass i = Item() i.price = -1 # Traceback (most recent call last): # ... # ValueError: value must be > 0
做为追求简洁的程序员,咱们不由会问,在 price = quantity('price')
这行代码中,属性名重复了两次,能不能在 quantity
函数中自动读取左边的属性名呢,这样代码就能够简化成 price = quantity()
了。
答案显然是否认的,由于右边的函数先被调用,而后才能把结果赋值给左边的变量。不过咱们能够采用迂回策略,变相的实现上面的需求:
def quantity(): try: quantity.count += 1 except AttributeError: quantity.count = 0 storage_name = '_{}:{}'.format('quantity', quantity.count) def qty_getter(instance): return instance.__dict__[storage_name] def qty_setter(instance, value): if value > 0: instance.__dict__[storage_name] = value else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter)
这段代码中咱们利用了两个技巧。首先函数是一等公民, 因此函数也是对象,天然就有属性。因此咱们利用 try ... except
很容易的就给函数工厂添加了一个计数器对象 count
,它每次调用都会增长,而后再拼接成存储时用的键 storage_name
,而且能够保证不一样 property
实例的存储键名各不相同。
其次,storage_name
在 getter
和 setter
函数中都被引用到,而这两个函数又被 property
的实例引用,因此 storage_name
会由于被持有而延长生命周期。这也正是闭包的一大特性:可以捕获自由变量并延长它的生命周期和做用域。
咱们来验证一下:
class Item: price = quantity() number = quantity() def __init__(self): pass i = Item() i.price = 1 i.number = 2 i.price # 获得 1,能够正常访问 i.number # 获得 2,能够正常访问 i.__dict__ # {'_quantity:0': 1, '_quantity:1': 2}
可见如今存储的键名能够被正确地自动生成。
文件描述符的做用和特性工厂同样,都是为了批量的应用特性。它的写法也和特性工厂很是相似:
class Quantity: def __init__(self, storage_name): self.storage = storage_name def __get__(self, instance, owner): return instance.__dict__[self.storage] def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage] = value else: raise ValueError('value must be > 0')
主要有如下几个改动:
property
类的实例了,所以 getter
和 setter
方法的名字是固定的,这样才能知足协议。__get__
方法的第一个参数是描述符类 Quantity
的实例,第二个参数 self
是要读取属性的实例,好比上面的 i
,也被称做托管实例。第三个参数是托管类,也就是 Item
。__set__
方法的前两个参数含义相似,第三个则是要读取的属性名,好比 price
。和特性工厂相似,属性描述符也能够实现 storage_name
的自动生成,这里就不重复代码了。看起来属性描述符和特性工厂几乎同样,但因为属性描述符是类,它就能够继承。好比这里的 Quantity
描述符有两个功能:自动存储和值的校验。自动存储是一个很是通用的逻辑,而值的校验是可变的业务逻辑,因此咱们能够先定义一个 AutoStorage
描述符来实现自动存储功能,而后留下一个空的 validate
函数交给子类去重写。
而特性工厂做为函数,天然就没有上述功能,这二者的区别相似于 3.2.1 节中介绍的静态函数与全局函数的区别。
咱们知道类的属性都会存储在 __dict__
字典中,即便没有显式的给属性赋值,但只要字典里面有这个字段,也是能够读取到的:
class Person: pass p = Person() p.__dict__['name'] = 'bestswifter' p.name # 不会报错,而是返回字典中的值,'bestswifter'
但咱们在特性工厂和属性描述符的实现中,都是直接把属性的值存储在 __dict__
中,并且键就是属性名。以前咱们还介绍过,特性的工做原理是没有直接访问实例的属性,而是读取了 property
的实例。那直接把值存在 __dict__
中,会不会致使特性失效,直接访问到原始内容呢?从以前的实践结果来看,答案是否认的,要解释这个问题,咱们须要搞明白访问实例属性的查找顺序。
假设有这么一段代码:
o = cls() # 假设 o 是 cls 类的实例
o.attr # 试图访问 o 的属性 attr
再对上一节中的属性描述符作一个简单的分类:
覆盖型描述符:定义了 __set__ 方法的描述符
非覆盖型描述符:没有定义 __set__ 方法的描述符
在执行 o.attr 时,查找顺序以下:
若是 attr 出如今 cls 或父类的 __dict__ 中,且 attr 是覆盖型描述符,那么调用 __get__ 方法。
不然,若是 attr 出如今 o 的__dict__ 中,返回 o.__dict__[attr]
不然,若是attr 出如今 cls 或父类的 __dict__ 中,若是 attr 是非覆盖型描述符,那么调用 __get__ 方法。
不然,若是没有非覆盖型描述符,直接返回 cls.__dict__[attr]
不然,若是 cls 实现了 __getattr__ 方法,调用这个方法
抛出 AttributeError
因此,在访问类的属性时,覆盖型描述符的优先级是高于直接存储在 __dict__
中的值的。
本节内容部分摘自个人这篇文章:从 Swift 的面向协议编程说开去,本节聊的是多继承在 Python 中的知识,若是想阅读关于多继承的讨论,请参考原文。
不少语言类的书籍都会介绍,多继承是个危险的行为。诚然,狭义上的多继承在绝大多数状况下都是不合理的。这里所谓的 “狭义”,指的是一个类拥有多个父类。咱们要明确一个概念:继承的目的不是代码复用,而是声明一种 is a
的关系,代码复用只是 is a
关系的一种外在表现。
所以,若是你须要狭义上的多继承,仍是应该先问问本身,真的存在这么多 is a
的关系么?你是须要声明这种关系,仍是为了代码复用。若是是后者,有不少更优雅的解决方案,由于多继承的一个直接问题就是菱形问题(Diamond Problem)。
可是广义上的多继承是必须的,不能由于惧怕多继承的问题就忽略多继承的优势。广义多继承 指的是经过定义接口(Interface)以及接口方法的默认实现,造成“一个父类,多个接口”的模式,最终实现代码的复用。固然,不是每一个语言都有接口的概念,好比 Python 里面叫 Mixin,会在 3.3.3 节中介绍。
广义上的多继承很是常见,有一些教科书式的例子,好比动物能够按照哺乳动物,爬行动物等分类,也能够按照有没有翅膀来分类。某一个具体的动物可能知足上述好几类。在实际的开发中也处处都是广义多继承的使用场景,好比 iOS 或者安卓开发中,系统控件的父类都是固定的,若是想让他们复用别的父类的代码,就会比较麻烦。