标签归档:Python基础教程

这3种数组内的字典去重方式 你会几种?

你知道吗?如果数组是字典组成的,直接对数组内的字典采用set的方式进行去重,会报错:

test = [{"a": 1}, {"a": 1}, {"a": 3}, {"b": 4}]
test = list(set(test))
>>>TypeError: unhashable type: 'dict'

因为使用set去重的前提是该对象为不可变对象,而字典是可变对象,因此无法使用该方法去重。

那么怎么解决这个问题呢?有三个办法。

1.使用reduce方法

reduce() 函数会对参数序列中元素进行累积。

比如:

from functools import reduce
>>>def add(x, y) :            # 两数相加
...    return x + y
... 
>>>reduce(add, [1,2,3,4,5])   # 计算列表和:1+2+3+4+5
15

上述写法也能用lambda函数简化为:

from functools import reduce
>>> reduce(lambda x, y: x+y, [1,2,3,4,5])  # 使用 lambda 匿名函数
15

因此,我们自己编写一个函数进行数组内的字典去重:

from functools import reduce

data = [{"a": 1}, {"a": 1}, {"a": 3}, {"b": 4}]
result = []
def unduplicate(result, data):
    if data not in result:
        result = result + [data]
    return result

for i in data:
    result = unduplicate(result, i)

>>> result
>>> [{'a': 1}, {'a': 3}, {'b': 4}]

稍显复杂,如果使用reduce函数和lambda函数,代码能简化很多:

def delete_duplicate(data):
    func = lambda x, y: x + [y] if y not in x else x
    data = reduce(func, [[], ] + data)
    return data

>>> delete_duplicate(data)
>>> [{'a': 1}, {'a': 3}, {'b': 4}]

当然, 我也能一行写完这个功能:

data = reduce(lambda x, y: x + [y] if y not in x else x, [[], ] + data)

只不过有可能会被打死在工位上,所以不建议这么干。

2.奇怪的技巧

就如文章开头提到的,字典之所以不能用set去重,是因为它是可变对象。

但是…如果我们把它变成不可变对象呢?

data = [{"a": 1}, {"a": 1}, {"a": 3}, {"b": 4}]
def delete_duplicate(data):
    immutable_dict = set([str(item) for item in data])
    data = [eval(i) for i in immutable_dict]
    return data
>>> delete_duplicate(data)
>>> [{'a': 1}, {'a': 3}, {'b': 4}]

没错,这能成。

1.遍历字典,将每个子项变成字符串存放到数组中,再通过set函数去重。

2.通过eval函数,将去重后的数组里的每个子项重新转化回字典。

如此Python,怎能不好玩?

3.高效的方式

上面讲了两种骚操作,其实都不太建议在实际工作中使用。

一个原因是真的太骚了,怕被打趴在工位上。

另一个原因是,它们在应对较大数据量的时候,性能不太行。

下面是最正统的方式:

data = [dict(t) for t in set([tuple(d.items()) for d in data])]
>>>data
>>>[{'a': 1}, {'b': 2}]

其实和第二种方式一样,是将数组内的每个字典转成元组,也就是不可变对象,再使用set进行去重。去重完毕后再使用dict函数将元组重新组成字典对。

但是,这种方法对于字典内还有字典的数据结构是不适用的,因此对于字典对里还有字典情况的去重,比如:

data2 = [{"a": {"b": "c"}}, {"a": {"b": "c"}}]

这种情况我建议使用第二种方式去重:

data2 = [{"a": {"b": "c"}}, {"a": {"b": "c"}}]
def delete_duplicate_str(data):
    immutable_dict = set([str(item) for item in data])
    data = [eval(i) for i in immutable_dict]
    return data
print(delete_duplicate_str(data2))

>>> [{'a': {'b': 'c'}}]

怎么样,这三种方式你都学会了吗?如果觉得有收获的话记得收藏一下。以后遇到类似的去重场景时可以拿出阅读一下。

我们的文章到此就结束啦,如果你喜欢今天的 Python 教程,请持续关注Python实用宝典。

