2019年5月

在这个从零构建一个 Python 游戏系列的第六部分中,为你的角色创建一些平台来旅行。

这是仍在进行中的关于使用 Pygame 模块来在 Python 3 中创建电脑游戏的系列文章的第六部分。先前的文章是:

一个平台类游戏需要平台。

Pygame 中,平台本身也是个妖精,正像你那个可玩的妖精。这一点是重要的,因为有个是对象的平台,可以使你的玩家妖精更容易与之互动。

创建平台有两个主要步骤。首先,你必须给该对象编写代码,然后,你必须映射出你希望该对象出现的位置。

编码平台对象

要构建一个平台对象,你要创建一个名为 Platform 的类。它是一个妖精,正像你的 Player 妖精 一样,带有很多相同的属性。

你的 Platform 类需要知道很多平台类型的信息,它应该出现在游戏世界的哪里、它应该包含的什么图片等等。这其中很多信息可能还尚不存在,这要看你为你的游戏计划了多少,但是没有关系。正如直到移动你的游戏角色那篇文章结束时,你都没有告诉你的玩家妖精移动速度有多快,你不必事先告诉 Platform 每一件事。

在这系列中你所写的脚本的开头附近,创建一个新的类。在这个代码示例中前三行是用于说明上下文,因此在注释的下面添加代码:

import pygame
import sys
import os
## 新代码如下:

class Platform(pygame.sprite.Sprite):
# x location, y location, img width, img height, img file    
def __init__(self,xloc,yloc,imgw,imgh,img):
    pygame.sprite.Sprite.__init__(self)
    self.image = pygame.image.load(os.path.join('images',img)).convert()
    self.image.convert_alpha()
    self.image.set_colorkey(ALPHA)
    self.rect = self.image.get_rect()
    self.rect.y = yloc
    self.rect.x = xloc

当被调用时,这个类在某个 X 和 Y 位置上创建一个屏上对象,具有某种宽度和高度,并使用某种图像作为纹理。这与如何在屏上绘制出玩家或敌人非常类似。

平台的类型

下一步是绘制出你的平台需要出现的地方。

瓷砖方式

实现平台类游戏世界有几种不同的方法。在最初的横向滚轴游戏中,例如,马里奥超级兄弟和刺猬索尼克,这个技巧是使用“瓷砖”方式,也就是说有几个代表地面和各种平台的块,并且这些块被重复使用来制作一个关卡。你只能有 8 或 12 种不同的块,你可以将它们排列在屏幕上来创建地面、浮动的平台,以及你游戏中需要的一切其它的事物。有人发现这是制作游戏最容易的方法了,因为你只需要制作(或下载)一小组关卡素材就能创建很多不同的关卡。然而,这里的代码需要一点数学知识。

 title=

SuperTux ,一个基于瓷砖的电脑游戏。

手工绘制方式

另一种方法是将每个素材作为一个整体图像。如果你喜欢为游戏世界创建素材,那你会在用图形应用程序构建游戏世界的每个部分上花费很多时间。这种方法不需要太多的数学知识,因为所有的平台都是整体的、完整的对象,你只需要告诉 Python 将它们放在屏幕上的什么位置。

每种方法都有优势和劣势,并且根据于你选择使用的方式,代码稍有不同。我将覆盖这两方面,所以你可以在你的工程中使用一种或另一种,甚至两者的混合。

关卡绘制

总的来说,绘制你的游戏世界是关卡设计和游戏编程中的一个重要的部分。这需要数学知识,但是没有什么太难的,而且 Python 擅长数学,它会有所帮助。

你也许发现先在纸张上设计是有用的。拿一张表格纸,并绘制一个方框来代表你的游戏窗体。在方框中绘制平台,并标记其每一个平台的 X 和 Y 坐标,以及它的宽度和高度。在方框中的实际位置没有必要是精确的,你只要保持数字合理即可。譬如,假设你的屏幕是 720 像素宽,那么你不能在一个屏幕上放 8 块 100 像素的平台。

当然,不是你游戏中的所有平台都必须容纳在一个屏幕大小的方框里,因为你的游戏将随着你的玩家行走而滚动。所以,可以继续绘制你的游戏世界到第一屏幕的右侧,直到关卡结束。

如果你更喜欢精确一点,你可以使用方格纸。当设计一个瓷砖类的游戏时,这是特别有用的,因为每个方格可以代表一个瓷砖。

 title=

一个关卡地图示例。

坐标系

你可能已经在学校中学习过笛卡尔坐标系。你学习的东西也适用于 Pygame,除了在 Pygame 中你的游戏世界的坐标系的原点 0,0 是放置在你的屏幕的左上角而不是在中间,是你在地理课上用过的坐标是在中间的。

 title=

在 Pygame 中的坐标示例。

X 轴起始于最左边的 0,向右无限增加。Y 轴起始于屏幕顶部的 0,向下延伸。

图片大小

如果你不知道你的玩家、敌人、平台是多大的,绘制出一个游戏世界是毫无意义的。你可以在图形程序中找到你的平台或瓷砖的尺寸。例如在 Krita 中,单击“图像”菜单,并选择“属性”。你可以在“属性”窗口的最顶部处找到它的尺寸。

另外,你也可以创建一个简单的 Python 脚本来告诉你的一个图像的尺寸。打开一个新的文本文件,并输入这些代码到其中:

#!/usr/bin/env python3

from PIL import Image
import os.path
import sys

if len(sys.argv) > 1:
    print(sys.argv[1])
else:
    sys.exit('Syntax: identify.py [filename]')

pic = sys.argv[1]
dim = Image.open(pic)
X   = dim.size[0]
Y   = dim.size[1]

print(X,Y)

保存该文本文件为 identify.py

要使用这个脚本,你必须安装一些额外的 Python 模块,它们包含了这个脚本中新使用的关键字:

$ pip3 install Pillow --user

一旦安装好,在你游戏工程目录中运行这个脚本:

$ python3 ./identify.py images/ground.png
(1080, 97)

在这个示例中,地面平台的图形的大小是 1080 像素宽和 97 像素高。

平台块

如果你选择单独地绘制每个素材,你必须创建想要插入到你的游戏世界中的几个平台和其它元素,每个素材都放在它自己的文件中。换句话说,你应该让每个素材都有一个文件,像这样:

 title=

每个对象一个图形文件。

你可以按照你希望的次数重复使用每个平台,只要确保每个文件仅包含一个平台。你不能使用一个文件包含全部素材,像这样:

 title=

你的关卡不能是一个图形文件。

当你完成时,你可能希望你的游戏看起来像这样,但是如果你在一个大文件中创建你的关卡,你就没有方法从背景中区分出一个平台,因此,要么把对象绘制在它们自己的文件中,要么从一个更大的文件中裁剪出它们,并保存为单独的副本。

注意: 如同你的其它素材,你可以使用 GIMP、Krita、MyPaint,或 Inkscape 来创建你的游戏素材。

平台出现在每个关卡开始的屏幕上,因此你必须在你的 Level 类中添加一个 platform 函数。在这里特例是地面平台,它重要到应该拥有它自己的一个组。通过把地面看作一组特殊类型的平台,你可以选择它是否滚动,或它上面是否可以站立,而其它平台可以漂浮在它上面。这取决于你。

