python进阶之学习笔记_Python学习笔记(进阶篇二)

由于这块较为重要且较难,所以基本上转载了所有原文描述。

面向对象

基本定义

class Student(object):

pass复制代码

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的init方法,在创建实例的时候,就把name,score等属性绑上去:

class Student(object):

def __init__(self, name, score):

self.name = name

self.score = score复制代码

注意:特殊方法“init”前后有两个下划线!!!注意到init方法的第一个参数永远是self,表示创建的实例本身,因此,在init方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身。

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

数据封装

class Student(object):

def __init__(self, name, score):

self.name = name

self.score = score

def print_score(self):

print('%s: %s' % (self.name, self.score))复制代码

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传入:

访问限制

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线,在Python中,实例的变量名如果以开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:

class Student(object):

def __init__(self, name, score):

self.__name = name

self.__score = score

def print_score(self):

print('%s: %s' % (self.__name, self.__score))复制代码

改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.name和实例变量.score了:

>>>bart = Student('Bart Simpson', 98)

>>>bart.__name

Traceback (most recent call last):

File "", line 1, in

AttributeError: 'Student' object has no attribute '__name'复制代码

如果又要允许外部代码修改score怎么办?可以再给Student类增加set_score方法:

class Student(object):

...

def set_score(self, score):

self.__score = score复制代码

继承和多态

概念基本与java中的类似,只记载不同处。

对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了(因为python的参数列表没有参数类型的一说):

由于Python是动态语言,根据类创建的实例可以任意绑定属性。

给实例绑定属性的方法是通过实例变量,或者通过self变量:

class Student(object):

def __init__(self, name):

self.name = name

s = Student('Bob')

s.score = 90复制代码

但是,如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):

name = 'Student'复制代码

获取对象信息

当我们拿到一个对象的引用时,如何知道这个对象是什么类型、有哪些方法呢?

使用type()

首先,我们来判断对象类型,使用type()函数:

基本类型都可以用type()判断:

>>>import types

>>>def fn():

... pass

...

>>>type(fn)==types.FunctionType

True

>>>type(abs)==types.BuiltinFunctionType

True

>>>type(lambda x: x)==types.LambdaType

True

>>>type((x for x in range(10)))==types.GeneratorType

True复制代码

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。

>>>isinstance(h, Husky)

True复制代码

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

>>>dir('ABC')

['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']复制代码

类似xxx的属性和方法在Python中都是有特殊用途的,比如len方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的len()方法,所以,下面的代码是等价的:

>>>len('ABC')

3

>>>'ABC'.__len__()

3复制代码

我们自己写的类,如果也想用len(myObj)的话,就自己写一个len()方法:

>>>class MyDog(object):

... def __len__(self):

... return 100

...

>>>dog = MyDog()

>>>len(dog)

100复制代码

剩下的都是普通属性或方法,比如lower()返回小写的字符串:

>>>'ABC'.lower()

'abc'复制代码

紧接着,可以测试该对象的属性:

>>>hasattr(obj, 'x') # 有属性'x'吗?

True复制代码

可以传入一个default参数,如果属性不存在,就返回默认值:

>>>getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404

404复制代码

面向对象高级编程

使用__slots__

正常情况下,当我们定义了一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法。

但是,给一个实例绑定的方法和属性,对另一个实例是不起作用的:

>>>s2 = Student() # 创建新的实例

>>>s2.set_age(25) # 尝试调用方法

Traceback (most recent call last):

File "", line 1, in

AttributeError: 'Student' object has no attribute 'set_age'复制代码

为了给所有实例都绑定方法,可以给class绑定方法:

>>>def set_score(self, score):

... self.score = score

...

>>>Student.set_score = set_score复制代码

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

为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的slots变量,来限制该class实例(不包括类属性)能添加的属性:

class Student(object):

__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称复制代码

这样我们就可以保证只允许动态添加名字为name何age的属性和方法了

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

>>>class GraduateStudent(Student):

... pass

...

>>>g = GraduateStudent()

>>>g.score = 9999复制代码

除非在子类中也定义slots,这样,子类实例允许定义的属性就是自身的slots加上父类的slots。

使用@property

Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):

@property

def score(self):

return self._score

@score.setter

def score(self, value):

if not isinstance(value, int):

raise ValueError('score must be an integer!')

if value < 0 or value > 100:

raise ValueError('score must between 0 ~ 100!')

self._score = value复制代码

多重继承

python中是可以多重继承的,这种设计通常称之为MixIn。。

使用示例:

class C(A , B):复制代码

大坑:

使用多重继承时,如果多个父类同时拥有相同的方法或属性,会优先使用左边的(子类未复写的情况下)

继承次序有可能引起程序崩溃,比如B继承A,B(A),C继承A和B,C(A.B)

python中的类构造器__init__在继承时,并不会去默认调用父类的构造器(除非子类没有相应的构造器,python就会自己去父类找)

定制类

__str__方法

类似于toString方法,但是又有一定区别,通常我们自定义java类中的toString方法时,我们在外面调用都会显式调用这个类对象的toString方法,而python中,我们直接打印这个对象就会自动调用此方法,如果不定义此方法,那么就会打印出对象的内存地址。

__repr__方法

和上面的方法类似,不过此方法主要针对开发者,如果你同样想要显示自己想要的内容,那么一样需要自定义。