有任何问题,可以在公众号后台回复:加群,回答相应验证信息,进入互助群询问。

原创不易,希望你能在下面点个赞和在看支持我继续创作,谢谢!

给作者打赏,选择打赏金额
¥1¥5¥10¥20¥50¥100¥200 自定义

​Python实用宝典 ( pythondict.com )
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 小坑之字符串驻留

本文整理了许多字符串驻留的坑,部分整合自wtfpython英文版,并增加了大量的后续说明。

# example1:
>>> a = "wtf"
>>> b = "wtf"
>>> a is b
True

# example2:
>>> a = "wtf!"
>>> b = "wtf!"
>>> a is b
False

# example3:
>>> a, b = "wtf!", "wtf!"
>>> a is b 
True # 3.7 版本返回结果为 False. 
# example4:
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False # 3.7 版本返回结果为 True  

字符串的这些问题,像是在和你说 1 != 1 一样坑爹。

究其原因,其实是CPython在编译的时候会自动进行优化,在某些情况下它会尝试使用已经存在的不可变对象,而不是创建一个新的对象,而恰好,字符串就是不可变对象。这种使用已存在的不可变对象的行为被称为“驻留 ” 。

驻留的原本设计意图是用于节省内存的,但是确实有时候会坑到程序员。怎样判断自己的字符串会否被驻留呢?请看这份代码:
https://github.com/python/cpython/blob/3.6/Objects/codeobject.c#L19

简单地来讲:

1.所有长度为0和1的字符串都会被驻留

2.字符串在编译时被实现的会被驻留(如’wtf’会被驻留,但是 ”.join([‘w’, ‘t’, ‘f’]) 不会)

3.字符串中只包含ASCII下的字母、数字和下划线时会被驻留. 所以’wtf!’由于包含!不会被驻留。

我们的example1中,由于发生了驻留,所以a和b是同一个字符串对象。而example2中,由于没有发生字符串驻留,a=”wtf!”和b=”wtf!”实际上使用的不是同一个字符串对象,你可以使用id获得对象的唯一标志,你会发现它们的不同:

a和b都为wtf!时:

>>> a = "wtf!"
>>> b = "wtf!"
>>> a is b
False
>>> a == b
True
>>> id(a)
2272774097864
>>> id(b)
2272774097024  

再来看看没有发生驻留时的情况,a和b都为wtf时:

# a和b都为wtf
>>> a = "wtf"
>>> b = "wtf"
>>> a is b
True
>>> a == b
True
>>> id(a)
2272774096744
>>> id(b)
2272774096744 

明白了吧?如果你想从结果识别对象是否发生驻留,关键就看对象的唯一标志有没有被改变。

不过,如example3所示,当你在同一行中将a和b都设置为 wtf! 的时候,Python解释器会创建一个新的对象,然后同时引用第二个变量,这时候它两的唯一标志id就是一样的。(example3仅适用于python3.7以下,后面被改了)。

example4中,发生了常量折叠,这其实也是一种优化技术。编译时表达式 ‘a’*20 会被替换成 ‘aaaaaaaaaaaaaaaaaaaa’ (不要数了,20个),不过只有长度小于20的字符串才会发生常量替换,这就是为什么 ‘a’*21并不等于 ‘aaaaaaaaaaaaaaaaaaaaa’ (不要数了,21个) 。

好,感谢大家的阅读,今天的…..等等,你以为这就结束了吗?还有呢:

>>> a = 10
>>> b = 10
>>> a is b
True
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a = 257
>>> b = 257
>>> a is b
False 

这又是为啥啊?请注意,Python中,对于整数对象,如果其值处于[-5,256]的闭区间内,则值相同的对象是同一个对象,否则为不同对象。我知道你想问,别问,问就是源码本身就这么写的(其实主要还是从性能方面考虑,-5到256这段数值被经常使用,因此干脆设为同一个对象重复使用,避免分配空间—赋予类别—赋予初始值等一系列操作)。

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python 重载与重写 及 泛型函数