添加这两个函数到你的 Level 类:

def ground(lvl,x,y,w,h):
    ground_list = pygame.sprite.Group()
    if lvl == 1:
        ground = Platform(x,y,w,h,'block-ground.png')
        ground_list.add(ground)

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list

def platform( lvl ):
    plat_list = pygame.sprite.Group()
    if lvl == 1:
        plat = Platform(200, worldy-97-128, 285,67,'block-big.png')
        plat_list.add(plat)
        plat = Platform(500, worldy-97-320, 197,54,'block-small.png')
        plat_list.add(plat)
    if lvl == 2:
        print("Level " + str(lvl) )
       
    return plat_list

ground 函数需要一个 X 和 Y 位置,以便 Pygame 知道在哪里放置地面平台。它也需要知道平台的宽度和高度,这样 Pygame 知道地面延伸到每个方向有多远。该函数使用你的 Platform 类来生成一个屏上对象,然后将这个对象添加到 ground_list 组。

platform 函数本质上是相同的,除了其有更多的平台。在这个示例中,仅有两个平台,但是你可以想有多少就有多少。在进入一个平台后,在列出另一个前你必须添加它到 plat_list 中。如果你不添加平台到组中,那么它将不出现在你的游戏中。

提示: 很难想象你的游戏世界的 0 是在顶部,因为在真实世界中发生的情况是相反的;当估计你有多高时,你不会从上往下测量你自己,而是从脚到头顶来测量。

如果对你来说从“地面”上来构建你的游戏世界更容易,将 Y 轴值表示为负数可能有帮助。例如,你知道你的游戏世界的底部是 worldy 的值。因此 worldy 减去地面的高度(在这个示例中是 97)是你的玩家正常站立的位置。如果你的角色是 64 像素高,那么地面减去 128 正好是你的玩家的两倍高。事实上,一个放置在 128 像素处平台大约是相对于你的玩家的两层楼高度。一个平台在 -320 处比三层楼更高。等等。

正像你现在可能所知的,如果你不使用它们,你的类和函数是没有价值的。添加这些代码到你的设置部分(第一行只是上下文,所以添加最后两行):

enemy_list  = Level.bad( 1, eloc )
ground_list = Level.ground( 1,0,worldy-97,1080,97 )
plat_list   = Level.platform( 1 )

并把这些行加到你的主循环(再一次,第一行仅用于上下文):

enemy_list.draw(world)  # 刷新敌人
ground_list.draw(world)  # 刷新地面
plat_list.draw(world)  # 刷新平台

瓷砖平台

瓷砖类游戏世界更容易制作,因为你只需要在前面绘制一些块,就能在游戏中一再使用它们创建每个平台。在像 OpenGameArt.org 这样的网站上甚至有一套瓷砖供你来使用。

Platform 类与在前面部分中的类是相同的。

groundplatformLevel 类中,然而,必须使用循环来计算使用多少块来创建每个平台。

如果你打算在你的游戏世界中有一个坚固的地面,这种地面是很简单的。你只需要从整个窗口的一边到另一边“克隆”你的地面瓷砖。例如,你可以创建一个 X 和 Y 值的列表来规定每个瓷砖应该放置的位置,然后使用一个循环来获取每个值并绘制每一个瓷砖。这仅是一个示例,所以不要添加这到你的代码:

# Do not add this to your code
gloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656]

不过,如果你仔细看,你可以看到所有的 Y 值是相同的,X 值以 64 的增量不断地增加 —— 这就是瓷砖的大小。这种重复是精确地,是计算机擅长的,因此你可以使用一点数学逻辑来让计算机为你做所有的计算:

添加这些到你的脚本的设置部分:

gloc = []
tx   = 64
ty   = 64

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1

ground_list = Level.ground( 1,gloc,tx,ty )

现在,不管你的窗口的大小,Python 会通过瓷砖的宽度分割游戏世界的宽度,并创建一个数组列表列出每个 X 值。这里不计算 Y 值,因为在平的地面上这个从不会变化。

为了在一个函数中使用数组,使用一个 while 循环,查看每个条目并在适当的位置添加一个地面瓷砖:

def ground(lvl,gloc,tx,ty):
    ground_list = pygame.sprite.Group()
    i=0
    if lvl == 1:
        while i < len(gloc):
            ground = Platform(gloc[i],worldy-ty,tx,ty,'tile-ground.png')
            ground_list.add(ground)
            i=i+1

    if lvl == 2:
        print("Level " + str(lvl) )

    return ground_list

除了 while 循环,这几乎与在上面一部分中提供的瓷砖类平台的 ground 函数的代码相同。

对于移动的平台,原理是相似的,但是这里有一些技巧可以使它简单。

你可以通过它的起始像素(它的 X 值)、距地面的高度(它的 Y 值)、绘制多少瓷砖来定义一个平台,而不是通过像素绘制每个平台。这样,你不必操心每个平台的宽度和高度。

这个技巧的逻辑有一点复杂,因此请仔细复制这些代码。有一个 while 循环嵌套在另一个 while 循环的内部,因为这个函数必须考虑每个数组项的三个值来成功地建造一个完整的平台。在这个示例中,这里仅有三个平台以 ploc.append 语句定义,但是你的游戏可能需要更多,因此你需要多少就定义多少。当然,有一些不会出现,因为它们远在屏幕外,但是一旦当你进行滚动时,它们将呈现在眼前。

def platform(lvl,tx,ty):
    plat_list = pygame.sprite.Group()
    ploc = []
    i=0
    if lvl == 1:
        ploc.append((200,worldy-ty-128,3))
        ploc.append((300,worldy-ty-256,3))
        ploc.append((500,worldy-ty-128,4))
        while i < len(ploc):
            j=0
            while j <= ploc[i][2]:
                plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'tile.png')
                plat_list.add(plat)
                j=j+1
            print('run' + str(i) + str(ploc[i]))
            i=i+1
           
    if lvl == 2:
        print("Level " + str(lvl) )

    return plat_list

要让这些平台出现在你的游戏世界,它们必须出现在你的主循环中。如果你还没有这样做,添加这些行到你的主循环(再一次,第一行仅被用于上下文)中:

        enemy_list.draw(world)  # 刷新敌人
        ground_list.draw(world) # 刷新地面
        plat_list.draw(world)   # 刷新平台

启动你的游戏,根据需要调整你的平台的放置位置。如果你看不见屏幕外产生的平台,不要担心;你不久后就可以修复它。

到目前为止,这是游戏的图片和代码:

 title=

到目前为止,我们的 Pygame 平台。

#!/usr/bin/env python3
# draw a world
# add a player and player control
# add player movement
# add enemy and basic collision
# add platform

# GNU All-Permissive License
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved.  This file is offered as-is,
# without any warranty.

import pygame
import sys
import os

'''
Objects
'''