__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 # 返回下一个值复制代码

可以看出,通常我们需要配合next方法同时使用。

__getitem__方法

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

>>>Fib()[5]

Traceback (most recent call last):

File "", line 1, in

TypeError: 'Fib' object does not support indexing复制代码

要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

class Fib(object):

def __getitem__(self, n):

a, b = 1, 1

for x in range(n):

a, b = b, a + b

return a复制代码

现在,就可以按下标访问数列的任意一项了。

但是list有个神奇的切片方法:

>>>list(range(100))[5:10]

[5, 6, 7, 8, 9]复制代码

对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:

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 L复制代码

现在试试Fib的切片:

>>>f = Fib()

>>>f[0:5]

[1, 1, 2, 3, 5]

>>>f[:10]

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]复制代码

但是没有对step参数作处理:

>>>f[:10:2]

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]复制代码

也没有对负数作处理,所以,要正确实现一个getitem()还是有很多工作要做的。

此外,如果把对象看成dict,getitem()的参数也可能是一个可以作key的object,例如str。

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

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__方法

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。比如定义Student类,只有一个字段name。当我们视图调用score这个字段时就会出错。

要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个getattr()方法,动态返回一个属性。修改如下:

class Student(object):

def __init__(self):

self.name = 'Michael'

def __getattr__(self, attr):

if attr=='score':

return 99复制代码

当调用不存在的属性时,比如score,Python解释器会试图调用getattr(self, 'score')来尝试获得属性,这样,我们就有机会返回score的值:

返回函数也是完全可以的:

class Student(object):

def __getattr__(self, attr):

if attr=='age':

return lambda: 25复制代码

只是调用方式要变为:

>>>s.age()

25复制代码

此外,注意到任意调用如s.abc都会返回None,这是因为我们定义的getattr默认返回就是None。要让class只响应特定的几个属性,我们就要按照约定,抛出AttributeError的错误:

class Student(object):

def __getattr__(self, attr):

if attr=='age':

return lambda: 25

raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)复制代码

这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。

这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。

举个例子:

现在很多网站都搞REST API,比如新浪微博、豆瓣啥的,调用API的URL类似:

如果要写SDK,给每个URL对应的API都写一个方法,那得累死,而且,API一旦改动,SDK也要改。

利用完全动态的getattr,我们可以写出一个链式调用:

class Chain(object):

def __init__(self, path=''):

self._path = path

def __getattr__(self, path):

return Chain('%s/%s' % (self._path, path))

def __str__(self):

return self._path

__repr__ = __str__复制代码

试试:

>>>Chain().status.user.timeline.list

'/status/user/timeline/list'复制代码

这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变!

还有些REST API会把参数放到URL中,比如GitHub的API:

GET /users/:user/repos

调用时,需要把:user替换为实际用户名。如果我们能写出这样的链式调用:

Chain().users('michael').repos

就可以非常方便地调用API了。有兴趣的童鞋可以试试写出来。

__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.复制代码

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

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

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

>>>callable(Student())

True

>>>callable(max)

True

>>>callable([1, 2, 3])

False

>>>callable(None)

False

>>>callable('str')

False复制代码

通过callable()函数,我们就可以判断一个对象是否是“可调用”对象。

使用枚举类

当我们需要定义常量时,一个办法是用大写变量通过整数来定义,例如月份:

JAN = 1

FEB = 2

MAR = 3

...

NOV = 11

DEC = 12复制代码

好处是简单,缺点是类型是int,并且仍然是变量。

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))复制代码

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():

print(name, '=>', member, ',', member.value)复制代码

value属性则是自动赋给成员的int常量,默认从1开始计数。

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from 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复制代码

@unique装饰器可以帮助我们检查保证没有重复值。

使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时定义的,而是运行时动态创建的。

比方说我们要定义一个Hello的class,就写一个hello.py模块:

class Hello(object):

def hello(self, name='world'):

print('Hello, %s.' % name)复制代码

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:

>>> from hello import Hello

>>> h = Hello()

>>> h.hello()

Hello, world.

>>> print(type(Hello))

>>> print(type(h))

复制代码

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型就是class Hello。

我们说class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:

>>> def fn(self, name='world'): # 先定义函数

... print('Hello, %s.' % name)

...

>>> Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello class

>>> h = Hello()

>>> h.hello()

Hello, world.

>>> print(type(Hello))

>>> print(type(h))

复制代码

要创建一个class对象,type()函数依次传入3个参数:

class的名称;

继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;

class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx...来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

metaclass

除了使用type()动态创建类以外,要控制类的创建行为,还可以使用metaclass。

metaclass,直译为元类,简单的解释就是:

当我们定义了类以后,就可以根据这个类创建出实例,所以:先定义类,然后创建实例。

但是如果我们想创建出类呢?那就必须根据metaclass创建出类,所以:先定义metaclass,然后创建类。

连接起来就是:先定义metaclass,就可以创建类,最后创建实例。

所以,metaclass允许你创建类或者修改类。换句话说,你可以把类看成是metaclass创建出来的“实例”。

metaclass是Python面向对象里最难理解,也是最难使用的魔术代码。正常情况下,你不会碰到需要使用metaclass的情况,所以,以下内容看不懂也没关系,因为基本上你不会用到。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值