在上一次的推送《Python 监控文件事件变化—以音乐高潮提取为例》中,最后继承 LoggingEventHandler 类,对 on_created 进行修改的时候,我使用了一个词叫:重载,随后不久我便意识到犯下了一个错误。

我们先来看看重载与重写的概念:

重载:

重载的条件如下:

1.一个类里面

2.方法名字相同

3.参数不同

而参数不同可分为:参数类型不同,参数个数不同。而上一次推送中,对on_created的修改并没有针对参数,我们只是单纯地对函数内容进行修改而已。因此只能叫重写

重写:

条件如下:

1.参数列表与原函数一致。

2.返回类型与原函数一致。

因此,根据这个条件,Python中绝大部分的继承修改只能叫重写,而不能叫重载

Python其实不需要重载这个概念。为什么呢?重载主要是针对参数而言的,一个是改变参数的类型,一个是改变参数的个数。而Python不需要限定参数类型,又 可以接受可变参数,因此函数重载就显得非常鸡肋了。

不过我们依然能通过singledispatch装饰器来实现重载,不过官方称其为泛型函数:

在需要被重载的函数上加上装饰器:

from functools import singledispatch


@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg) 

然后用register属性注册重载后的函数:

@fun.register(int)
def _(arg: int, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)


@fun.register(list)
def _(arg: list, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem) 

效果如下:

>>> fun(42, verbose=True)
Strength in numbers, eh? 42

>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam 

看,这样我们就顺利完成了重载,尽管其实大部分时候都不会用到。

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

快来用Python制作只属于你和ta的聊天渠道吧

是否担心微信的数据流会被监视?是否担心你和ta聊天的小秘密会被保存到某个数据里?没关系,现在我们可以用Python做一个只属于你和ta的聊天渠道,来解除你们心中的担忧。

1.原理简介

在我们今天的教程中,将用到即时通讯的概念,即时通讯允许两人或多人同时使用网络传递文字信息、文字、语音等。即时通讯一般都基于socket连接,socket连接可用于发送或接受数据,一般的组合形式是IP+端口号

也就是说,在我们的例子中,聊天的双方,由一方要承担“服务器 ” 的责任,维持一个socket服务器,等待连接进入;另一方则是“客户端”,在服务器端维持等待状态时即可发送请求,建立连接。

当你和ta想进入“小黑屋 ” 里聊天的时候,只有有一方充当服务器,另一方充当客户端即可,作为“服务器端 ” 的那个人,在微信中将IP和端口号告诉对方,即可构建连接,在小黑屋里聊天,这个小黑屋里的数据不会被任何数据保留(除非你自己做了一个保存的数据)。

2.代码编写

好了,基本原理我们已经讲清楚了。不过,在开始教程之前,你得先安装好了Python,如果还没有安装,可以看这篇文章:https://pythondict.com/python-tutorials/how-to-install-python/

2.1 服务器端

聊天的时候,我们有时候会遇到双方同时发消息的情况。这种聊天方式就叫全双工聊天方式:“服务器”可向“客户端”发送消息,“客户端”也可向“服务端”发送消息,而且允许同时发送消息。

服务器端怎么实现全双工的聊天方式呢?其实很简单,只要用多线程就行了,主线程用于接收客户端的连接,连接成功后新建两个线程:一个用于发送消息,一个用于接收消息:

首先,建立socket服务器:

import socket
import traceback
# 设定ip和端口号
host = ''
port = 51423 
# 建立socket服务器
s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.bind((host,port))
s.listen() 
while True:
    # 等待连接
    try:
        clientsock, clientaddr = s.accept()
    except KeyboardInterrupt:
        raise
    except:
        traceback.print_exc()
        continue 

其中,AF_INET指的是用IPv4进行通信,而SOCK_STREAM指的是TCP协议。端口号你可以随意设定,服务器端的IP地址默认为空即可。

在while循环中不断等待用户的连接。如果有用户连接成功了,我们将进入下一步,分别建立发送和接受线程:

# 建立接收线程
t = _thread.start_new_thread(processRecv, (clientsock,))

# 建立发送线程
r = _thread.start_new_thread(processSend, (clientsock,)) 

