自定义输出,必须遵循 CustomStringConvertible
协议,实现 description
方法:
1 | class Person: CustomStringConvertible { |
想了好久
自定义输出,必须遵循 CustomStringConvertible
协议,实现 description
方法:
1 | class Person: CustomStringConvertible { |
算法是每一个程序员都必须要认真学习的一门课程,虽然在工作中很少能够用到,但是它能够让我们理解程序的世界。由于前段时间学习了Python,Python语法简洁,便于理解。如果看不明白,可以借助这个网站来帮助理解。
冒泡排序很简单,重复的遍历数列,依次比较两个元素,满足条件时,将两个元素进行互换。
思路:
1 | #!/usr/bin/env python3 |
快排通常比其他的算法快,因此常常被采用。快排算法体现了分治法的思想。
思路:
1 | #!/usr/bin/env python3 |
对于每个未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
思路:
1 | #!/usr/bin/env python3 |
对于一个列表l = {k1, k2, … kn},将其从小到大排序。
思路:
1 | #!/usr/bin/env python3 |
归并排布是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,然后再合并数组。
例如:有数列[5, 10, 2, 4, 6],第一次递归后,
思路:
先考虑合并两个有序数组:思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
再考虑递归分解:思路是将数组分解成left和right,如果这两个数组内部数据是有序的,那么就可以用上面合并数组的方法将这两个数组合并排序。如何让这两个数组内部是有序的?可以再二分,直至分解出的小组只含有一个元素时为止,此时认为该小组内部已有序。然后合并排序相邻二个小组即可。
1 | #!/usr/bin/env python3 |
希尔排序的基本思想是:将数组中的数据列在一个按照一定的增量列在一个表当中,然后分别进行插入排序。一直重复这个过程,每次都改变这个增量的长度。最后这个表只有一列。
思路:有以下的数组[3, 2, 4, 5, 1, 6, 8, 7]
,将增量列为数组长度的一半,那么得到的表为:
1 | 3 2 4 5 |
然后对每列进行排序,如下表:
1 | 1 2 4 5 |
再次将增量依次折半,并按照下表进行排序,如下表:
1 | 1 2 |
当增量为1的时候,就是插入排序了,重复多次后,当增量为小于0
的时候,排序完成,就得到排序后的数组列表。代码如下:
1 | #!/usr/bin/env python3 |
堆排序是采用二叉堆来的数据结构实现的,实质上还是一维数组。二叉堆是一个近似完全二叉树。
二叉堆有以下性质:
思路:
1 | #!/usr/bin/env python3 |
算法的时间复杂度和空间复杂度可参考这里
排序算法的时间复杂度和空间复杂度对比表:
操作系统多任务的普及,跟物理设备的性能的提升有着很大的关系。这一切都归功于CPU的并发处理能力,在多核快速发展的今天,CPU的计算速度越来越快。CPU以轮询的方式处理不同的任务,这样保证了在操作系统层面上优异的用户体验。可以说多任务是迄今为止最伟大的创举,使得生产力产生质的飞越。Python的os
模块封装了常见的系统调用。
Unix/Linux操作系统内核中提供了fork()
系统调用,操作系统会自动将当前进程(父进程)复制一份(子进程),然后分别在父进程和子进程中返回。子进程永远返回0
,父进程返回子进程的ID,子进程通过getppid()
接可以拿到父进程的ID。
1 |
线程和进程是计算机科学中一个非常重要的概念。比如执行一个应用程序,它就是一个独立的进程。这个应用程序里面会执行不同的任务,这些任务是由线程来完成的。一个进程至少有一个线程来完成应用程序的功能。多进程意味着可以创建多个互相之间独立的应用程序,多线程则意味着一个独立的进程当中可以同时执行不同的任务。
Unix/Linux系统提供了一个fork()
系统函数调用,当调用该函数,操作系统自动将当前进程(父进程)复制一份(子进程),然后分别在父进程和子进程当中返回。子进程当中永远返回0
,父进程则返回子进程的ID,这样父进程就可以知道fork出来的子进程的ID。子进程可以通过getpid()
函数获取到父进程的ID。这些常见的系统调用封装在Python的os
模块中:
Python中的标准库提供了_thread
和threading
模块两个线程模块,_thread
是低级模块,threading
是高级模块,它是对_thread
模块进行了封装:
1 | import time, threading |
在使用多线程的时候,由于所有的变量都可以被任意线程访问,所有则该变量有可能被任何线程修改。
为了防止变量被任意线程更改,可以利用threading
模块的Lock()
实现。使用lock = threading.Lock()
创建一个锁,使用lock.acquire()
来将需要只能单一线程访问的资源加锁,这样别的线程就不能访问。访问紫竹院结束,使用lock.release()
来释放锁。通常为了避免出现死锁的现象,我们可以使用try...finally...
保证锁一定能被释放:
1 | import threading |
Python封装操作系统读写文件的API,用法与C兼容。进行文件读写的时候,由于操作磁盘上文件的行为都是由操作系统完成的,所以需要请求操作系统打开文件对象(也称之为文件描述符),接着通过操作系统提供的接口从文件对象中读取或写入数据。
Python内置open()
函数可以读取文件,如果文件不存在则抛出IOError
:
1 | # r 表示 只读 |
如果文件打开成功,接着可以调用read()
函数一次读取文件全部内容,并返回一个str
对象。要记住:在文件使用完毕后必须要进行关闭
:
1 | >>> f.read() |
在进行文件操作的时候,都有可能产生IOError
错误,我们也可以捕获这个错误:
1 | try: |
Python中使用with
语句来简化调用形式,可以自动调用close()
方法:
1 | with open('/User/1.txt', 'r') as f: |
常用的读取文件的函数:
read()
函数一次性读取文件全部内容read(size)
函数每次最多读取 size 个字节readline()
函数每次读取一行内容readlines()
函数一次读取所有内容并按行返回list
读取二进制文件:
1 | # 读取视频、图片等二进制文件 |
字符串编码:
open()
函数还可以接受两个参数,第一个参数是将读取的字符编码进行编码,如果文件中出现非法编码的字符串,会抛出UnicodeDecodeError
错误。这种情况,还可以接受第二个参数:errors
。可以直接忽略编码出现的错误:
1 | f = open('/Users/1.txt', 'r', encoding='gbk', errors='ignore') |
写操作和读操作差不多,主要区别是open()
函数的文件权限标示符不一致:w
或wb
表示写文本文件或写二进制文件。当然,写操作完成一定要调用close()
来关闭文件,也可以像上面那样使用with
语句。
1 | with open('/Users/test.txt', 'w') as f: |
如果想写入特定编码的文本,可以给open()
函数传入encoding
参数将字符串转为指定编码。
在内当中读写字符串:
1 | >>> from io import StringIO |
也可以用字符串初始化一个StringIO,然后,像读文件一样读取:
1 | >>> from io import StringIO |
读取内存中的二进制数据:
1 | >>> from io import BytesIO |
用bytes初始化BytesIO,然后读取它:
1 | >>> from io import BytesIO |
Python内置的os
模块可以直接调用操作系统提供的接口,只需要import os
模块:
1 | >>> import os |
操作文件和目录一般在os
和os.path
模块中。
1 | # 查看蛋清目录的绝对路径 |
Python中的pickle
模块提供对象序列化功能
1 | >>> import pickle |
在序列化 JSON 的时候,我们也可以这样做:
1 | # 序列化 JSON |
dump()函数还接受不同的参数,来序列化自定义的对象:
1 | import json |
刚开始学习Python,一直不理解Python中的memetaclass到底是用来做什么的,直到我在stackoverflow上看到这个回答。感觉第二个回答比较好,闲暇时间将它翻译出来作为备用吧。
在理解metaclass之前,你需要掌握Python中的类。Python中的类借鉴了Smalltalk语言,并且关于类拥有一些奇特的想法。
在大多数语言中,类仅仅是一小段用来描述如何产生对象的代码,这一点在Python中也成立。
1 | >>> class ObjectCreator(object): |
但是,Python中的类不仅仅如此。Python中的类也是对象。
一旦你使用关键字class
,Python立即执行并创建一个对象,就像这样在内存中创建一个名字为ObjectCreator
的对象
1 | >>> class ObjectCreator(object): |
这个对象(类)能创建对象(实例),这就是为什么被称作类。
译者注:
这里的类也是对象,而生成的实例也是对象,或者说称之为实例更好些,英文原文如下:
This object (the class) is itself capable of creating objects (the instances), and this is why it’s a class.
它是一个对象,因此:
栗子:
1 | >>> print(ObjectCreator) # 你可以打印一个类,因为它是一个对象 |
由于类是一个对象,你可以在运行时动态创建,就像其他对象那样。首先,你可以使用class
在函数中创建一个类:
1 | >>> def choose_class(name): |
但是这样并不是那么动态,因为你仍然必须写整个类。既然类是对象,那么肯定有东西能生成它。
当你使用class
关键字,Python会自动的创建对象。但是像Python中大多数的东西一样,它可以通过手动创建。
还记得函数type
吗?这个古老又非常好用的函数可以让你知道某个对象的类型。
1 | >>> print(type(1)) |
type
还拥有不同的能力,他也可以在运行时创建一个类。type
可以接收一个类描述作为参数,并且返回一个类。(我知道,根据你传入参数的不同,同一个函数拥有两种不同的作用是一件很傻的事情。在Python中这个为了能够向后兼容)
type
的用法是这样的:
1 | type(name of the class, |
栗子:
1 | >>> class MyShinyClass(object): |
也可以通过这种方法手动创建:
1 | >>> MyShinyClass = type('MyShinyClass', (), {}) # 返回一个类对象 |
你会注意到我们使用”MyShinyClass”作为类名,并且作为变量作为这个类的引用。类和变量是不同的,在这里没有必要将它们复杂化。
type
接受一个字典来定义类的属性。所以:
1 | >>> class Foo(object): |
也可以写成:
1 | >>> Foo = type('Foo', (), {'bar':True}) |
可以像正常类一样来使用:
1 | >>> print(Foo) |
当然,你也可以从它继承,因此:
1 | >>> class FooChild(Foo): |
或者这样:译者注:
这里要注意type
函数的第二个参数要使用单个元素的元组写法。
1 | >>> FooChild = type('FooChild', (Foo,), {}) |
最终你想要给你的类添加方法,你只要有定义一个函数,并将其赋值给字典中的某个属性即可。
1 | >>> def echo_bar(self): |
在动态创建类之后,你可以添加更多的方法,就像向常规的类对象添加方法一样。
1 | >>> def echo_bar_more(self): |
你可以看到,在Python里,类就是对象,你可以在运行时动态的创建类。这就是当你使用关键字class
的时候,Python在幕后做的事情,他是通过元类来实现的。
元类就是创建类的东西,你定义类是为了创建对象,对吧?同样我们也知道Python中的类就是对象。那么,元类创建类,它们是类的类,你可以将它们当成这样:
1 | MyClass = MetaClass() |
你已经看到type
让你这样做:
1 | MyClass = type('MyClass', (), {}) |
这是因为函数type
是一个元类,它是Python中所有类的元类。现在你可能会疑惑为什么它是小写,而不是Type
?
我想它可能是为了与str
保持一致,str
是创建字符串对象,int
是创建整数对象,type
就是创建类对象。你可以检查__class__
属性。
在Python中一切都是对象,包括整数、字符串、函数、类。它们都是对象,并且所有的这些都可以从类中创建:
1 | >>> age = 35 |
那么,__class__
的__class__
属性是什么?
1 | >>> age.__class__.__class__ |
所以说,元类就是创建类对象的东西,你可以叫它“类工厂”。type
是Python中的内建元类,当然,你也可以创建自己的元类。
当你写一个类的时候,可以添加__metaclass__
属性:
1 | class Foo(object): |
如果你这样做了,Python将会使用元类来创建类Foo
。当心,这里有一些技巧。当你写下class Foo(object)
,但是类Foo
现在还没有在内存当中创建。Python将会在类中查找__metaclass__
定义,如果找到了,将会创建类对象Foo
,如果没有找到,它将会使用type
来创建类。请反复的读这句话。当你写下以下时:
1 | class Foo(Bar): |
Python会按照以下运行:
Foo
中有__metaclass__
属性吗?如果有,Python通过使用__metaclass__
在内存中创建名字为Foo
的类对象(我说的是类对象,请跟上我)。如果Python中找不到__metaclass__
,它会在模块层级上找__metaclass__
,并尝试同样的操作(但只适用于不继承任何东西的类,基本上是旧式的类)。接着,如果Python中没有找到__metaclass__
,它会使用Bar
(父类)的元类(这里或许默认是type
)来穿件类对象。
这里要小心,__metaclass__
属性并不是从父类继承而来的,父类中的元类(Bar.__class__
)是的。如果Bar
使用type()
(并不是type.__new__()
)来创建Bar
的__metaclass__
属性,子类将不会继承这种行为。
现在有一个很大的问题,__metaclass__
中可以存放什么?答案是:可以创建类的东西。那么什么可以创建类呢?type
,或者是任何使用到type
或者子类化type
的东西都可以。
元类的主要目的是当创建类的时候能自动的改变类。通常你会为API做这样的事情,你希望可以创建符合当前上下文的类。
假想已个很傻的例子,当你决定模块中所有的类的属性都应该是大写的形式。有好几种办法可以做到,其中一种是设置模块级别的__metaclass__
。幸运的是,__metaclass__
能够自动被调用,他并不需要是一个正式的类(我知道某些被称为’class’的东西并不需要是一个class,可以尝试画图理解下,很有帮助)。所以,我们以一个简单的例子作为开始:
1 | # 元类将自动通过传给 type 的参数作为自己的参数 |
现在让我们使用真正的class来作为元类在做一次:
1 | # ‘type’ 实际上是一个类,就像 'str', 'int' |
但是这不是真正的面向对象(OOP)。我们直接调用type
,并且并没有改写父类的__new__
方法,可以这样写:
1 | class UpperAttrMetaclass(type): |
你或许已经注意到额外的参数upperattr_metaclass
,这并没有什么特别的:__new__
总是接收它定义的类作为第一个参数。就像你使用self作为一个普通方法或者类的类方法的第一个参数那样。
当然,为了更加清晰,名字我定义的很长。但是就像self
那样,所有的参数都有传统的名称,因此在真正的产品中元类应该是这样:
1 | class UpperAttrMetaclass(type): |
我们可以使用super
来是它更加清晰,这回有利于继承(是的,你可以拥有元类,从元类继承,从type继承):
1 | class UpperAttrMetaclass(type): |
就是这样,关于元类真的没什么可以说的了。
使用元类的代码比较复杂,其背后原因并不是元类本身。而是你通常使用元类做一些晦涩难懂的事情,依赖自省、控制继承、__dict__
等等。
的确,元类在使用黑魔法的时候非常有用,因此会弄出复杂的东西来,但就元类本身,它们是非常简单的:
因为 __metaclass__
可以接收任何调用对象,为什么使用类,因为它看起来比较复杂?这里有好几个原因:
UpperAttrMetaclass(type)
,你会知道接下来做什么。__new__
,__init__
,__call__
方法,这些都允许你做不同的事情。就算你讲所有的东西都放在__new__
当中处理,游戏而还是觉得使用__init__
更加舒服。现在有一个大问题,为什么要使用一些容易出错的功能?好吧,通常你用不到它:
“元类就是深入的魔法,99%的用户应该不要担心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。(那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。)” —— Python界的领袖 Tim Peters
元类最主要的用途是创建API。一个典型的例子是 Django ORM。它允许你这样定义:
1 | class Person(models.Model): |
但是,如果你这样定义:
1 | guy = Person(name='bob', age='35') |
它不会返回IntegerField
对象,它会返回一个int
,甚至可以从数据库中取出来。这是可能的,因为models.Model
中定义了一个__metaclass__
,并使用了一些魔法能够将你定义的Person
类转变成对数据库的复杂的hook。Django将这些看起来很复杂的东西通过暴露出一个简单的使用元类的API将其化简,通过这个API重新创建代码,在背后完成真正的工作。
首先,你知道类是一个对象并且能够穿件实例。事实上,类本身也是元类的实例。
1 | >>> class Foo(object): pass |
Python中一切都是对象,要么是类的实例,要么是元类的实例。
除了type
,type
实际上是它自己的元类。在纯Python环境中这可不是你能够做到的,这是通过在实现层面耍一些小手段做到的。
其次,元类是复杂的。你或许不想通过使用它来对简单的类做修改,你可以改变类使用下面的两种技巧:
99%的时间你需要改变类,你最好使用这些,但是99%的时间你根本不需要修改类。:D
Python中通过class
关键字定义类,object
表示从哪个类继承下来的。
1 | class Student(object): |
Python中通过把属性的名称钱加上两个下划线__
,这就表明这是一个私有变量,只有内部可以访问。有时候,可能会遇到单下划线开头的实例变量,比如:_name
。这样的实例变量外部是可以访问的,但是当我们看到这样的实例变量的时候,要把它看做实例变量,不要随意访问。注意:变量名类似于 __xxx__ 这种变量是特殊变量,可以直接访问,不是私有变量。我们不能用 __name__ 这样的变量名称
。
1 | class Student(object): |
外部不能访问__name
的原因是:Python解释器将__name
变量改成了_Student__name
,所以仍然可以通过_Student__name
来访问__name
变量,不过不推荐这么做。
1 | # Tom |
1 | class Animal(object): |
由于Dog是从Animal继承而来,所以Dog可以看成为Animal类型,而Animal不能看成Dog。可以用isinstance()
来判断变量是否是某个类型:
1 | >>> isinstance(animal, Animal) |
现在给Animal对象增加一个函数,接受一个Animal
类型的变量:
1 | def run_twice(animal): |
使用多态的好处:当传入Cat或者Dog时,我们只需要接收Animal类型就可以了。因为 Cat、Dog 都是Animal类型,然后按照Animal类型进行操作,由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法。
对于一个变量,只需要知道是Animal类型,无需确切的知道其子类型,就可以放心的调用run()方法,具体是调用Dog还是Cat类型的run()方法,由运行时该对象的确切类型决定。这就是多态的厉害之处,调用方只管调用,不管细节。当我们新增一种Animal的子类时,只要确保run()方法编写正确,不管原来的代码是如何调用的。这就是著名的开闭原则
。
对扩展开放:允许新增Animal子类;
对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。
鸭子类型
并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。对于静态语言来说,如果需要传入Animal
类型,则传入的对象必须是Animal
类型或者其子类。对于Python,只要保证传入的对象有一个run()方法就可以了
:
1 | def Timer(object): |
1 | # 使用 type 判断对象类型 |
1 |
|
类属性虽然归类所有,但是类的实例都可以访问到,实例属性千万不能与类属性同名,相同名称的实例属性会屏蔽掉类属性:
1 | >>> s = Student() |
__slots__
使用__slots__
可以限定实例的属性:
1 | class Student(object): |
注意:
使用__slots__
仅对当前的类实例有效,对其子类是无效的。
__str__
__str__
用来做格式化输出:
1 | class Student(object): |
__iter__
如果一个类想被用于for ... in
循环,类似于list那样,必须实现一个 __iter__()
方法,改方法返回一个迭代对象,然后,for循环就会不断的调用该对象的__iter__()
方法获取的循环的下一个值,直到遇到StopIteration
错误时退出循环。
1 | class Fib(object): |
__getitem__
如果让一个类像数组那样支持下表运算,需要实现__getitem__()
方法:
1 | class Fib(object): |
__getattr__
当调用类的方法或属性时,如果不存在,就会报AttributeError
错误,使用__getattr__()
,动态返回一个属性:1
2
3
4
5
6
7
8
9
10
11class Student(object):
def __init__(self):
self.name = 'Tom'
def __getattr__(self, attr):
if attr=='age':
return 18
s = Student()
# 如果没有定义 __getattr__ 方法,则会报AttributeError错误
s.age
注意:
在没有实现__getattr__
方法时,Student类并没有定义age属性,如果访问age属性,会出现AttributeError: 'Student' object has no attribute 'age'
错误。如果实现了__getattr__
方法,就会动态添加属性。只有没有找到某个属性的情况下,才会调用__getattr__
属性。
这里有一个利用 __getattr__
设计 REST API 的例子:
1 | class Chain(object): |
__call__
在调用实例方法的时候,通常使用instance.method()
这种方式调用。然而,也可以通过实现__call__()
实现实例本身调用。
1 | class Student(object): |
Python是支持多继承的,在不支持多继承的编程语言当中,通常需要设计复杂的继承关系。而Python可以通过MixIn
这种方式,为类增加额外的功能。在编写程序的过程中,只要保证主线采取单一继承,通过MixIn
来给一个类增加额外的功能。
Python中也支持Enum类:1
2
3
4
5
6
7
8from enum import Enum
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
# 这样我们就获得了Month类型的枚举类。可以枚举它的所有成员:
# value 属性是自动赋给成员的 int 常量,默认为1开始
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
也可以更精确的控制枚举类型,从Enum派生出自定义类。@unique
装饰器用来保证没有重复值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from 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
>>> 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
刚开始学习Python中的装饰器,一直不理解有什么用处,后来在知乎上看到某个回答,解决了自己的一些疑惑,在这里做个记录。
Python中的装饰器其实是一个函数,可以在不改变任何的代码的前提下为某个函数增加额外的功能。通常使用装饰器可以抽离出大量与函数功能本身无关的代码并继续重用。也可以理解成为已经存在的对象添加额外的功能。
举个列子,定义个函数:
1 | def foo(): |
如果现在添加一个新的需求,输出函数执行日志。我们可以定义一个print_log(func)
函数:
1 | # print_log函数接收一个函数作为参数,执行完log后再执行真正的业务代码。 |
这种方式看起来非常好,能满足我们的目的。但是这种方式破坏了原来函数的封装性,以前直接调用函数foo()
,现在不得不调用print_log(foo)
。当然也可以使用装饰器来达到同样的目的。
1 | def print_log(func): |
print_log
函数就是装饰器,它将执行的业务方法包再函数里面,我们看起来就好像foo
函数被print_log
函数装饰了。在函数进入和退出时,被称为一个横切面,这种编程方式被称为面向切片编程
。@符号是装饰器的语法糖,在定义函数的时候使用,从而避免再一次赋值操作。
1 | def print_log(func): |
这样就可以省略前面的foo = print_log(foo)
,使用@print_log
替代。如果还有其他的函数,我们可以继续使用这个装饰器,这样就增加了程序的可读性。
装饰器还可以带参数,比如@decorator(a)
。这样就可以提供更大的灵活性。
1 | def print_log(level): |
这相当于将原来的装饰器进行一层封装,并返回装饰器decorator
,当我们使用@print_log(level="warn")
调用的时候,将参数level传递到装饰器当中。
在执行到foo = print_log(level="warn")(foo)
时,执行foo.__name__
发现该函数变成wrapper
,并不是foo
。这是因为经过decorator
装饰后的函数,返回值已经变成wrapper
函数,并不是foo
函数。Python内置的functools.wraps
能将原函数的信息暂时保存起来。这样使得装饰器函数也有和原函数一样的信息。
1 | import functools |
Python中内置很多有用的装饰器: @staticmathod、@classmethod、@property
@property
装饰器就是将一个方法变成属性调用,把一个getter方法变成属性,只需要加上@property
。此时,@property
又创建了另外一个装饰器@score.setter
,将一个setter方法变成属性赋值,这样就可以对将方法转换成属性来操作。这样就可以对参数进行检查。举个例子:
1 | class Student(object): |
装饰器的顺序:
1 | @a |
等效于:f = a(b(c(f)))
类装饰器比函数装饰器较灵活,
1 | class Foo(object): |