class Platform(pygame.sprite.Sprite):
    # x location, y location, img width, img height, img file    
    def __init__(self,xloc,yloc,imgw,imgh,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img)).convert()
        self.image.convert_alpha()
        self.rect = self.image.get_rect()
        self.rect.y = yloc
        self.rect.x = xloc

class Player(pygame.sprite.Sprite):
    '''
    Spawn a player
    '''
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.movex = 0
        self.movey = 0
        self.frame = 0
        self.health = 10
        self.score = 1
        self.images = []
        for i in range(1,9):
            img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert()
            img.convert_alpha()
            img.set_colorkey(ALPHA)
            self.images.append(img)
            self.image = self.images[0]
            self.rect  = self.image.get_rect()

    def control(self,x,y):
        '''
        control player movement
        '''
        self.movex += x
        self.movey += y

    def update(self):
        '''
        Update sprite position
        '''

        self.rect.x = self.rect.x + self.movex
        self.rect.y = self.rect.y + self.movey

        # moving left
        if self.movex < 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[self.frame//ani]

        # moving right
        if self.movex > 0:
            self.frame += 1
            if self.frame > ani*3:
                self.frame = 0
            self.image = self.images[(self.frame//ani)+4]

        # collisions
        enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False)
        for enemy in enemy_hit_list:
            self.health -= 1
            print(self.health)

        ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False)
        for g in ground_hit_list:
            self.health -= 1
            print(self.health)


class Enemy(pygame.sprite.Sprite):
    '''
    Spawn an enemy
    '''
    def __init__(self,x,y,img):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(os.path.join('images',img))
        #self.image.convert_alpha()
        #self.image.set_colorkey(ALPHA)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y
        self.counter = 0

    def move(self):
        '''
        enemy movement
        '''
        distance = 80
        speed = 8

        if self.counter >= 0 and self.counter <= distance:
            self.rect.x += speed
        elif self.counter >= distance and self.counter <= distance*2:
            self.rect.x -= speed
        else:
            self.counter = 0

        self.counter += 1

class Level():
    def bad(lvl,eloc):
        if lvl == 1:
            enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy
            enemy_list = pygame.sprite.Group() # create enemy group
            enemy_list.add(enemy)              # add enemy to group

        if lvl == 2:
            print("Level " + str(lvl) )

        return enemy_list

    def loot(lvl,lloc):
        print(lvl)

    def ground(lvl,gloc,tx,ty):
        ground_list = pygame.sprite.Group()
        i=0
        if lvl == 1:
            while i < len(gloc):
                print("blockgen:" + str(i))
                ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png')
                ground_list.add(ground)
                i=i+1

        if lvl == 2:
            print("Level " + str(lvl) )

        return ground_list

'''
Setup
'''
worldx = 960
worldy = 720

fps = 40 # frame rate
ani = 4  # animation cycles
clock = pygame.time.Clock()
pygame.init()
main = True

BLUE  = (25,25,200)
BLACK = (23,23,23 )
WHITE = (254,254,254)
ALPHA = (0,255,0)

world = pygame.display.set_mode([worldx,worldy])
backdrop = pygame.image.load(os.path.join('images','stage.png')).convert()
backdropbox = world.get_rect()
player = Player() # spawn player
player.rect.x = 0
player.rect.y = 0
player_list = pygame.sprite.Group()
player_list.add(player)
steps = 10 # how fast to move

eloc = []
eloc = [200,20]
gloc = []
#gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630]
tx = 64 #tile size
ty = 64 #tile size

i=0
while i <= (worldx/tx)+tx:
    gloc.append(i*tx)
    i=i+1
    print("block: " + str(i))

enemy_list = Level.bad( 1, eloc )
ground_list = Level.ground( 1,gloc,tx,ty )

'''
Main loop
'''
while main == True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit(); sys.exit()
            main = False

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(-steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(steps,0)
            if event.key == pygame.K_UP or event.key == ord('w'):
                print('jump')

        if event.type == pygame.KEYUP:
            if event.key == pygame.K_LEFT or event.key == ord('a'):
                player.control(steps,0)
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                player.control(-steps,0)
            if event.key == ord('q'):
                pygame.quit()
                sys.exit()
                main = False

#    world.fill(BLACK)
    world.blit(backdrop, backdropbox)
    player.update()
    player_list.draw(world) #refresh player position
    enemy_list.draw(world)  # refresh enemies
    ground_list.draw(world)  # refresh enemies
    for e in enemy_list:
        e.move()
    pygame.display.flip()
    clock.tick(fps)

(LCTT 译注:到本文翻译完为止,该系列已经近一年没有继续更新了~)


via: https://opensource.com/article/18/7/put-platforms-python-game

作者:Seth Kenlon 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

负责任的库作者与其用户的十个约定。

想象一下你是一个造物主,为一个生物设计一个身体。出于仁慈,你希望它能随着时间进化:首先,因为它必须对环境的变化作出反应;其次,因为你的智慧在增长,你对这个小东西想到了更好的设计,它不应该永远保持一个样子。

 title=

然而,这个生物可能有赖于其目前解剖学的特征。你不能无所顾忌地添加翅膀或改变它的身材比例。它需要一个有序的过程来适应新的身体。作为一个负责任的设计者,你如何才能温柔地引导这种生物走向更大的进步呢?

对于负责任的库维护者也是如此。我们向依赖我们代码的人保证我们的承诺:我们会发布 bug 修复和有用的新特性。如果对库的未来有利,我们有时会删除某些特性。我们会不断创新,但我们不会破坏使用我们库的人的代码。我们怎样才能一次实现所有这些目标呢?

添加有用的特性

你的库不应该永远保持不变:你应该添加一些特性,使你的库更适合用户。例如,如果你有一个爬行动物类,并且如果有个可以飞行的翅膀是有用的,那就去添加吧。

class Reptile:
    @property
    def teeth(self):
        return 'sharp fangs'

    # 如果 wings 是有用的,那就添加它!
    @property
    def wings(self):
        return 'majestic wings'

但要注意,特性是有风险的。考虑 Python 标准库中以下功能,看看它出了什么问题。

bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == False

这很奇怪:将任何时间对象转换为布尔值都会得到 True,但午夜时间除外。(更糟糕的是,时区感知时间的规则更加奇怪。)

我已经写了十多年的 Python 了,但直到上周才发现这条规则。这种奇怪的行为会在用户代码中引起什么样的 bug?

比如说一个日历应用程序,它带有一个创建事件的函数。如果一个事件有一个结束时间,那么函数也应该要求它有一个开始时间。

def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time and not start_time:
        raise ValueError("Can't pass end_time without start_time")

    # 女巫集会从午夜一直开到凌晨 4 点
create_event(datetime.date.today(),
             datetime.time(0, 0),
             datetime.time(4, 0))

不幸的是,对于女巫来说,从午夜开始的事件无法通过校验。当然,一个了解午夜怪癖的细心程序员可以正确地编写这个函数。

def create_event(day,
                 start_time=None,
                 end_time=None):
    if end_time is not None and start_time is None:
        raise ValueError("Can't pass end_time without start_time")

但这种微妙之处令人担忧。如果一个库作者想要创建一个伤害用户的 API,那么像午夜的布尔转换这样的“特性”很有效。

 title=

但是,负责任的创建者的目标是使你的库易于正确使用。

这个功能是由 Tim Peters 在 2002 年首次编写 datetime 模块时造成的。即时是像 Tim 这样的奠基 Python 的高手也会犯错误。这个怪异之处后来被消除了,现在所有时间的布尔值都是 True。

# Python 3.5 以后

bool(datetime.time(9, 30)) == True
bool(datetime.time(0, 0)) == True

不知道午夜怪癖的古怪之处的程序员现在可以从这种晦涩的 bug 中解脱出来,但是一想到任何依赖于古怪的旧行为的代码现在没有注意变化,我就会感到紧张。如果从来没有实现这个糟糕的特性,情况会更好。这就引出了库维护者的第一个承诺:

第一个约定:避免糟糕的特性

最痛苦的变化是你必须删除一个特性。一般来说,避免糟糕特性的一种方法是少添加特性!没有充分的理由,不要使用公共方法、类、功能或属性。因此:

第二个约定:最小化特性

特性就像孩子:在充满激情的瞬间孕育,但是它们必须要支持多年(LCTT 译注:我怀疑作者在开车,可是我没有证据)。不要因为你能做傻事就去做傻事。不要画蛇添足!

 title=

但是,当然,在很多情况下,用户需要你的库中尚未提供的东西,你如何选择合适的功能给他们?以下另一个警示故事。

一个来自 asyncio 的警示故事

你可能知道,当你调用一个协程函数,它会返回一个协程对象:

async def my_coroutine():
    pass

print(my_coroutine())
<coroutine object my_coroutine at 0x10bfcbac8>

你的代码必须 “ 等待 await ” 这个对象以此来运行协程。人们很容易忘记这一点,所以 asyncio 的开发人员想要一个“调试模式”来捕捉这个错误。当协程在没有等待的情况下被销毁时,调试模式将打印一个警告,并在其创建的行上进行回溯。

当 Yury Selivanov 实现调试模式时,他添加了一个“协程装饰器”的基础特性。装饰器是一个函数,它接收一个协程并返回任何内容。Yury 使用它在每个协程上接入警告逻辑,但是其他人可以使用它将协程转换为字符串 “hi!”。

import sys

def my_wrapper(coro):
    return 'hi!'

sys.set_coroutine_wrapper(my_wrapper)

async def my_coroutine():
    pass

print(my_coroutine())
hi!

这是一个地狱般的定制。它改变了 “ 异步 async “ 的含义。调用一次 set_coroutine_wrapper 将在全局永久改变所有的协程函数。正如 Nathaniel Smith 所说:“一个有问题的 API” 很容易被误用,必须被删除。如果 asyncio 开发人员能够更好地按照其目标来设计该特性,他们就可以避免删除该特性的痛苦。负责任的创建者必须牢记这一点:

第三个约定:保持特性单一

幸运的是,Yury 有良好的判断力,他将该特性标记为临时,所以 asyncio 用户知道不能依赖它。Nathaniel 可以用更单一的功能替换 set_coroutine_wrapper,该特性只定制回溯深度。

import sys

sys.set_coroutine_origin_tracking_depth(2)

async def my_coroutine():
    pass

print(my_coroutine())
<coroutine object my_coroutine at 0x10bfcbac8>

RuntimeWarning:'my_coroutine' was never awaited

Coroutine created at (most recent call last)
    File "script.py", line 8, in <module>
        print(my_coroutine())

这样好多了。没有可以更改协程的类型的其他全局设置,因此 asyncio 用户无需编写防御代码。造物主应该像 Yury 一样有远见。

第四个约定:标记实验特征“临时”

如果你只是预感你的生物需要犄角和四叉舌,那就引入这些特性,但将它们标记为“临时”。

 title=

你可能会发现犄角是无关紧要的,但是四叉舌是有用的。在库的下一个版本中,你可以删除前者并标记后者为正式的。

删除特性

无论我们如何明智地指导我们的生物进化,总会有一天想要删除一个正式特征。例如,你可能已经创建了一只蜥蜴,现在你选择删除它的腿。也许你想把这个笨拙的家伙变成一条时尚而现代的蟒蛇。

 title=

删除特性主要有两个原因。首先,通过用户反馈或者你自己不断增长的智慧,你可能会发现某个特性是个坏主意。午夜怪癖的古怪行为就是这种情况。或者,最初该特性可能已经很好地适应了你的库环境,但现在生态环境发生了变化,也许另一个神发明了哺乳动物,你的生物想要挤进哺乳动物的小洞穴里,吃掉里面美味的哺乳动物,所以它不得不失去双腿。

 title=

同样,Python 标准库会根据语言本身的变化删除特性。考虑 asyncio 的 Lock 功能,在把 await 作为一个关键字添加进来之前,它一直在等待:

lock = asyncio.Lock()

async def critical_section():
    await lock
    try:
        print('holding lock')
    finally:
        lock.release()

但是现在,我们可以做“异步锁”:

lock = asyncio.Lock()

async def critical_section():
    async with lock:
        print('holding lock')

新方法好多了!很短,并且在一个大函数中使用其他 try-except 块时不容易出错。因为“尽量找一种,最好是唯一一种明显的解决方案”,旧语法在 Python 3.7 中被弃用,并且很快就会被禁止。

不可避免的是,生态变化会对你的代码产生影响,因此要学会温柔地删除特性。在此之前,请考虑删除它的成本或好处。负责任的维护者不会愿意让用户更改大量代码或逻辑。(还记得 Python 3 在重新添加会 u 字符串前缀之前删除它是多么痛苦吗?)如果代码删除是机械性的动作,就像一个简单的搜索和替换,或者如果该特性是危险的,那么它可能值得删除。

是否删除特性

 title=

反对支持
代码必须改变改变是机械性的
逻辑必须改变特性是危险的

就我们饥饿的蜥蜴而言,我们决定删除它的腿,这样它就可以滑进老鼠洞里吃掉它。我们该怎么做呢?我们可以删除 walk 方法,像下面一样修改代码:

class Reptile:
    def walk(self):
        print('step step step')

变成这样:

class Reptile:
    def slither(self):
        print('slide slide slide')

这不是一个好主意,这个生物习惯于走路!或者,就库而言,你的用户拥有依赖于现有方法的代码。当他们升级到最新库版本时,他们的代码将会崩溃。

# 用户的代码,哦,不!
Reptile.walk()

因此,负责任的创建者承诺:

第五条预定:温柔地删除

温柔地删除一个特性需要几个步骤。从用腿走路的蜥蜴开始,首先添加新方法 slither。接下来,弃用旧方法。

import warnings

class Reptile:
    def walk(self):
        warnings.warn(
                "walk is deprecated, use slither",
                 DeprecationWarning, stacklevel=2)
        print('step step step')

    def slither(self):
        print('slide slide slide')

Python 的 warnings 模块非常强大。默认情况下,它会将警告输出到 stderr,每个代码位置只显示一次,但你可以禁用警告或将其转换为异常,以及其它选项。

一旦将这个警告添加到库中,PyCharm 和其他 IDE 就会使用删除线呈现这个被弃用的方法。用户马上就知道该删除这个方法。

Reptile().walk()

当他们使用升级后的库运行代码时会发生什么?

$ python3 script.py

DeprecationWarning: walk is deprecated, use slither
    script.py:14: Reptile().walk()

step step step

默认情况下,他们会在 stderr 上看到警告,但脚本会成功并打印 “step step step”。警告的回溯显示必须修复用户代码的哪一行。(这就是 stacklevel 参数的作用:它显示了用户需要更改的调用,而不是库中生成警告的行。)请注意,错误消息有指导意义,它描述了库用户迁移到新版本必须做的事情。

你的用户可能会希望测试他们的代码,并证明他们没有调用弃用的库方法。仅警告不会使单元测试失败,但异常会失败。Python 有一个命令行选项,可以将弃用警告转换为异常。

> python3 -Werror::DeprecationWarning script.py

Traceback (most recent call last):
    File "script.py", line 14, in <module>
        Reptile().walk()
    File "script.py", line 8, in walk
        DeprecationWarning, stacklevel=2)
DeprecationWarning: walk is deprecated, use slither

现在,“step step step” 没有输出出来,因为脚本以一个错误终止。

因此,一旦你发布了库的一个版本,该版本会警告已启用的 walk 方法,你就可以在下一个版本中安全地删除它。对吧?

考虑一下你的库用户在他们项目的 requirements 中可能有什么。

# 用户的 requirements.txt 显示 reptile 包的依赖关系
reptile

下次他们部署代码时,他们将安装最新版本的库。如果他们尚未处理所有的弃用,那么他们的代码将会崩溃,因为代码仍然依赖 walk。你需要温柔一点,你必须向用户做出三个承诺:维护更改日志,选择版本化方案和编写升级指南。

第六个约定:维护变更日志

你的库必须有更改日志,其主要目的是宣布用户所依赖的功能何时被弃用或删除。

版本 1.1 中的更改

新特性

  • 新功能 Reptile.slither()

弃用

  • Reptile.walk() 已弃用,将在 2.0 版本中删除,请使用 slither()

负责任的创建者会使用版本号来表示库发生了怎样的变化,以便用户能够对升级做出明智的决定。“版本化方案”是一种用于交流变化速度的语言。

第七个约定:选择一个版本化方案

有两种广泛使用的方案,语义版本控制和基于时间的版本控制。我推荐任何库都进行语义版本控制。Python 的风格在 PEP 440 中定义,像 pip 这样的工具可以理解语义版本号。

如果你为库选择语义版本控制,你可以使用版本号温柔地删除腿,例如:

1.0: 第一个“稳定”版,带有 walk() 1.1: 添加 slither(),废弃 walk() 2.0: 删除 walk()

你的用户依赖于你的库的版本应该有一个范围,例如:

# 用户的 requirements.txt
reptile>=1,<2

这允许他们在主要版本中自动升级,接收错误修正并可能引发一些弃用警告,但不会升级到个主要版本并冒着更改破坏其代码的风险。

如果你遵循基于时间的版本控制,则你的版本可能会编号:

2017.06.0: 2017 年 6 月的版本 2018.11.0: 添加 slither(),废弃 walk() 2019.04.0: 删除 walk()

用户可以这样依赖于你的库:

# 用户的 requirements.txt,基于时间控制的版本
reptile==2018.11.*

这非常棒,但你的用户如何知道你的版本方案,以及如何测试代码来进行弃用呢?你必须告诉他们如何升级。

第八个约定:写一个升级指南

下面是一个负责任的库创建者如何指导用户:

升级到 2.0

从弃用的 API 迁移

请参阅更改日志以了解已弃用的特性。

启用弃用警告

升级到 1.1 并使用以下代码测试代码:

python -Werror::DeprecationWarning

​​​​​​ 现在可以安全地升级了。

你必须通过向用户显示命令行选项来教会用户如何处理弃用警告。并非所有 Python 程序员都知道这一点 —— 我自己就每次都得查找这个语法。注意,你必须发布一个版本,它输出来自每个弃用的 API 的警告,以便用户可以在再次升级之前使用该版本进行测试。在本例中,1.1 版本是小版本。它允许你的用户逐步重写代码,分别修复每个弃用警告,直到他们完全迁移到最新的 API。他们可以彼此独立地测试代码和库的更改,并隔离 bug 的原因。

如果你选择语义版本控制,则此过渡期将持续到下一个主要版本,从 1.x 到 2.0,或从 2.x 到 3.0 以此类推。删除生物腿部的温柔方法是至少给它一个版本来调整其生活方式。不要一次性把腿删掉!

 title=

版本号、弃用警告、更改日志和升级指南可以协同工作,在不违背与用户约定的情况下温柔地改进你的库。Twisted 项目的兼容性政策 解释的很漂亮:

“先行者总是自由的”

运行的应用程序在没有任何警告的情况下都可以升级为 Twisted 的一个次要版本。

换句话说,任何运行其测试而不触发 Twisted 警告的应用程序应该能够将其 Twisted 版本升级至少一次,除了可能产生新警告之外没有任何不良影响。

现在,我们的造物主已经获得了智慧和力量,可以通过添加方法来添加特性,并温柔地删除它们。我们还可以通过添加参数来添加特性,但这带来了新的难度。你准备好了吗?

添加参数

想象一下,你只是给了你的蛇形生物一对翅膀。现在你必须允许它选择是滑行还是飞行。目前它的 move 功能只接受一个参数。

# 你的库代码
def move(direction):
    print(f'slither {direction}')

# 用户的应用
move('north')

你想要添加一个 mode 参数,但如果用户升级库,这会破坏他们的代码,因为他们只传递了一个参数。

# 你的库代码
def move(direction, mode):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 一个用户的代码,出现错误!
move('north')

一个真正聪明的创建者者会承诺不会以这种方式破坏用户的代码。

第九条约定:兼容地添加参数

要保持这个约定,请使用保留原始行为的默认值添加每个新参数。

# 你的库代码
def move(direction, mode='slither'):
    assert mode in ('slither', 'fly')
    print(f'{mode} {direction}')

# 用户的应用
move('north')

随着时间推移,参数是函数演化的自然历史。它们首先列出最老的参数,每个都有默认值。库用户可以传递关键字参数以选择特定的新行为,并接受所有其他行为的默认值。

# 你的库代码
def move(direction,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户应用
move('north', extra_sinuous=True)

但是有一个危险,用户可能会编写如下代码:

# 用户应用,简写
move('north', 'slither', False, True)

如果在你在库的下一个主要版本中去掉其中一个参数,例如 turbo,会发生什么?

# 你的库代码,下一个主要版本中 "turbo" 被删除
def move(direction,
         mode='slither',
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户应用,简写
move('north', 'slither', False, True)

用户的代码仍然能编译,这是一件坏事。代码停止了曲折的移动并开始招呼 Lyft,这不是它的本意。我相信你可以预测我接下来要说的内容:删除参数需要几个步骤。当然,首先弃用 trubo 参数。我喜欢这种技术,它可以检测任何用户的代码是否依赖于这个参数。

# 你的库代码
_turbo_default = object()

def move(direction,
         mode='slither',
         turbo=_turbo_default,
         extra_sinuous=False,
         hail_lyft=False):
    if turbo is not _turbo_default:
        warnings.warn(
                "'turbo' is deprecated",
                DeprecationWarning,
                stacklevel=2)
    else:
        # The old default.
        turbo = False

但是你的用户可能不会注意到警告。警告声音不是很大:它们可以在日志文件中被抑制或丢失。用户可能会漫不经心地升级到库的下一个主要版本——那个删除 turbo 的版本。他们的代码运行时将没有错误、默默做错误的事情!正如 Python 之禅所说:“错误绝不应该被默默 pass”。实际上,爬行动物的听力很差,所有当它们犯错误时,你必须非常大声地纠正它们。

 title=

保护用户的最佳方法是使用 Python 3 的星型语法,它要求调用者传递关键字参数。

# 你的库代码
# 所有 “*” 后的参数必须以关键字方式传输。
def move(direction,
         *,
         mode='slither',
         turbo=False,
         extra_sinuous=False,
         hail_lyft=False):
    # ...

# 用户代码,简写
# 错误!不能使用位置参数,关键字参数是必须的
move('north', 'slither', False, True)

有了这个星,以下是唯一允许的语法:

# 用户代码
move('north', extra_sinuous=True)

现在,当你删除 turbo 时,你可以确定任何依赖于它的用户代码都会明显地提示失败。如果你的库也支持 Python2,这没有什么大不了。你可以模拟星型语法(归功于 Brett Slatkin):

# 你的库代码,兼容 Python 2
def move(direction, **kwargs):
    mode = kwargs.pop('mode', 'slither')
    turbo = kwargs.pop('turbo', False)
    sinuous = kwargs.pop('extra_sinuous', False)
    lyft = kwargs.pop('hail_lyft', False)

    if kwargs:
        raise TypeError('Unexpected kwargs: %r' 
                        % kwargs)

# ...

要求关键字参数是一个明智的选择,但它需要远见。如果允许按位置传递参数,则不能仅在以后的版本中将其转换为仅关键字。所以,现在加上星号。你可以在 asyncio API 中观察到,它在构造函数、方法和函数中普遍使用星号。尽管到目前为止,Lock 只接受一个可选参数,但 asyncio 开发人员立即添加了星号。这是幸运的。

# In asyncio.
class Lock:
    def __init__(self, *, loop=None):
        # ...

现在,我们已经获得了改变方法和参数的智慧,同时保持与用户的约定。现在是时候尝试最具挑战性的进化了:在不改变方法或参数的情况下改变行为。

改变行为

假设你创造的生物是一条响尾蛇,你想教它一种新行为。

 title=

横向移动!这个生物的身体看起来是一样的,但它的行为会发生变化。我们如何为这一进化步骤做好准备?

Image by HCA [CC BY-SA 4.0], via Wikimedia Commons, 由 Opensource.com 修改

当行为在没有新函数或新参数的情况下发生更改时,负责任的创建者可以从 Python 标准库中学习。很久以前,os 模块引入了 stat 函数来获取文件统计信息,比如创建时间。起初,这个时间总是整数。

>>> os.stat('file.txt').st_ctime
1540817862

有一天,核心开发人员决定在 os.stat 中使用浮点数来提供亚秒级精度。但他们担心现有的用户代码还没有做好准备更改。于是他们在 Python 2.3 中创建了一个设置 stat_float_times,默认情况下是 False 。用户可以将其设置为 True 来选择浮点时间戳。

>>> # Python 2.3.
>>> os.stat_float_times(True)
>>> os.stat('file.txt').st_ctime
1540817862.598021

从 Python 2.5 开始,浮点时间成为默认值,因此 2.5 及之后版本编写的任何新代码都可以忽略该设置并期望得到浮点数。当然,你可以将其设置为 False 以保持旧行为,或将其设置为 True 以确保所有 Python 版本都得到浮点数,并为删除 stat_float_times 的那一天准备代码。

多年过去了,在 Python 3.1 中,该设置已被弃用,以便为人们为遥远的未来做好准备,最后,经过数十年的旅程,这个设置被删除。浮点时间现在是唯一的选择。这是一个漫长的过程,但负责任的神灵是有耐心的,因为我们知道这个渐进的过程很有可能于意外的行为变化拯救用户。

第十个约定:逐渐改变行为

以下是步骤:

  • 添加一个标志来选择新行为,默认为 False,如果为 False 则发出警告
  • 将默认值更改为 True,表示完全弃用标记
  • 删除该标志

如果你遵循语义版本控制,版本可能如下:

库版本库 API用户代码
1.0没有标志预期的旧行为
1.1添加标志,默认为 False,如果是 False,则警告设置标志为 True,处理新行为
2.0改变默认为 True,完全弃用标志处理新行为
3.0移除标志处理新行为

你需要个主要版本来完成该操作。如果你直接从“添加标志,默认为 False,如果是 False 则发出警告”变到“删除标志”,而没有中间版本,那么用户的代码将无法升级。为 1.1 正确编写的用户代码必须能够升级到下一个版本,除了新警告之外,没有任何不良影响,但如果在下一个版本中删除了该标志,那么该代码将崩溃。一个负责任的神明从不违反扭曲的政策:“先行者总是自由的”。

负责任的创建者

 title=

我们的 10 个约定大致可以分为三类:

谨慎发展

  1. 避免不良功能
  2. 最小化特性
  3. 保持功能单一
  4. 标记实验特征“临时”
  5. 温柔删除功能

严格记录历史

  1. 维护更改日志
  2. 选择版本方案
  3. 编写升级指南

缓慢而明显地改变

  1. 兼容添加参数
  2. 逐渐改变行为

如果你对你所创造的物种保持这些约定,你将成为一个负责任的造物主。你的生物的身体可以随着时间的推移而进化,一直在改善和适应环境的变化,而不是在生物没有准备好就突然改变。如果你维护一个库,请向用户保留这些承诺,这样你就可以在不破坏依赖该库的代码的情况下对库进行更新。


这篇文章最初是在 A. Jesse Jiryu Davis 的博客上'出现的,经允许转载。

插图参考:


via: https://opensource.com/article/19/5/api-evolution-right-way

作者:A. Jesse 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

大约 80% 的 Web 应用程序由 PHP 提供支持。类似地,SQL 也是如此。PHP 5.5 版本之前,我们有用于访问 MySQL 数据库的 mysql\_ 命令,但由于安全性不足,它们最终被弃用。

弃用这件事是发生在 2013 年的 PHP 5.5 上,我写这篇文章的时间是 2018 年,PHP 版本为 7.2。mysql\_ 的弃用带来了访问数据库的两种主要方法:mysqli 和 PDO 库。

虽然 mysqli 库是官方指定的,但由于 mysqli 只能支持 mysql 数据库,而 PDO 可以支持 12 种不同类型的数据库驱动程序,因此 PDO 获得了更多的赞誉。此外,PDO 还有其它一些特性,使其成为大多数开发人员的更好选择。你可以在下表中看到一些特性比较:

PDOMySQLi
数据库支持12 种驱动只有 MySQL
范例OOP过程 + OOP
预处理语句(客户端侧)YesNo
1命名参数YesNo

现在我想对于大多数开发人员来说,PDO 是首选的原因已经很清楚了。所以让我们深入研究它,并希望在本文中尽量涵盖关于 PDO 你需要的了解的。

连接

第一步是连接到数据库,由于 PDO 是完全面向对象的,所以我们将使用 PDO 类的实例。

我们要做的第一件事是定义主机、数据库名称、用户名、密码和数据库字符集。

$host = 'localhost';
$db   = 'theitstuff';
$user = 'root';
$pass = 'root';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$conn = new PDO($dsn, $user, $pass);

之后,正如你在上面的代码中看到的,我们创建了 DSN 变量,DSN 变量只是一个保存数据库信息的变量。对于一些在外部服务器上运行 MySQL 的人,你还可以通过提供一个 port=$port_number 来调整端口号。

最后,你可以创建一个 PDO 类的实例,我使用了 $conn 变量,并提供了 $dsn$user$pass 参数。如果你遵循这些步骤,你现在应该有一个名为 $conn 的对象,它是 PDO 连接类的一个实例。现在是时候进入数据库并运行一些查询。

一个简单的 SQL 查询

现在让我们运行一个简单的 SQL 查询。

$tis = $conn->query('SELECT name, age FROM students');
while ($row = $tis->fetch())
{
  echo $row['name']."\t";
  echo $row['age'];
  echo "<br>";
}

这是使用 PDO 运行查询的最简单形式。我们首先创建了一个名为 tis(TheITStuff 的缩写 )的变量,然后你可以看到我们使用了创建的 $conn 对象中的查询函数。

然后我们运行一个 while 循环并创建了一个 $row 变量来从 $tis 对象中获取内容,最后通过调用列名来显示每一行。

很简单,不是吗?现在让我们来看看预处理语句。

预处理语句

预处理语句是人们开始使用 PDO 的主要原因之一,因为它提供了可以阻止 SQL 注入的语句。

有两种基本方法可供使用,你可以使用位置参数或命名参数。

位置参数

让我们看一个使用位置参数的查询示例。

$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(?, ?)");
$tis->bindValue(1,'mike');
$tis->bindValue(2,22);
$tis->execute();

在上面的例子中,我们放置了两个问号,然后使用 bindValue() 函数将值映射到查询中。这些值绑定到语句问号中的位置。

我还可以使用变量而不是直接提供值,通过使用 bindParam() 函数相同例子如下:

$name='Rishabh'; $age=20;
$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(?, ?)");
$tis->bindParam(1,$name);
$tis->bindParam(2,$age);
$tis->execute();

命名参数

命名参数也是预处理语句,它将值/变量映射到查询中的命名位置。由于没有位置绑定,因此在多次使用相同变量的查询中非常有效。

$name='Rishabh'; $age=20;
$tis = $conn->prepare("INSERT INTO STUDENTS(name, age) values(:name, :age)");
$tis->bindParam(':name', $name);
$tis->bindParam(':age', $age);
$tis->execute();

你可以注意到,唯一的变化是我使用 :name:age 作为占位符,然后将变量映射到它们。冒号在参数之前使用,让 PDO 知道该位置是一个变量,这非常重要。

你也可以类似地使用 bindValue() 来使用命名参数直接映射值。

获取数据

PDO 在获取数据时非常丰富,它实际上提供了许多格式来从数据库中获取数据。

你可以使用 PDO::FETCH_ASSOC 来获取关联数组,PDO::FETCH_NUM 来获取数字数组,使用 PDO::FETCH_OBJ 来获取对象数组。

$tis = $conn->prepare("SELECT * FROM STUDENTS");
$tis->execute();
$result = $tis->fetchAll(PDO::FETCH_ASSOC);

你可以看到我使用了 fetchAll,因为我想要所有匹配的记录。如果只需要一行,你可以简单地使用 fetch

现在我们已经获取了数据,现在是时候循环它了,这非常简单。

foreach ($result as $lnu){
  echo $lnu['name'];
  echo $lnu['age']."<br>";
}

你可以看到,因为我请求了关联数组,所以我正在按名称访问各个成员。

虽然在定义希望如何传输递数据方面没有要求,但在定义 $conn 变量本身时,实际上可以将其设置为默认值。

你需要做的就是创建一个 $options 数组,你可以在其中放入所有默认配置,只需在 $conn 变量中传递数组即可。

$options = [
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];
$conn = new PDO($dsn, $user, $pass, $options);

这是一个非常简短和快速的 PDO 介绍,我们很快就会制作一个高级教程。如果你在理解本教程的任何部分时遇到任何困难,请在评论部分告诉我,我会在那你为你解答。


via: http://www.theitstuff.com/easiest-pdo-tutorial-basics

作者:Rishabh Kandari 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Slimbook Battery Optimizer 是一个美观实用的指示器小程序,它可以让你在安装了 Linux 的笔记本上快速切换电源模式来延长续航时间。

Slimbook 是一个销售 预装 Linux 的笔记本电脑 的西班牙电脑制造商,他们发布了一款好用的小程序,用来在基于 Ubuntu 的 Linux 发行版下调整电池性能。

因为 Slimbook 销售他们自己的 Linux 系统,所以他们制作了一些在 Linux 上用于调整他们自己硬件性能的小工具。Battery Optimizer 就是这样一个工具。

要使用这个实用小程序,你不必购买 Slimbook 的产品,因为 Slimbook 已经将它在 他们的官方 PPA 源 发行了。

Slimbook Battery Optimizer 简介

这个程序叫 Slimbook Battery。它是一个常驻顶栏的指示器小程序,使得你可以快速切换电源模式。

Slimbook Battery Mode Ubuntu

你可能在 Windows 中见过类似的程序。Slimbook Battery 和它们一样,提供了类似的电源计划:

  • 节能:最大程度延长电池续航时间
  • 平衡:性能与节能间的最佳平衡
  • 高性能:最大程度提高性能

你可以在高级模式中配置这些模式:

配置多种多样的电源模式

如果你觉得你把设置调乱了,你可以用“恢复默认设置”的按钮还原它。

你也可以修改像程序自启或默认电源模式这样的通用设置。

Slimbook Battery 通用设置

Slimbook 有专门为多种电源管理参数提供的页面。如果你希望自己配置,请参照 此页

不过,我认为 Slimbook 的界面需要一些改进。例如,某些页面上的“问题标记”的图标应该改为可点击的,以此提供更多信息。然而,在我写这篇文章时,那个标记仍然无法点击。

总的来说,Slimbook Battery 是一个小巧精美的软件,你可以用它来快速切换电源模式。如果你决定在 Ubuntu 及其衍生发行版上(比如 Linux Mint 或 elementary OS 等),你可以使用官方 PPA 源

在基于 Ubuntu 的发行版上安装 Slimbook Battery

打开终端,一步一步地使用以下命令:

sudo add-apt-repository ppa:slimbook/slimbook
sudo apt update
sudo apt install slimbookbattery

安装好之后,在菜单中搜索 Slimbook Battery:

启动 Slimbook Battery Optimizer

在你点击它之后,你会发现它出现在了顶栏。你可以在这里选择你希望使用的电源模式。

Slimbook Battery 电源模式

卸载 Slimbook Battery

如果你不再使用它,你可以通过以下命令来卸载它:

sudo apt remove slimbookbattery
sudo add-apt-repository -r ppa:slimbook/slimbook

在我看来,这样的应用程序为某些特定的目的服务,这是值得鼓励的。这个工具给了你一条调整电源模式的捷径,和调整性能的更多选项。

你用过 Slimbook Battery 吗?你觉得它如何?


via: https://itsfoss.com/slimbook-battry-optimizer-ubuntu/

作者:Abhishek Prakash 选题:lujun9972 译者:zhs852 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

半个月前,我们推出了一个“文章助手”的小程序,用于解决在微信公众号文章中无法放置可点击的链接的问题。

可能对于我们这种技术向的文章来说,很多时候都需要插入链接,而一个认真的读者也经常希望可以点击链接看看。在我们从 2013 年开始运营公众号以来,我们对此问题有过几种解决方案:

  • 直接将链接以文字的方式显示在正文中,读者需要手工选择并复制链接,然后另外在浏览器中打开
  • 类似于文章脚注一样,将链接放置在文末,读者需要翻到文末,根据所要找的链接标号来复制并在浏览器中打开
  • 将链接整理后放在另外一个 web 页面中,然后通过“阅读原文”的方式引导读者去点击,在其中完成链接功能

但是这几种方法都不太尽如人意,读者经常还是会下意识去点击文章内该出现链接的地方。所以,我们最近又有了新的解决方案。我们利用微信小程序的能力,在微信公众号文章内,采用小程序链接来替代外部链接;点击小程序链接后会打开该小程序,自动复制外部链接到剪贴板;打开浏览器(自动)贴入剪贴板中的链接来访问。

应该说,效果还是达到了我们的预期,虽然有些功能限于小程序本身支持无法做到,比如无法得到来源公众号的信息、无法主动唤起浏览器等。

不过,在使用过程中,公众号编辑们发现对链接一个个进行替换非常繁琐,所以,我决定给这个“文章助手”提供一个“助手”。我做了一个静态页面,在此页面内,只要将你编辑的公众号文章内容贴入其中的输入框,一键点击即可将全部链接转换为“文章助手”链接。

此外,经常还有认识或不认识的朋友,对我们的公众号排版表示好奇,比如这种 注释性的标签 Ruby tag 是怎么回事?也有人希望采用 PingFang 字体,这个是微信编辑器默认不提供的。这次我们就一便提供好了。

“文章助手”的助手地址如下: https://linux.cn/static/tools/a.html

  • 这个页面是纯静态页面,你可以连着其中使用的 jQuery 保存下来自行使用。
  • 该小程序永久免费,并永久不添加第三方广告。
  • 除不可抗力(如被微信官方封杀,但目前我们判断并未触犯微信使用规则),该小程序会一直运营下去。
  • 贴入输入框的文章内容可能暂时不能显示来自微信已发送文章内的图片,但是并不影响转换和转换后再贴入微信编辑器内使用。
  • 在输入框中选定格式“中文(English)”这样的内容时,点击下方的“转换 RUBY”的按钮,会将该字符串转换为 中文 English 样式。
  • 如果要全文使用“PingFang”字体,转换前勾选即可(最显眼的区别是,逗号和句号是垂直居中而不是底线对齐的)。

好了,老王的这些家底都给你们了,祝你们的公众号文章看起来越来越专业、越来越漂亮。

Ispell 可以帮助你在纯文本中消除超过 50 种语言的拼写错误。

好的拼写是一种技巧。它是一项需要时间学习和掌握的技能。也就是说,有些人从来没有完全掌握这种技能,我知道有两三个出色的作家就无法完全掌握拼写。

即使你拼写得很好,偶尔也会输入错字。特别是在最后期限前如果你快速敲击键盘,那就更是如此。无论你的拼写的是什么,通过拼写检查器检查你所写的内容总是一个好主意。

我用纯文本完成了我的大部分写作,并经常使用名为 Aspell 的命令行拼写检查器来完成这项工作。Aspell 不是唯一的工具。你可能还想要看下不错的 Ispell

入门

自 1971 年以来,Ispell 就以各种形式出现过。不要被它的年龄欺骗。Ispell 仍然是一个可以在 21 世纪高效使用的应用。

在开始之前,请打开终端窗口并输入 which ispell 来检查计算机上是否安装了 Ispell。如果未安装,请打开发行版的软件包管理器并从那里安装 Ispell。

不要忘记为你使用的语言安装词典。我唯一使用的语言是英语,所以我只需下载美国和英国英语字典。你可以不局限于我的(也是唯一的)母语。Ispell 有超过 50 种语言的词典

 title=

使用 Ispell

如果你还没有猜到,Ispell 只能用在文本文件。这包括用 HTML、LaTeX 和 nroff 或 troff 标记的文档。之后会有更多相关内容。

要开始使用,请打开终端窗口并进入包含要运行拼写检查的文件的目录。输入 ispell 后跟文件名,然后按回车键。

 title=

Ispell 高亮了它无法识别的第一个词。如果单词拼写错误,Ispell 通常会提供一个或多个备选方案。按下 R,然后按下正确选择旁边的数字。在上面的截图中,我按了 R0 来修复错误。

另一方面,如果单词拼写正确,请按下 A 然后移动到下一个拼写错误的单词。

继续这样做直到到达文件的末尾。Ispell 会保存你的更改,创建你刚检查的文件的备份(扩展名为 .bak),然后关闭。

其他几个选项

此示例说明了 Ispell 的基本用法。这个程序有很多选项,有些你可能会用到,而另一些你可能永远不会使用。让我们快速看下我经常使用的一些。

之前我提到过 Ispell 可以用于某些标记语言。你需要告诉它文件的格式。启动 Ispell 时,为 TeX 或 LaTeX 文件添加 -t,为 HTML 文件添加 -H,对于 groff 或 troff 文件添加 -n。例如,如果输入 ispell -t myReport.tex,Ispell 将忽略所有标记。

如果你不想在检查文件后创建备份文件,请将 -x 添加到命令行。例如,ispell -x myFile.txt

如果 Ispell 遇到拼写正确但不在其字典中的单词,比如名字,会发生什么?你可以按 I 将该单词添加到个人单词列表中。这会将单词保存到 /home 目录下的 .ispell_default 的文件中。

这些是我在使用 Ispell 时最有用的选项,但请查看 Ispell 的手册页以了解其所有选项。

Ispell 比 Aspell 或其他命令行拼写检查器更好或者更快么?我会说它不比其他的差或者慢。Ispell 不是适合所有人。它也许也不适合你。但有更多选择也不错,不是么?


via: https://opensource.com/article/19/5/spelling-command-line-ispell

作者:Scott Nesbitt 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出