clientsock就是我们得到的socket连接,processRecv和processSend分别用于处理接受信息和处理发送信息:

import _thread 
def processRecv(clientsock):
    """
    接受消息
        :param clientsock: 客户端的socket连接
    """
    while True:
        data = clientsock.recv(4096)
        if not len(data):
            break
        print (data.decode('utf-8'))
    clientsock.close()

def processSend(clientsock):
    """
    发送消息
        :param clientsock: 客户端的socket连接
    """
    while True:
        data = input("> ")
        data = data
        clientsock.sendall(data.encode('utf-8'))
    clientsock.close() 

有个小细节要注意,socket连接的sendall函数只支持bytes类型的数据,所以我们要encode(‘utf-8’)。

服务端的所有代码就这样,没错,就是这么简单。

2.2 客户端

客户端则更简单,主线程本身设定为接受消息,那么我们只需要多一个线程用于发送消息即可。客户端的全部代码如下:

#-*-coding:utf-8-*-

import _thread
import sys
from socket import *

def send_message(tcpCliSock):
    """
    发送信息
        :param tcpCliSock: 与服务端的socket连接
    """
    while True:
        message = input('> ')
        if not message:
            break
        tcpCliSock.send(message.encode('utf-8'))

    tcpCliSock.close()

if(len(sys.argv) < 3):
    HOST = 'localhost'
    PORT = 51423
else:
    HOST = sys.argv[1]
    PORT = int(sys.argv[2])

BUFSIZ = 1024
ADDR = (HOST,PORT)

tcpCliSock = socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(ADDR)

# 建立发送消息的线程
s = _thread.start_new_thread(send_message, (tcpCliSock,))

while True:
    rdata = tcpCliSock.recv(BUFSIZ)
    if not rdata:
        break
    print (rdata.decode('utf-8'))
    
tcpCliSock.close() 

其中,HOST部分填写对方的IP,PORT部分填写端口号。sys.argv用于通过参数输入这两个值,比如我们将客户端文件命名为:client.py, 在cmd中输入:

python client.py 127.0.0.1 51423

能直接传入参数执行脚本,除此之外,其他部分和服务端其实差不多。注意把接受到的数据decode一下(因为我们发的时候encode了)。

3. 改进

实际上,这份代码虽然可以用,但是还是存在许多问题的。比如在你们聊天的时候,突然又有一个人向服务端发送连接请求怎么办?这时候我们需要在服务端加一份拥有验证的代码,要求对方输入聊天室密码后才可建立连接。

这个密码必须是你们双方才知道的密码,任何通过第三方工具传播密码的行为都是不可靠和不安全的。这样才可以防止第三者的偷听。增加一个密码功能其实也不难,这部分交给大家自己去实现啦!

本文完整源代码下载请在公众号后台回复:聊天渠道

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Python生成器不该这么用

最近在知乎上有人误解了Python生成器的使用,在这里我们来统一探讨下它这么用对不对。

举一个例子,编写一个函数计算一串数字里所有偶数的个数,其实是很简单的问题,但是有些人是用生成器这么写的:

In [66]: def f1(x):
   ....:     return sum(c in '02468' for c in str(x))
   ....: 
In [68]: x = int('1234567890'*50)
In [69]: %timeit f1(x)
10000 loops, best of 5: 52.2 µs per loop

生成器这么用其实是速度最慢的一种做法,花费了52微秒。我们来看看如果我改成列表解析式会怎么样:

In [67]: def f2(x):
  ....:     return sum([c in '02468' for c in str(x)])
  ....:  
In [70]: %timeit f2(x)
10000 loops, best of 5: 40.5 µs per loop 

你看,这个加速非常地明显,仅花费了40.5微秒

而且还能进一步改进, 如果我们改变之前定义的f2,让它在列表解析式后判断数字是否为偶数,是偶数才会成为最终生成的列表中的一员,这样有另一个加速:

In [71]: def f3(x):
   ....:     return sum([True for c in str(x) if c in '02468'])
   ....: 
In [72]: %timeit f3(x)
10000 loops, best of 5: 34.9 µs per loop


34.9微秒,Perfect! 不仅如此,还能继续加速!sum对于整数有一个快速路径,但是这个快速路径只激活类型为int的变量. bool不行,因此我们把True改成1,能再加一次速!

In [73]: def f4(x):
   ....:     return sum([1 for c in str(x) if c in '02468'])
   ....: 
In [74]: %timeit f4(x)
10000 loops, best of 5: 33.3 µs per loop

​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典


Python里Yield关键词的作用

要理解yield的作用,您必须理解生成器是什么。在理解生成器之前,必须先理解迭代器。

迭代器

当您创建一个列表时,您可以逐个读取它的项。逐项读取其项称为迭代:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylist是一个可迭代的对象。当你使用列表解析式时,你创建了一个列表,因此也是一个迭代器:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

所有你可以用”for… in ….”都是迭代器,包括列表、字符串、文件…等等。

这些迭代器非常方便,因为您可以随心所欲地读取它们,但是您将所有的值都存储在内存中,当您有很多值时,这就非常浪费内存了。

生成器

生成器是迭代器,这种迭代器只能迭代一次。生成器不会将所有值都存储在内存中,它们会动态生成这些值:

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

它和列表解析式是类似的,只是用()代替了[]。但是,您不能在mygenerator中对i执行第二次,因为生成器只能使用一次:它print(0),然后忘记它,print(1),最后是4。

Yield

yield是一个与return类似的关键字,只是函数将返回一个生成器

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
 <generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

您需要知道的是函数将返回一组只需要读取一次的值,将这个特性理解清楚,用对地方将极大地提高性能,下次我们将介绍在什么时候该用它。

要掌握yield,您必须了解,在调用函数时,在函数体中编写的代码不会运行。函数只返回生成器对象,这有点棘手:-)

然后,您的代码将从每次使用生成器时停止的地方继续。

现在是最难的部分:

for函数第一次调用从函数创建的生成器对象时,它将从头运行函数中的代码,直到达到yield,然后返回循环的第一个值。然后,彼此调用将再次运行您在函数中编写的循环,并返回下一个值,直到没有要返回的值为止,就如我们上面的例子所示。

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Vscode 设置中文

如果你刚下载VSCode,发现界面全是英文的,不要急,下面就来教你怎么改成中文。

1. 首先打开vscode界面,点击View – Command Palette (或输入 Ctrl + shift + P)进入命令面板.

打开命令面板

2. 然后输入 configure language, 选择Configure Display Language (配置显示语言)。

Configure Display Language

3. 然后查看有没有zh-cn的选项,如果有,直接选择zh-cn替换,然后按照提示重启vscode就能看到界面变回中文了。

如果没有zh-cn的选项,则选择install additional languages (添加其他语言选项),左边会弹出扩展窗口,扩展窗口找到中文简体,然后点击install安装。然后重复第1, 2步骤选择中文即可。

install additional languages

VsCode系列文章:

Python 使用VS Code进行调试

VSCode 设置中文

Python 编程的最好搭档—VSCode 详细指南

我们的文章到此就结束啦,如果你希望我们今天的Python 教程,请持续关注我们,如果对你有帮助,麻烦在下面点一个赞/在看哦有任何问题都可以在下方留言区留言,我们都会耐心解答的!


​Python实用宝典 (pythondict.com)
不只是一个宝典
欢迎关注公众号:Python实用宝典

Pycharm 代码左边的数字是什么?

代码左边的数字

这个是Pycharm的书签功能

去除:

删除书签,将光标移至书签所在行,按快捷键F11即可消除。

添加:

在所在行按快捷键Ctrl+Shift+任意数字或者快捷键Ctrl+F11 即可添加,就可以插入一个标签了。

使用:

要将光标跳转到某个书签,只需要Ctrl+对应的标签数字。

如果想要查看所有书签位置:Shift + F11。

了解更多

更多有趣的Python教程欢迎关注公众号:Python实用宝典

或者访问网站:Python实用宝典