TowardsDataScience 2023 博客中文翻译(二百六十五)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

Python getattr() 函数解释

原文:towardsdatascience.com/python-getattr-function-explained-pyshark-cc7f49c59b2e

在本文中,我们将探讨如何使用 Python getattr() 函数。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Misha Sv

·发表于 Towards Data Science ·4 分钟阅读·2023 年 3 月 20 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Shane Aldendorff 拍摄于 Unsplash

目录

  • 介绍

  • 使用 getattr() 动态访问对象的属性

  • 使用 getattr() 构建动态 API

  • 使用 getattr() 动态加载模块

  • 结论

介绍

Python getattr() 函数是一个内置函数,允许动态访问对象的属性。具体来说,它用于检索 Python 对象的名称属性。

Python getattr() 函数的语法是:

getattr(object, name[, default])

其中:

  • object — 我们希望从中检索属性的 Python 对象

  • name — Python 对象中命名属性的名称

  • default — 可选参数,用于指定如果未找到指定属性时的返回值。如果未指定,代码将返回AttributeError

getattr() 函数在调用时,会搜索指定的 Python 对象中的名称属性并返回其值。

在接下来的章节中,我们将探讨一些 getattr() 函数的常见使用案例。

使用 getattr() 动态访问对象的属性

Python getattr() 函数最流行的使用案例之一是动态访问对象的属性。

让我们开始创建一个新的 Python 对象 Car,它有三个属性(makemodelprice):

class Car:

    def __init__(self, make, model, price):

        self.make = make
        self.model = model
        self.price = price

接下来,我们将创建一个带有一些示例值的该类实例:

car = Car('Audi', 'Q7', 100000)

现在我们可以使用 getattr() 函数动态访问这个类的属性。

例如,假设我们想要检索刚刚创建的 car 对象的 price 属性:

attr_name = 'price'

attr_value = getattr(car, attr_name)

print(attr_value)

你应该得到:

100000

如果你尝试检索对象没有的属性,你会看到 AttributeError

例如,这个对象没有属性colour,所以让我们看看当我们尝试检索它时会发生什么:

attr_name = 'colour'

attr_value = getattr(car, attr_name)

print(attr_value)

你应该得到:

AttributeError: 'Car' object has no attribute 'colour'

如果你正在处理多个类,而不知道它们是否具有你正在寻找的属性,这种方法非常有用,它可以节省大量时间和代码量,快速运行这些测试以检索属性值。

使用 getattr() 构建动态 API

Python getattr() 函数的另一个用例是构建 Python 中的动态 API。

让我们开始创建一个简单的 Calculator 类,包含几个执行数学计算的方法:

class Calculator:

    def add(self, x, y):
        return x + y

    def subtract(self, x, y):
        return x - y

现在我们可以围绕这个 Calculator 类构建一个 API,它将允许动态调用任何方法(使用 Python getattr() 函数):

class CalculatorAPI:

    def __init__(self, calculator):

        self.calculator = calculator

    def call_method(self, method_name, *args):

        method = getattr(self.calculator, method_name, None)

        if method:
            return method(*args)
        else:
            return f"Method '{method_name}' not found"

一旦 API 构建完成,我们可以用不同的计算,如加法和减法来测试它,并检查结果:

calculator = Calculator()

api = CalculatorAPI(calculator)

print(api.call_method("add", 7, 8))
print(api.call_method("subtract", 9, 1))

你应该得到:

15
8

在这个例子中,我们使用 Python getattr() 函数动态访问 Python 类的所需方法。

使用 getattr() 动态加载模块

Python getattr() 函数的另一个用例是在运行时动态加载模块。

在这个例子中,我们将使用一个内置的 Python 模块,这实际上是 import 语句的实现。具体来说,我们将使用 import_module() 函数进行编程导入。

我们将使用 getattr() 函数来访问加载模块中的特定函数。

假设我们想构建一个小程序,询问用户要导入哪个模块、要访问该模块的哪个函数以及要执行什么操作。

例如,我们想导入数学模块,访问 sqrt() 函数并找到 25 的平方根。

我们将以编程方式加载模块和函数,并执行计算:

#Import the required dependency
import importlib

#Define module name
module_name = 'math'

#Programmatically load module
module = importlib.import_module(module_name)

#Define function name
function_name = 'sqrt'

#Programmatically load function
function = getattr(module, function_name)

#Define input for the function
num = 25

#Calculate the result
result = function(num)

#Print the result
print(f"Result: {result}")

你应该得到:

5.0

虽然这是一个非常简单的例子,看起来不像是 sqrt() 函数的有用应用,但它说明了动态加载模块和函数的一般思路。

结论

在这篇文章中,我们探讨了 Python getattr() 函数。

现在你已经了解了基本功能,你可以在项目中练习使用它,以向代码中添加更多功能。

如果你有任何问题或建议,请随时在下面留言,查看更多我的 Python Functions 教程。

最初发布于 https://pyshark.com 2023 年 3 月 20 日。

Python help() 函数解释

原文:towardsdatascience.com/python-help-function-explained-fba9c15f42b1

在这篇文章中,我们将探讨如何使用 Python help() 函数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Misha Sv

·发布于 Towards Data Science ·4 分钟阅读·2023 年 1 月 13 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Toa Heftiba 提供,来源于 Unsplash

目录

  • 介绍

  • 使用交互式帮助工具访问文档

  • 使用 help() 访问对象文档

  • 使用 help() 访问用户定义的函数文档

  • 结论

介绍

在 Python 中,我们经常使用新的模块、函数、类或对象,这些模块、函数、类或对象我们以前没有使用过,且这些文档我们还没有阅读过。

我们可以使用 Python help() 函数来更快地获取这些信息,而不是浏览文档网站寻找特定的函数或类。

Python help() 函数用于显示指定模块、函数、类或对象的文档。

help() 函数的处理定义如下:

help([object]) -> display documentation

使用交互式帮助工具访问文档

你可以在不带任何参数的情况下调用 Python help() 函数,它会启动一个交互提示符,你可以利用它来查找任何 Python 对象的文档。

让我们启动交互式帮助工具:

#Start help utility
help()

你应该会看到一个帮助工具在终端中启动:

Welcome to Python 3.7's help utility!

If this is your first time using Python, you should definitely check out
the tutorial on the Internet at https://docs.python.org/3.7/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To quit this help utility and
return to the interpreter, just type "quit".

To get a list of available modules, keywords, symbols, or topics, type
"modules", "keywords", "symbols", or "topics".  Each module also comes
with a one-line summary of what it does; to list the modules whose name
or summary contain a given string such as "spam", type "modules spam".

help>

一旦帮助工具启动,我们可以利用它查找 Python 对象的文档。

例如,让我们尝试在帮助工具中运行 map 查找 Python map() 函数 的文档:

help> map

你应该会得到函数文档:

Help on class map in module builtins:

class map(object)
 |  map(func, *iterables) --> map object
 |  
 |  Make an iterator that computes the function using arguments from
 |  each of the iterables.  Stops when the shortest iterable is exhausted.
 |  
 |  Methods defined here:
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __next__(self, /)
 |      Implement next(self).
 |  
 |  __reduce__(...)
 |      Return state information for pickling.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.

如你所见,文档包含函数描述、方法和文档字符串。

使用 help() 访问对象文档

你可以在不使用交互式帮助工具的情况下,一步访问 Python 对象文档。

只需以以下格式运行 Python help() 函数,并将 Python 对象作为参数传递进去:

help([object])

让我们尝试使用这种方法访问Python map() 函数的文档:

#Find documentation of Python map() function
help(map)

并且你应该会得到:

Help on class map in module builtins:

class map(object)
 |  map(func, *iterables) --> map object
 |  
 |  Make an iterator that computes the function using arguments from
 |  each of the iterables.  Stops when the shortest iterable is exhausted.
 |  
 |  Methods defined here:
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __next__(self, /)
 |      Implement next(self).
 |  
 |  __reduce__(...)
 |      Return state information for pickling.
 |  
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.

如你所见,显示的文档与我们使用交互式帮助工具找到的文档是一样的。

使用 help() 访问用户定义函数的文档

Python help() 函数也可以显示用户定义函数的信息。

在之前的示例中,我们访问了 Python 内置函数的文档,现在让我们创建一个自己的函数,并写一个简短的描述,然后尝试访问它的文档。

首先,创建一个空的 main.py 文件,然后创建一个简单的函数,该函数将两个数字相加并返回它们的和:

#Define a function
def add(x, y):
    '''
    This function adds two given integer arguments

    Parameters:
    x : integer
    y : integer

    Output:
    val: integer
    '''

    val = x + y

    return val

现在我们已经定义了函数,在同一个 Python 文件中,我们可以调用 help() 函数,并将函数名称(add)作为参数传递:

#Define a function
def add(x, y):
    '''
    This function adds two given integer arguments

    Parameters:
    x : integer
    y : integer

    Output:
    val: integer
    '''

    val = x + y

    return val

#Find documentation of user defined function add()
help(add)

并且你应该会得到:

Help on function add in module __main__:

add(x, y)
    This function adds two given integer arguments

    Parameters:
    x : integer
    y : integer

    Output:
    val: integer

它显示了存储在 docstring 中的函数文档,包括其描述、输入参数和返回值。

结论

在本文中,我们探讨了如何使用 Python help() 函数,包括交互式帮助工具,访问内置函数以及用户定义函数的文档。

如果你有任何问题或有修改建议,请随时在下面留言,并查看更多我的Python 函数教程。

最初发布于 https://pyshark.com 2023 年 1 月 13 日。

Python 继承:你应该继承自 dict 还是 UserDict

原文:towardsdatascience.com/python-inheritance-should-you-inherit-from-dict-or-userdict-9b4450830cbb

PYTHON PROGRAMMING

他们说你不应该继承 dict 而应该继承 UserDict。这是真的吗?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Marcin Kozak

·发布于 Towards Data Science ·15 分钟阅读·2023 年 5 月 10 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

字典是 Python 基本数据类型之一。照片由 Waldemar 提供,Unsplash

继承自 dict 通常不是最佳选择——不仅因为他们这么说,还因为重载的方法不会工作。相反,你应该继承 collections.UserDict。但如果你不想重载 dict 方法,只是想添加新的方法呢?在本文中,我们将讨论何时以及如何继承 dictcollections.UserDict 类。

在他那本精彩的书籍 Fluent Python. 2nd ed. 中,Luciano Ramalho 解释了为什么你不应该创建继承自 dict 的自定义类。这条规则的理由,一开始看起来很奇怪,但其实简单而关键:dict 是一个高度优化的类型,由 C 实现,它不会调用你在 dict 子类中重载的方法。

这将是一个令人讨厌的惊喜,不是吗?让我们来看一个例子。假设你想创建一个类似字典的类,其中提供的值将被转换为它们的字符串表示。让我们尝试通过继承 dict 内置类型来做到这一点:

class StringDict(dict):
    def __setitem__(self, key, value):
        super().__setitem__(key, str(value))

这看起来像是完全有效的 Python 代码。让我们看看这怎么运作:

>>> class StringDict(dict):
...     def __setitem__(self, key, value):
...         super().__setitem__(key, str(value))
... 
>>> mydict = StringDict(first=1, second=2, third=3)
>>> mydict
{'first': 1, 'second': 2, 'third': 3}

嗯,这根本不起作用——或者说,这个__setitem__方法根本不起作用。我们想将值转换为字符串,但它们没有被转换。不过,我们没有看到任何错误;这个类本身以某种方式工作——实际上,它的工作方式就像一个普通的字典一样。(或者说,它提供了相同的结果但更慢;我们稍后会讨论这个问题。)

为了达到你想要的效果,你应该继承 UserDict

>>> from collections import UserDict
>>> class StringUserDict(UserDict):
...     def __setitem__(self, key, value):
...         super().__setitem__(key, str(value))
... 
>>> mydict = StringUserDict(first=1, second=2, third=3)
>>> mydict
{'first': '1', 'second': '2', 'third': '3'}

正如你所见,我们在定义中唯一改变的就是继承 UserDict 而不是 dict

所以现在你知道了。使用 UserDict 就足够了。太好了。

会吗?

结果

等等。我们来考虑一下。在决定使用 UserDict 而不是 dict 是否如此出色之前,我们应该考虑一些事情。

首先,我们知道 Python 的内建类型是高度优化的,因为它们是用 C 实现的,而这种实现本身也经过了高度优化。

其次,我们知道我们不应该继承 dict,因为它的 C 实现方法不会调用用 Python 实现的重写方法。

第三,简单的一点需要检查,collections.UserDict 是用 Python 实现的。在 Linux 中,你可以在这里找到它的定义:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

UserDict 定义的本地化在 Linux 中。来自 VS Code 的截图。图片由作者提供

在这种情况下,以下问题自然会出现:如果是这样,我自定义的继承 UserDict 的类会有良好的性能吗?

我立即猜测不会。dict 的优化来自 C 实现,而 UserDict 是用 Python 实现的。它为什么应该有所优化呢?我们将在接下来的部分中检查这一点。

UserDictdict 的基准测试

对于基准测试,我们将使用标准库中的 timeit 模块。你可以在这里了解更多信息:

## 使用 timeit 进行 Python 代码基准测试

最流行的 Python 代码时间基准测试工具,内建的 timeit 模块提供了比大多数工具更多的功能…

[towardsdatascience.com

为了使基准测试稍微简单和结构化一点,让我们定义一个简单的函数来对两个或更多代码片段进行时间基准测试:

 import rounder
import timeit
import pprint

def compare(
    __snippet1,
    __snippet2,
    *args,
    number=10_000_000,
    repeat=7,
    setup="from collections import UserDict"):
    snippets = [__snippet1, __snippet2, *args]
    results = {}
    for i, snippet in enumerate(snippets):
        name = snippet if len(snippet) < 30 else f"snippet {i + 1}"
        results[name] = min(timeit.repeat(
            snippet, number=number, repeat=repeat, setup=setup
            )) / number,
    results = rounder.signif_object(results, digits=4)
    pprint.pprint(results)

几件事:

  • 这个函数使用了 [rounder](https://pypi.org/project/rounder/) 包,将字典中的所有数字四舍五入到四位有效数字;你可以在这里了解更多信息:

## rounder:在复杂的 Python 对象中四舍五入数字

rounder 包允许你通过一个命令将任何对象中的所有数字进行四舍五入。

[towardsdatascience.com

  • __snippet1__snippet2 是仅限位置的参数,因此你不能通过名称调用它们。这要归功于双下划线前缀。

  • 多亏了两个片段参数后的 *args,你可以提供更多的片段,也可以作为位置关键字;你可以根据需要使用任意多个。

  • 所有剩余的参数都是仅限关键字的。在这里,这是通过将它们放在 *args 之后实现的。¹

  • 这个函数报告的结果是七次运行中最快的一次的平均值。因此,所有结果都是直接可比的,即使timeit.repeat()函数使用了不同的number值。

  • 这个函数隐式返回None并打印基准测试的简短报告,使用标准库pprint模块中的pprint()函数。通常,避免将返回语句替换为打印²,除非你的函数/方法是一个打印的函数。

好的,我们马上会看到这个函数的实际效果。首先,让我们比较一下dict()UserDict()创建实例的速度。然而,我们可以通过两种方法实例化一个常规字典,即dict()和(显著更快的){},所以我们会同时考虑这两者:

>>> compare("UserDict()", "dict()", "{}")
{'UserDict()': (1.278e-07,), 'dict()': (3.826e-08,), '{}': (1.518e-08,)}

在本文的所有基准测试中,我使用了 Python 3.11,在 Windows 10 机器上,WSL 1 环境中,32 GB 内存和四个物理(八个逻辑)核心。基准测试显示,创建一个新实例时,UserDict的速度是dict的两倍慢。

如上所述,我们在结果字典中看到的值代表了创建一个UserDict或常规dict(通过两种方法创建)的时间。显然,创建一个UserDict实例需要更多时间,大约1.3e-07秒——而{}需要大约1.5e-08秒。差异不大?注意当你需要创建一个单一实例时,但想象一下创建数百万个字典。因此,创建一个常规字典所需的时间大约是创建UserDict3–8 倍,具体取决于实例化方法。

让我们看看较大字典的情况。我们将通过字典推导式创建一个简单的数值字典。由于UserDict不允许使用 dictcomp 语法(另一个缺点),我们唯一能做的就是先使用 dictcomp 语法创建一个常规字典,然后将其转换为UserDict实例:

>>> compare(
    "UserDict({i: i**2 for i in range(1000)})",
    "{i: i**2 for i in range(1000)}",
    number=100_000)
{'snippet 1': (0.0001316,), 'snippet 2': (5.027e-05,)}

一个常规字典快了大约 2.5 倍。考虑到创建一个空字典的速度甚至更快,这似乎相当惊人。我们必须记住,这些基准测试的结果可能会有所不同。但我们也必须记住,当我们使用许多重复测试(这里是十万次——我们可以使用更多)时,结果的差异应该相对较小。

当我们比较查找时间时,大小会影响结果吗?基本上,键查找与字典大小无关,因此,字典大小的不同应该不会影响结果。

首先,一个小字典:

>>> setup = """from collections import UserDict
... d = {'x': 1, 'y': 2, 'z': 3}
... ud = UserDict(d)
... """
>>> compare("ud['x']", "d['x']", setup=setup)
{"ud['x']": (4.754e-08,), "d['x']": (1.381e-08,)}

好的,所以慢了大约 3.5 倍。现在,对于一个更大的10_000键值对的字典:

>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10_000)}
... ud = UserDict(d)
... """
>>> compare("ud['9999']", "d['9999']", setup=setup, number=1_000_000)
{"ud['9999']": (7.785e-08,), "d['9999']": (2.787e-08,)}

对于 1000 万个元素的情况:

>>> compare("ud['9999']", "d['9999']", setup=setup, number=100_000)
{"ud['9999']": (6.662e-08,), "d['9999']": (2.499e-08,)}

因此,大小确实不重要,每次dict都快了大约 3–3.5 倍。让我们看看,这次仅针对中等大小的字典,如何处理不存在的键:

>>> compare(
...     "ud.get('a', None)",
...     "d.get('a', None)",
...     setup=setup,
...     number=1_000_000)
{"d.get('a', None)": (4.318e-08,), "ud.get('a', None)": (4.525e-07,)}

这次差距更大,dict的速度超过了10倍。

检查一个键是否在字典中呢?

>>> compare("'a' in ud", "'a' in d", setup=setup, number=1_000_000)
{"'a' in d": (1.465e-08,), "'a' in ud": (4.562e-08,)}

所以,再次是 3–3.5 倍快。

现在,让我们基准测试一个频繁操作,即遍历字典;再次检查不同大小的字典:

>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10)}
... ud = UserDict(d)
... """
>>> compare(
...     "for i, v in ud.items(): pass",
...     "for i, v in d.items(): pass",
...     setup=setup,
...     number=1_000_000
... )
{'for i, v in d.items(): pass': (1.726e-07,),
 'for i, v in ud.items(): pass': (1.235e-06,)}

>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(10_000)}
... ud = UserDict(d)
... """
>>> compare(
...     "for i, v in ud.items(): pass",
...     "for i, v in d.items(): pass",
...     setup=setup,
...     number=10_000
... )
{'for i, v in d.items(): pass': (0.0001255,),
 'for i, v in ud.items(): pass': (0.00112,)}

>>> setup = """from collections import UserDict
... d = {str(i): i for i in range(100_000)}
... ud = UserDict(d)
... """
>>> compare(
...     "for i, v in ud.items(): pass",
...     "for i, v in d.items(): pass",
...     setup=setup,
...     number=10_000
... )
{'for i, v in d.items(): pass': (0.001772,),
 'for i, v in ud.items(): pass': (0.01718,)}

好的,对于小型字典来说,dict在遍历其键值对(通过.items()方法提供)时大约快 7 倍。对于中型字典(在我们的实验中有 1 万元素),快约 9 倍。对于更大的字典(有100_000个元素),结果类似,因此一旦开始循环,循环本身似乎并不依赖于字典的类型。

由于这只是一个相当小的基准测试,我们可以得出结论:常规字典在遍历其项目时应该比UserDict快约 5–10 倍。

结论基准测试

也许我们在这里停下来吧。我们可以进行更多基准测试,但这不是重点。我不想进行dictUserDict在执行时间上的全面比较;如果你感兴趣,可以尝试代码进行一系列可靠的基准测试。相反,我想阐明这个问题,并检查是否像我基于UserDictdict的实现知识所预期的那样,前者明显比后者慢。

而且——除非你认为 5–10 倍更慢是一个微不足道的数字。所以,如果你能的话,考虑使用常规字典,而不是那些继承UserDict的字典,除非你必须改变dict的行为。

啊……为什么我们不能直接继承dict?!为什么?

或者……我们可以吗?

不要继承dict?那为什么不呢?!

也许你已经注意到,不继承dict的规则与用 C 实现的dict方法有关,这些方法不会调用在 Python 中重载的内置dict方法。但如果你只是想给dict添加一些功能,而不触动已经用 C 实现的方法呢?

这是一个非常好的问题。答案简短而简单:是的,你可以这样做!你可以dict继承;只需不要重载dict的方法,仅此而已。

问题是,基于dict的类会像dict一样高效吗?或者说像collections.UserDict一样吗?为了回答这个问题,我们需要运行更多的基准测试。

让我们想象我们在一个字典中保存一些数据,我们想要添加一个.summarize()方法来计算数据的一些摘要统计信息。它可能像这样(仅作为示例):

from collections.abc import Sequence
from typing import Callable

def try_calculate(func: Callable, *args, **kwargs):
    """Try calculations; when data are incorrect, return nan."""
    try:
        return func(*args, **kwargs)
    except TypeError:
        return float("nan")

class RichDict(dict):
    measures = {
        "sum": sum,
        "n": len,
        "mean": lambda x: sum(x) / len(x),
    }

    def summarize(self):
        statistics = {}
        for k, v in self.items():
            if isinstance(v, str):
                statistics[k] = {"n": len(v)}
            elif isinstance(v, Sequence):
                statistics[k] = {
                    name: try_calculate(func, v)
                    for name, func
                    in self.measures.items()
                }
        return statistics

RichDict是一个dict,多了一个方法:.summarize()。这个方法执行以下操作:

  • 它遍历数据的键值对(通过.items方法获取)。

  • 当值是字符串时,statistics仅包含长度,并作为一个包含一个键n的字典返回。

  • 当值是Sequence时,计算主要的摘要统计信息。度量以可调用的形式保存在类属性RichDict.measures中,它是一个字典。

  • 该方法保护计算:如果无法计算一个度量,则会捕获异常并返回float("nan")(表示不是数字)作为计算结果。这样,例如,Python 在尝试计算空列表的均值时不会抛出错误。

如果你想添加一个度量,可以轻松做到:

RichDict.measures["min"] = min
RichDict.measures["max"] = max

如果函数更复杂,你可以使用lambda函数:

RichDict.measures["max-to-mean"] = lambda x: max(x) / min(x)

或者,更好地,首先定义一个函数,然后在这里分配它:

def max_to_min(x: float) -> float:
    return max(x) / min(x)

RichDict.measures["max-to-mean"] = max_to_min

请注意,由于.measures是一个类属性,所有的RichDict实例(包括即将创建的和已经存在的)都将具有扩展的度量,包括minmax统计信息。

这是RichDict在实际应用中的一个例子:

>>> d = RichDict(x=[1,4,5,7],
...              y=[1,"1",2],
...              z="Shout Bamalama!",
...              f=10)
>>> 
>>> stats = d.summarize()
>>> stats # doctest: NORMALIZE_WHITESPACE
{'x': {'sum': 17, 'n': 4, 'mean': 4.25, 'min': 1, 'max': 7, 'max-to-min': 7},
 'y': {'sum': nan, 'n': 3, 'mean': nan, 'min': nan, 'max': nan, 'max-to-min': nan},
 'z': {'n': 15}}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

上面,RichDict类有一个类属性,包含用于序列数据的度量;对于字符串,.summarize() 方法只计算一个度量。更新类,使其具有两个类属性measures_seqmeasures_str,其设计方式与上面的measures相同。对于字符串,.summarize() 方法应按序列的方式计算度量,即使用measures_str

你可以在附录 1 中找到解决方案。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在代码中,我使用了标准库中的doctests模块进行文档测试。如果你有兴趣了解更多关于这个有趣模块的内容,可以从这篇文章中了解:

## 使用 doctest 进行 Python 文档测试:简单方法

doctest 允许进行文档、单元和集成测试以及测试驱动开发。

towardsdatascience.com

好了,现在我们知道RichDict有效,我们知道我们可以子类化dict。我们现在想要了解的是RichDict增加的功能(用 Python 定义,而不是 C 语言,就像dict的基础代码一样)是否会为dict的常规行为增加一些开销。为此,我们将基准测试此类行为,例如创建一个新的RichDict与创建一个新的dict、键查找等。

让我们进行类似于上面为UserDict进行的基准测试。你可以在这个 GitHib gist中找到相关代码。你会在那里找到下面使用的setup值。

>>> compare("UserDict()", "RichDict()", "dict()", setup=setup)
{'UserDict()': (2.236e-07,), 'RichDict()': (1.073e-07,), 'dict()': (5.892e-08,)}

如上所示,当创建一个空实例时,RichDict的速度明显比UserDict快(约快 2 倍),但比dict慢(约慢 2 倍)。

>>> compare(
...     "UserDict({i: i**2 for i in range(1000)})",
...    "RichDict({i: i**2 for i in range(1000)})",
...    "{i: i**2 for i in range(1000)}",
...    number=100_000,
...    setup=setup)
{'snippet 1': (0.0001765,), # UserDict
 'snippet 2': (6.845e-05,), # RichDict
 'snippet 3': (5.388e-05,)} # dict

这一次,RichDictUserDict快约 2.5 倍,但比dict稍慢(约慢 1.3 倍)。

下面,你将找到更多基准测试的示例,为方便起见,示例之间以空行分隔:

>>> setup += """d = {'x': 1, 'y': 2, 'z': 3}
... ud = UserDict(d)
... rd = RichDict(d)
... """
>>> compare("ud['x']", "rd['x']", "d['x']", setup=setup)
{"ud['x']": (5.111e-08,), rd['x']": (3.024e-08,), "d['x']": (1.475e-08,)}

>>> compare(
...     "'a' in ud",
...     "'a' in rd",
...     "'a' in d",
...     setup=setup,
...     number=1_000_000)
{"'a' in d": (1.366e-08,),  # dict
 "'a' in rd": (2.228e-08,), # RichDict
 "'a' in ud": (4.436e-08,)} # UserDict

>>> compare(
...     "ud.get('a', None)",
...     "rd.get('a', None)",
...     "d.get('a', None)",
...     setup=setup,
...     number=1_000_000)
{"d.get('a', None)": (1.935e-08,),  # dict
 "rd.get('a', None)": (3.016e-08,), # RichDict
 "ud.get('a', None)": (5.125e-07,)} # UserDict

>>> compare(
...     "for i, v in ud.items(): pass",
...     "for i, v in rd.items(): pass",
...     "for i, v in d.items(): pass",
...     setup=setup,
...     number=1_000
... )
{'for i, v in d.items(): pass': (0.001783,),
 'for i, v in rd.items(): pass': (0.001743,),
 'for i, v in ud.items(): pass': (0.01627,)}

数字本身说明了我们需要的内容,所以我暂时将它们留给你。

总结基准测试结果

RichDict通常比dict慢(尽管有时只是很少),但比UserDict快。

因此,如果你只是想给dict添加一些功能,而不覆盖其内置方法,你绝对 可以子类化 dict。我会说这应该是你首选的方法,而不是子类化collections.UserDict,因为后者明显更慢。请记住,我们讨论的是当你不需要改变字典的常规行为,只是添加一些新行为的情况。

还要记住,使用内置类型的方式会有一个代价:你的类(在我们的例子中是RichDict)会比dict更慢。不过,它仍然比UserDict快,而UserDict的创建目的就是为了让你继承……嗯,不是从dict继承,而是让你创建一个具有dict相同行为的新类型(类)。不幸的是,使用UserDict是相当昂贵的,因为它的性能比dict差得多。

结论

让我们总结一下关于子类化dictUserDict的讨论。我们了解到,我们有三种选择:

  1. UserDict继承,当你想要覆盖dict的内置行为时。这将是最慢的选项。

  2. dict继承,当你不想覆盖dict的内置行为,而是添加新功能(方法)时。这将比选项 1 快。

  3. 使用内置的dict类型,而不创建自定义类。如果你需要自定义功能,你可以在接受dict实例作为参数的函数中实现它们。这是最快的选项(见下文)。

我们还没有讨论第三种选项,因为它不涉及子类化。不过,不需要讨论太多,因为这是最简单的方法,它使用的是一种更程序化的方法,而不是面向对象的方法。一方面,使用这种方法的summarize()函数比使用选项 2 中的RichDict.summarize()方法快一点(如果有的话)。这个要点包含了相应的基准测试代码;在我的机器上,它提供了一个小而稳定(从运行到运行)性能提升。另一方面,我们知道,常规dict的所有其他行为明显比RichDict要快。因此,一般来说,选项 3 提供了处理具有附加功能的字典的最快方法。

因此,如果性能很重要,最明智的选择似乎是第三种选项——即使用常规字典,并在外部函数中实现所需的附加行为。根据情况,这也可能是代码最清晰的选项,因为它不需要自定义数据结构,而是结合了字典(Python 中最常见的数据结构之一)和函数。通常,这意味着代码更清晰。

第二种选择意味着更差的性能,因为向dict添加方法会导致其行为的额外开销。如我们所知,选项 3 通过将方法移到字典之外来消除这种开销。

第一种选择在性能方面绝对是最差的。我认为只有在满足以下三个条件中的每一个时,这个选项才有意义:

  • 性能不重要

  • 你需要重写一个或多个内置的dict方法。

  • 由于创建一个将所有所需功能组合在一起的类,代码将更清晰、更易于使用。

脚注

¹ 我计划写一篇关于仅位置参数和仅关键字参数的专门文章。一旦发表,我会在这里链接。

² 顺便提一下,在交互式会话中,函数的返回会有相同的效果(当然,当结果未被赋值时)。不过,我是在脚本中运行基准测试的,而不是交互式会话中。

附录 1

练习的解决方案

你可以用各种方式来做。下面的解决方案避免了重复,但也使得向计算中添加另一种类型(到Sequencestr)变得容易。

from collections.abc import Sequence
from typing import Callable

class RichDict(dict):
    measures_seq = {
        "sum": sum,
        "n": len,
        "mean": lambda x: sum(x) / len(x),
    }
    measures_str = {
        "n": len,
    }

    def summarize(self):
        statistics = {}
        for k, v in self.items():
            if isinstance(v, str):
                measures = self.measures_str
            elif isinstance(v, Sequence):
                measures = self.measures_seq
            else:
                continue
            statistics[k] = {
                name: try_calculate(func, v)
                for name, func
                in measures.items()
            }
        return statistics

NaN = float("nan")

def try_calculate(func: Callable, *args, **kwargs):
    """Try calculations and when the data are incorrect, return nan."""
    try:
        return func(*args, **kwargs)
    except TypeError:
        return NaN

感谢阅读。如果你喜欢这篇文章,你可能还会喜欢我写的其他文章;你可以在这里查看。如果你想加入 Medium,请使用我下面的推荐链接:

## 使用我的推荐链接加入 Medium - Marcin Kozak

阅读 Marcin Kozak 的每一个故事(以及 Medium 上的其他成千上万位作家的故事)。你的会员费直接支持…

medium.com

Python:init 不是构造函数:深入探讨 Python 对象创建

原文:towardsdatascience.com/python-init-is-not-a-constructor-a-deep-dive-in-python-object-creation-9134d971e334

使用 Python 的构造函数创建快速且内存高效的类

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Mike Huls

·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 11 月 27 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Python 如何构建对象(图像由 ChatGPT 提供)

你知道__init__方法不是构造函数吗?但如果__init__创建对象,那究竟是什么呢?对象在 Python 中是如何创建的?Python 甚至有构造函数吗?

本文的目标是更好地理解 Python 如何创建对象操控这一过程以构建更好的应用程序

首先,我们将深入了解 Python 如何创建对象。接下来,我们将应用这些知识,讨论一些有趣的用例,并提供一些实际示例。让我们开始编码吧!

1. 理论:在 Python 中创建对象

在这一部分,我们将弄清楚在你创建对象时 Python 背后发生了什么。在下一部分,我们将运用这些新知识进行第二部分的实践。

如何在 Python 中创建对象?

这应该很简单;你只需创建一个类的实例。或者,你可以创建一个新的内置类型,比如strint。在下面的代码中,创建了一个基本类的实例。它只包含一个__init__函数和一个say_hello方法:

class SimpleObject:
  greet_name:str

  def __init__(self, name:str):
    self.greet_name = name

  def say_hello(self) -> None:
    print(f"Hello {self.greet_name}!")

my_instance = SimpleObject(name="bob")
my_instance.say_hello()

注意__init__方法。它接收一个name参数,并将其值存储在SimpleObject实例的greet_name属性上。这允许我们的实例保持状态

现在问题出现了:为了保存状态,我们需要有东西来保存状态__init__ 从哪里得到对象?

那么,init 是构造函数吗?

答案是:从技术上讲,没有。构造函数实际上创建新对象;__init__方法仅负责设置对象的状态。它只是通过参数接收值,并将这些值分配给像greet_name这样的类属性。

在 Python 中,对象的实际创建发生在初始化之前。对于对象创建,Python 使用一个名为**__new__****的方法,该方法存在于每个对象上。

## 为绝对初学者创建和发布自己的 Python 包

在 5 分钟内创建、构建和发布一个 Python 包

towardsdatascience.com

__new__ 做了什么?

__new__ 是一个类方法,意味着它是直接在类上调用的,而不是在类的实例上。它存在于每个对象上,并负责实际创建和返回对象。__new__ 的最重要的方面是它必须返回一个类的实例。我们将在本文后面进一步研究这个方法。

__new__ 方法来自哪里?

简短的回答是:Python 中的一切都是对象object 类有一个 **__new__** 方法。你可以把这看作是*“每个类都继承自* *object* 类”

请注意,即使我们的 SimpleObject 类没有继承任何东西,我们仍然可以证明它是 object 的一个实例:

# SimpleObject is of type 'object'
my_instance = SimpleObject(name="bob")
print(isinstance(my_instance, object))    # <-- True
# but all other types as well:
print(isinstance(42, object))             # <-- True
print(isinstance('hello world', object))  # <-- True
print(isinstance({"my": "dict"}, object)) # <-- True

总结来说,一切都是对象,object 定义了 __new__ 方法,因此 Python 中的一切都有一个 __new__ 方法。

__new____init__ 有何不同?

__new__ 方法用于实际创建对象:分配内存并返回新对象。一旦对象创建完成,我们可以用 __init__初始化它;设置初始的状态

## Python 的 args、kwargs 和传递参数的所有其他方式

精巧地设计你的函数参数的 6 个示例

towardsdatascience.com

Python 对象创建的过程是什么样的?

内部,下面的函数在你创建新对象时会被执行:

  • __new__:分配内存并返回新对象

  • __init__:初始化新创建的对象;设置状态

在下面的代码中,我们通过重写**__new__**来展示这一点。在下一部分我们将利用这一原则做一些有趣的事情:

class SimpleObject:
  greet_name:str

  def __new__(cls, *args, **kwargs):      # <-- newly added function
    print("__new__ method")               
    return super().__new__(cls)            

  def __init__(self, name:str):
    print("__init__ method")
    self.greet_name = name

  def say_hello(self) -> None:
    print(f"Hello {self.greet_name}!")

my_instance = SimpleObject(name="bob")
my_instance.say_hello()

我们将在接下来的部分解释为什么和如何工作。)这将打印以下内容:

__new__ method
__init__ method
Hello bob!

这意味着我们可以访问初始化我们类的实例的函数!我们还看到 __new__ 先执行。在下一部分我们将了解 __new__ 的行为:super().__new__(cls) 是什么意思?

__new__ 是如何工作的?

__new__的默认行为如下所示。在这一部分,我们将尝试理解发生了什么,以便在下一部分的实际示例中对其进行调整。

class SimpleObject:
  def __new__(cls, *args, **kwargs):
    return super().__new__(cls)

请注意,__new__是在super()方法上调用的,它返回一个“引用”*(实际上是一个代理对象)*到SimpleObject的父类。请记住,SimpleObject继承自object,其中定义了__new__方法。

分解:

  1. 我们获得了我们所在类的基类的“引用”。以SimpleObject为例,我们获得了object的“引用”

  2. 我们在“引用”上调用__new__,因此object.__new__

  3. 我们将cls作为参数传递。

    这就是像 *__new__* 这样的类方法的工作方式;它是对类本身的引用

综合起来:我们请求SimpleObject的父类创建一个SimpleObject的新实例。

这与my = object.__new__(SimpleObject)是一样的

那么我可以使用__new__创建一个新实例吗?

是的,请记住,默认的__new__实现实际上直接调用它:return super().**__new__**(cls)。因此,下面代码中的方法做了同样的事情:

# 1\. __new__ and __init__ are called internally
my_instance = SimpleObject(name='bob')

# 2\. __new__ and __init__ are called directly:
my_instance = SimpleObject.__new__(SimpleObject)
my_instance.__init__(name='bob')
my_instance.say_hello()

直接方法中发生的事情:

  1. 我们在SimpleObject上调用__new__函数,传递SimpleObject类型。

  2. SimpleObject.__new__ 在其父类(object)上调用__new__

  3. object.__new__创建并返回一个SimpleObject的实例

  4. SimpleObject.__new__返回新实例

  5. 我们调用__init__来初始化它。

这些事情在非直接方法中也会发生,但它们是在幕后处理的,所以我们没有注意到。

## 在 Python 中处理相对路径的简单技巧

轻松在运行时计算文件路径

towardsdatascience.com

实际应用 1:子类化不可变类型

现在我们知道__new__是如何工作的,我们可以利用它做一些有趣的事情。我们将理论付诸实践,子类化一个不可变类型。这样,我们可以拥有自己的特殊类型,其方法定义在一个非常快速的内置类型上。

目标

我们有一个处理许多坐标的应用程序。因此,我们希望将坐标存储在元组中,因为它们很小且内存高效。

我们将创建自己的Point类,继承自tuple。这样,Point是一个tuple,因此它非常快速且小巧,并且我们可以添加如下功能:

  • 对对象创建的控制(例如,只在所有坐标都是正数时创建新对象)

  • 额外的方法,例如计算两个坐标之间的距离。

cython-for-absolute-beginners-30x-faster-code-in-two-simple-steps-bbb6c10d06ad?source=post_page-----9134d971e334-------------------------------- ## Cython 的绝对初学者指南:两步实现代码 30 倍加速

为闪电般快速的应用程序提供简单的 Python 代码编译

[towardsdatascience.com

带有 new 重写的 Point 类

在第一次尝试中,我们仅创建一个继承自元组的Point类,并尝试使用x, y坐标初始化元组。这不会成功:

class Point(tuple):

  x: float
  y: float

  def __init__(self, x:float, y:float):
    self.x = x
    self.y = y

p = Point(1,2)    # <-- tuple expects 1 argument, got 2

失败的原因是因为我们的类是tuple的子类,而tuple不可变的。记住,tuple是通过__new__创建的,然后__init__运行。在初始化时,元组已经被创建,不能再被修改,因为它们是不可变的。

我们可以通过重写__new__来解决这个问题:

class Point(tuple):

  x: float
  y: float

  def __new__(cls, x:float, y:float):    # <-- newly added method
    return super().__new__(cls, (x, y))

  def __init__(self, x:float, y:float):
    self.x = x
    self.y = y

这之所以有效,是因为在__new__中,我们使用super()来获取Point的父类引用,即tuple。接下来,我们使用tuple.__new__并传递一个可迭代对象((x, y))来创建一个新元组。这与tuple((1, 2))是一样的。

控制实例创建和附加方法

结果是一个Point类,底层是一个tuple,但我们可以添加各种额外功能:

class Point(tuple):
    x: int
    y: int

    def __new__(cls, x:float, y:float):
      if x < 0 or y < 0:                                  # <-- filter inputs
          raise ValueError("x and y must be positive")
      return super().__new__(cls, (x, y))

    def __init__(self, x:float, y:float):
      self.x = x
      self.y = y

    def distance_from(self, other_point: Point):          # <-- new method
      return math.sqrt(
        (other_point.x - self.x) ** 2 + (other_point.y - self.y) ** 2
      )

p = Point(1, 2)
p2 = Point(3, 1)
print(p.distance_from(other_point=p2))  # <-- 2.23606797749979

注意我们添加了一个计算Point之间距离的方法,以及一些输入验证。我们现在在__new__中检查提供的Xy值是否为正,并在不符合条件时完全阻止对象创建。

## 使用 Docker 和 Compose 的环境变量和文件的完整指南

通过这个简单的教程保持你的容器安全和灵活

[towardsdatascience.com

实际应用 2:添加元数据

在这个示例中,我们从不可变的float创建了一个子类,并添加了一些元数据。下面的类将生成一个真正的float,但我们添加了一些关于符号的额外信息。

class Currency(float):

    def __new__(cls, value: float, symbol: str):
        obj = super(Currency, cls).__new__(cls, value)
        obj.symbol = symbol
        return obj

    def __str__(self) -> str:
        return f"{self.symbol} {self:.2f}"  # <-- returns symbol & float formatted to 2 decimals

price = Currency(12.768544, symbol='€')
print(price)                            # <-- prints: "€ 12.74"

正如你所见,我们继承自float,这使得Currency的实例实际上是一个float。如你所见,我们还可以访问诸如用于美观打印的符号等元数据。

还要注意这是一个实际的浮点数;我们可以毫无问题地执行float操作:

print(isinstance(price, float))        # True
print(f"{price.symbol} {price * 2}")   # prints: "€ 25.48"

## 参数与关键字参数:哪种方式在 Python 中调用函数最快?

timeit模块的清晰演示

towardsdatascience.com

实际应用 3:单例模式

有些情况下你不想每次实例化类时都返回一个新的对象。例如,一个数据库连接。单例模式将类的实例化限制为唯一实例。该模式用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例:

class Singleton:
  _instance = None

  def __new__(cls):
    if cls._instance is None:
      cls._instance = super(Singleton, cls).__new__(cls)
    return cls._instance

singleton1 = Singleton()
singleton2 = Singleton()

print(id(singleton1))
print(id(singleton2))
print(singleton1 is singleton2)  # True

这段代码创建一个Singleton类的实例(如果它尚不存在),并将其作为属性保存在cls上。当Singleton再次被调用时,它返回之前存储的实例。

## 使用 Python 的 AtExit 在程序退出后运行代码

注册在脚本结束或出错后运行的清理函数

towardsdatascience.com

其他实际应用

其他一些应用包括:

  • 控制实例创建

    我们在Point示例中已经看到过:在创建实例之前添加额外的逻辑。这可以包括输入验证、修改或日志记录。

  • 工厂方法

    根据输入在__new__中确定将返回哪个类。

  • 缓存

    对于资源密集型对象创建。像单例模式一样,我们可以在类本身上存储之前创建的对象。我们可以在__new__中检查是否已经存在等效的对象,并返回它,而不是创建一个新的。

## 从你的 Git 仓库创建可以用 PIP 安装的自定义私有 Python 包

使用你的 git 仓库分享你自己构建的 Python 包。

towardsdatascience.com

结论

在这篇文章中,我们深入探讨了 Python 对象创建,了解了它是如何工作的以及为什么这样工作。然后我们看了一些实际示例,演示了我们可以用新获得的知识做很多有趣的事情。控制对象创建可以使你创建高效的类,并显著提高你的代码的专业性。

为了进一步改进你的代码,我认为最重要的是真正理解你的代码,了解 Python 的工作原理并应用合适的数据结构。为此,请查看我的其他文章或这个演示

我希望这篇文章能像我期望的那样清晰,但如果不清楚,请告诉我可以进一步澄清的内容。同时,请查看我在 其他文章 上关于各种编程相关主题的文章:

祝编码愉快!

— Mike

附言:喜欢我做的事吗? 关注我

[## Mike Huls - Medium

阅读 Mike Huls 在 Medium 上的文章。我是一名全栈开发者,对编程、技术充满热情,…

mikehuls.medium.com](https://mikehuls.medium.com/?source=post_page-----9134d971e334--------------------------------)

Python 列表:处理有序数据集合的终极指南

原文:towardsdatascience.com/python-lists-the-definitive-guide-for-working-with-ordered-collections-of-data-53b06a194826

Python 列表的全面指南

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Federico Trotta

·发表于 Towards Data Science ·10 分钟阅读·2023 年 7 月 19 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图片由 Jill Wellington 提供,来源于 Pixabay

在编程时,我们总是需要处理数据结构。我的意思是,我们需要将信息存储在某个地方,以便以后可以重新使用。

Python 是一种非常灵活的编程语言,给我们提供了使用不同类型数据结构的可能性。

在这篇文章中,我们将分析 Python 列表。因此,如果你是 Python 初学者,并且正在寻找关于列表的全面指南,那么这篇文章绝对适合你。

在这里你将学到:

Table of Contents:

What is a list in Python?
The top 9 features in Python lists, with examples
  How to create a list in Python
  Accessing list elements
  Modifying the elements of a list
  Adding elements to a list
  Removing elements from a list
  Concatenating lists
  Calculating the lenght of a list
  Sorting the elements of a list
  List comprehension

什么是 Python 列表?

在 Python 中,列表是一种内置的数据结构,允许我们以文本或数字的形式存储和操作数据。

列表以有序的方式存储数据,这意味着可以通过位置访问列表中的元素。

列表也是一种可修改的数据结构,与 元组 相对。

最后,列表还可以存储重复的值而不会引发错误。

Python 列表的 9 大特性及示例

学习 Python 的最佳方式是亲自上手敲代码,并且尽可能地解决实际问题。

所以,现在我们将通过代码示例展示 Python 列表的 9 个主要特性,因为正如我们将看到的,理论在编程中意义不大:我们只需要编写代码并解决问题。

如何在 Python 中创建列表

要创建列表,我们需要使用方括号:

# Create a simple list
numbers = [1,2,3,"dog","cat"]

# Show list
print(numbers)

>>>

[1, 2, 3, 'dog', 'cat']

创建列表的另一种方法是使用内置方法 list。例如,假设我们想创建一个包含从 0 到 9 的数字的列表。我们可以使用内置方法 range 来创建这个范围,然后将其作为参数传递给 list 方法来创建列表,如下所示:

# Create a list in the range
list_range = list(range(10))

# Show list
print(range_list)

>>>

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

我们还可以创建所谓的列表中的列表,即嵌套列表。例如,假设我们想存储与跑步的人测量时间(以秒为单位)相关的数据。如果我们需要这些数据作为列表,我们可以像这样创建一个列表的列表:

# Create a list of lists
times = [["Jhon"], [23, 15, 18], ["Karen"], [17, 19, 15],
        ["Tom"], [21, 19, 25]]

# Print list
print(times)

>>>

[['Jhon'], [23, 15, 18], ['Karen'], [17, 19, 15], ['Tom'], [21, 19, 25]]

访问列表元素

列表中的元素可以通过其位置访问。我们需要记住的是,在 Python 中,我们从 0 开始计数。这意味着第一个元素通过 0 访问:

# Create a list of elements
values = [1,2,3,"dog","cat"]

# Print elements by accessing them
print(f"The first element is: {values[0]}")
print(f"The fourth element is: {values[3]}")

>>>

The first element is: 1
The fourth element is: dog

因此,我们只需要注意正确计数。

访问列表中的列表稍微复杂一些,但并不难。我们首先需要访问与外部列表相关的位置,然后在内部列表中计数。

正如我们所说的,实践胜于理论。在 Python 中,我们用一个例子来展示这个概念:

# Create a list of lists
times = [["Jhon"], [23, 15, 18], ["Karen"], [17, 19, 15],
        ["Tom"], [21, 19, 25]]

# Print
print(f"The first runner is:{times[0]}.\nHis first registered time is:{times[1][0]}\nThe min registered time is:{min(times[1])}")

>>>

The first runner is:['Jhon'].
His first registered time is:23
The min registered time is:15

Jhon 是第一个登记的跑步者,因此我们用 times[0] 访问它。

然后,我们想计算他第一次登记的时间。为此,我们需要输入 times[1][0],因为:[1] 表示第二个位置,相对于外部列表。意思是我们访问了内部列表 [23, 15, 18]。最后,[0] 访问了内部列表中的第一个数字,确实是 23。

修改列表中的元素

正如我们所说,列表是可修改的,要修改列表中的元素,我们需要访问它。

那么,让我们做一个例子:

# Create a list of numbers
numbers = [1, 2, 3, 4, 5]

# Modify the third element
numbers[2] = 10

# Print new list
print(numbers)

>>>

[1, 2, 10, 4, 5]

所以,在这种情况下,我们修改了第三个元素,将其从 3 改为 10。

我们还可以修改文本,特别是句子。让我们看一个例子:

# Create a sentence in a list
sentence = list("Hello, World!")

# Substitute "world" with "Python"
sentence[7:] = list("Python!")

# Print sentence
print(''.join(sentence))

>>>

"Hello, Python!"

所以,在这里,我们用 sentence[7:] 替换了列表 “sentence” 中从第七个(从 0 开始计数,如前所述)元素到最后一个元素的所有字母。

然后,我们使用了 ''.join(sentence) 方法来将句子作为一个整体打印。事实上,如果我们只是使用 print(),它会将字母逐个打印,如下所示:

print(sentence)

>>>

['H', 'e', 'l', 'l', 'o', ',', ' ', 'P', 'y', 't', 'h', 'o', 'n', '!']

向列表中添加元素

由于列表是可变的,我们可以向其中添加新元素,如果需要,并且我们有几种方法可以做到这一点。

第一种方法是使用 append() 方法,这在我们只需要向列表中添加一个元素时特别适用。例如:

# Create a list with fruits
fruits = ['apple', 'banana']

# Append the element "orange" to the list
fruits.append('orange')

# Print list
print(fruits)

>>>

['apple', 'banana', 'orange']

向现有列表中添加元素的另一种方法是使用 extend() 方法,这在需要一次添加多个元素时特别适用。例如,如下所示:

# Create a list of numbers
numbers = [1, 2, 3, 4, 5]

# Extend the list with new numbers
numbers.extend([6, 7, 8])

# Print list
print(numbers)

>>>

[1, 2, 3, 4, 5, 6, 7, 8]

从列表中移除元素

由于可变性,我们可以向列表中添加元素,也可以删除元素。

在这里,我们有两种方法:我们可以使用切片功能,或者可以直接指定要删除的元素。

让我们通过 Python 示例来看看这些:

# Create a list with fruits
fruits = ['apple', 'banana', 'orange']

# Remove the element banana
fruits.remove('banana')

# Print list
print(fruits)

>>>

['apple', 'orange']

所以,remove() 方法允许我们通过输入其值直接从列表中删除特定元素。

我们可以使用的另一种方法是通过以下方式访问我们想要删除的元素的位置:

# Create a list of numbers
numbers = [1, 2, 3, 4, 5]

# Delete on element: slicing method
popped_element = numbers.pop(2)

# Print
print(numbers)  
print(f"The deleted element is:{popped_element}")  

>>>

[1, 2, 4, 5]
The deleted element is:3

因此,pop() 方法通过访问索引从列表中删除一个元素。

选择使用哪一个?这取决于情况。如果我们有一个非常长的列表,通常使用 remove() 方法是个好主意,这样我们可以直接写出我们实际上想要删除的元素,而不会在计算索引时出错。

合并列表

列表的可变性使我们能够执行许多任务,例如将多个列表合并成一个列表。

这个操作很简单,使用 + 来进行,如下所示:

# Create a list
list1 = [1, 2, 3]

# Create a second list
list2 = [4, 5, 6]

# Concatenate list
combined_list = list1 + list2

# Print cncatenated lists
print(combined_list)

>>>

[1, 2, 3, 4, 5, 6]

当然,这个功能也可以在字符串上执行:

# Create a list
hello = ["Hello"]

# Create another list
world = ["world"]

# Concatenate
single_list = hello + world

# Print concatenated
print(single_list)

>>>

['Hello', 'world']

另一种合并列表的方法是将嵌套列表展平。换句话说,我们可以从嵌套列表中创建一个单一的“直线”列表,如下所示:

# Create a nested list
lists = [[1, 2], [3, 4], [5, 6]]

# Create a unique list
flattened_list = sum(lists, [])

# Print unique list
print(flattened_list)

>>>

[1, 2, 3, 4, 5, 6]

基本上,我们使用 sum() 方法来获取列表 lists 中的所有元素,并将它们附加到一个空列表 [] 中。

计算列表的长度

在前面的示例中,我们自己创建了列表,以演示如何操作列表的 Python 示例。

然而,当使用 Python 时,常常会从不同的来源检索数据,这意味着有人创建了一个我们实际上不知情的列表。

当我们面对一个未知列表时,我们最好先计算它的长度。我们可以这样做:

# Create a list
fruits = ['apple', 'banana', 'orange']

# Print list lenght
print(f"In this list there are {len(fruits)} elements")

>>>

 In this list there are 3 elements

所以,len() 方法计算列表中有多少个元素,而不必担心它们的类型。这意味着元素可以是所有数字、所有字符串,或两者兼有:len() 方法会统计它们全部。

对列表元素进行排序

当我们不知道列表的内容时,另一个可能执行的操作是对其元素进行排序。

我们有不同的方法来实现这一点。

我们从 sort() 方法开始:

# Creaye a list of numbers 
numbers = [5, 2, 1, 4, 3]

# Sort the numbers
numbers.sort()

# Print sorted list
print(numbers)

>>>

[1, 2, 3, 4, 5]

因此,我们可以直接将列表作为参数传递给 sort() 方法,它将对元素进行排序。

但如果我们想要排序一个包含字符串的列表呢?例如,假设我们想要按字母顺序对列表中的元素进行排序。我们可以这样做:

# Create a list of strings
words = ['cat', 'apple', 'dog', 'banana']

# Sort in alphabeticla order
sorted_words = sorted(words, key=lambda x: x[0])

# Print sorted list
print(sorted_words)

>>>

['apple', 'banana', 'cat', 'dog']

因此,在这种情况下,我们使用 sorted() 方法,需要指定:

  • 关于我们想要排序的列表的参数。在这种情况下,是 words

  • key。这意味着我们需要指定一种方法。在这种情况下,我们使用了一个 lambda 函数,通过 x[0] 获取每个元素的第一个字母,遍历所有元素:这是我们选择每个单词第一个字母的方式。

对字符串进行排序的另一种方式是按每个元素的字符数进行排序。换句话说,假设我们想要将较短的单词放在列表的开头,而将最长的单词放在末尾。我们可以这样做:

# Create a list of words
words = ['cat', 'apple', 'dog', 'banana']

# Sort words by lenght
words.sort(key=len)

# Print sorted list
print(words)

>>>

['cat', 'dog', 'apple', 'banana']

因此,即使使用sort()方法,我们也可以传递一个参数key。在这种情况下,我们选择了len,它计算每个单词的长度。因此,列表现在是按照从最短的单词到最长的单词的顺序排列的。

列表推导式

列表推导式是一种快速且简洁的方式,通过一行代码使用循环和语句的力量创建一个新列表。

让我们看一个例子。假设我们想取 1 到 6 的数字,并创建一个包含它们平方值的列表。我们可以这样做:

# Create a list of squared numbers
squares = [x ** 2 for x in range(1, 6)]

# Print list
print(squares) 

>>>

[1, 4, 9, 16, 25]

现在,我们可以不使用列表推导式而达到相同的结果,但需要大量代码,如下所示:

# Create empty list
squares = []

# Iterate over the numbers in the range
for squared in range(1, 6):
    # Calculare squares and append to empty list
    squares.append(squared ** 2)

# Print list    
print(squares)

>>>

[1, 4, 9, 16, 25]

因此,我们得到相同的结果,但列表推导式使我们只需一行代码即可实现。

我们还可以在列表推导式中使用if语句,这使得它比“标准方法”更加快捷和优雅,对于标准方法,我们需要使用for循环进行迭代,然后用if语句选择所需的值。

例如,假设我们想创建一个新的平方数列表,但只想要偶数。我们可以这样做:

# Create a list with numbers in a range
numbers = list(range(1, 11))

# Get the even squared numbers and create a new list
squared_evens = [x ** 2 for x in numbers if x % 2 == 0]

# Print list with squared & even numbers
print(squared_evens)

>>>

[4, 16, 36, 64, 100]

因此,我们需要记住,为了取得偶数,我们可以利用它们能被 2 整除的事实。所以,x % 2 == 0 获取那些被 2 除时余数为 0 的数字。也就是说:它们是偶数。

结论

在本文中,我们展示了关于 Python 列表的全面指南。

列表是一种非常重要且有用的数据结构。它们不难学习,但对于每个 Python 程序员来说都是一个基本资产。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Federico Trotta

我是 Federico Trotta,我是一名自由技术写作员。

想与我合作吗?联系我

Python 列表与 NumPy 数组:深入探讨内存布局和性能优势

原文:towardsdatascience.com/python-lists-vs-numpy-arrays-a-deep-dive-into-memory-layout-and-performance-benefits-a74ce774bc1e

快速计算

探索分配差异和效率提升

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Peng Qian

·发表于 Towards Data Science ·9 分钟阅读·2023 年 7 月 14 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NumPy 数组中的数据像书架上的书一样紧凑地排列。照片由 Eliabe Costa 拍摄,来源于 Unsplash

在本文中,我们将深入探讨原生 Python 列表和 NumPy 数组之间的内存设计差异,揭示为什么在许多情况下 NumPy 可以提供更好的性能。

我们将比较数据结构、内存分配和访问方法,展示 NumPy 数组的强大功能。

介绍

想象一下,你正在准备去图书馆找一本书。现在,你发现图书馆有两个货架:

第一个货架上装满了各种精美的盒子,有些盒子里装着光盘,有些装着图片,还有些装着书籍。只有物品的名称附在盒子上。

这代表了原生 Python 列表,其中每个元素都有自己的内存空间和类型信息。

然而,这种方法存在一个问题:盒子里有许多空余空间,浪费了货架空间。而且,当你想找一本特定的书时,你必须查看每一个盒子,这会花费额外的时间。

现在让我们来看第二个货架。这次没有盒子;书籍、光盘和图片都根据它们的类别紧凑地放在一起。

这是 NumPy 数组,它们在内存中以连续的方式存储数据,从而提高了空间利用率。

由于物品都是按类别分组的,你可以快速找到一本书,而不必在许多盒子中搜索。这就是为什么在许多操作中,NumPy 数组比原生 Python 列表更快的原因。

Python 列表:一种灵活但效率较低的解决方案

Python 中的一切都是对象

让我们从 Python 解释器开始:虽然 CPython 是用 C 编写的,但 Python 变量不是 C 中的基本数据类型,而是包含值和附加信息的 C 结构。

以 Python 整数x = 10_000为例,x不是栈上的基本类型。相反,它是指向内存堆对象的指针。

如果你深入研究Python 3.10的源代码,你会发现x所指向的 C 结构如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Python 整数与 C 原生整数。图像来源:作者。

PyObject_HEAD包含了如引用计数、类型信息和对象大小等信息。

Python 列表是包含一系列对象的对象

从中我们可以推断出,Python 列表也是一个对象,只不过它包含指向其他对象的指针。

我们可以创建一个只包含整数的列表:

integer_list = [1, 2, 3, 4, 5]

我们还可以创建一个包含多种对象类型的列表:

mixed_list = [1, "hello", 3.14, [1, 2, 3]]

Python 列表的优缺点

正如我们所看到的,Python 列表包含一系列指针对象。这些指针反过来指向内存中的其他对象。

这种方法的优点是灵活性。你可以将任何对象放入 Python 列表中,而无需担心类型错误。

然而,缺点也很明显:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Python 列表包含一系列指针对象。图像来源:作者

每个指针所指向的对象在内存中是分散的。当你遍历一个 Python 列表时,你需要根据指针查找每个对象的内存位置,这会导致性能下降。

NumPy 数组:一种用于增强性能的连续内存布局

接下来,让我们探索 NumPy 数组的组成部分和排列方式,以及它如何有利于缓存局部性向量化

NumPy 数组:结构和内存布局

根据 NumPy 的内部描述,NumPy 数组由两部分组成:

  1. 一部分存储了 NumPy 数组的元数据,描述了数据类型、数组形状等。

  2. 另一部分是数据缓冲区,它以紧凑的方式在内存中存储数组元素的值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NumPy 数组:结构和内存布局。图像来源:作者

NumPy 数组的内存布局

当我们观察 ndarray 的.flags属性时,我们发现它包括:

 In 1:  np_array = np.arange(6).reshape(2, 3, order='C')
        np_array.flags

Out 1:  C_CONTIGUOUS : True
        F_CONTIGUOUS : False
        OWNDATA : False
        WRITEABLE : True
        ALIGNED : True
        WRITEBACKIFCOPY : False
  • C_CONTIGUOUS,表示数据是否可以使用行优先顺序读取。

  • F_CONTIGUOUS,表示数据是否可以使用列优先顺序读取。

行优先顺序是 C 语言使用的数据排列方式,标记为order=’C’。这意味着数据按行存储。

另一方面,列优先顺序由 FORTRAN 使用,标记为order=’F’,按列存储数据。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

NumPy 数组的内存布局。图片由作者提供

NumPy 内存布局的优势

由于ndarray设计用于矩阵操作,它的所有数据类型都是相同的,具有相同的字节大小和解释。

这使得数据紧密打包在一起,带来了缓存局部性和向量化计算的优势。

缓存局部性:NumPy 的内存布局如何提高缓存利用率

什么是 CPU 缓存

NumPy 的连续内存布局有助于提高缓存命中率,因为它与 CPU 缓存的工作方式相匹配。为了更好地解释这一点,我们首先了解一下CPU 缓存的基本概念。

CPU 缓存是 CPU 和主内存(RAM)之间的小型高速存储区域。CPU 缓存的目的是加快内存中的数据访问速度。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

CPU 缓存是 CPU 和主内存(RAM)之间的小型高速存储区域。图片由作者提供

当 CPU 需要读写数据时,它首先检查数据是否已经在缓存中。

如果所需数据在缓存中(缓存命中),CPU 可以直接从缓存中读取。如果数据不在缓存中(缓存未命中),CPU 会从 RAM 中加载数据并将其存储在缓存中以供将来使用。

CPU 缓存通常以缓存行的形式组织,这些缓存行是连续的内存地址。当 CPU 访问 RAM 时,缓存会将整个缓存行加载到高速缓存中。

这意味着,如果 CPU 访问相邻的内存地址,在加载缓存行之后,后续访问更有可能命中缓存,从而提高性能。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当 CPU 访问 RAM 时,缓存会将整个缓存行加载到高速缓存中。图片由作者提供

NumPy 如何利用缓存

NumPy 的连续内存布局利用了这一事实。

NumPy 数组将数据存储在连续的内存地址中,这有助于提高缓存局部性。

当访问数组中的一个元素时,整个缓存行(包含相邻的数组元素)会被加载到缓存中。

当你遍历数组时,你会依次访问每个元素。由于数组元素在内存中是连续存储的,因此在遍历过程中缓存命中更有可能发生,从而提高性能。

这类似于去图书馆读书。你不仅取出所需的书,还会拿出相关的书并将它们放在桌子上。

这样,当你需要查阅相关材料时,它们会更容易获取,比起在书架上寻找更加高效。

向量化:释放 NumPy 内存布局的威力

什么是向量化

向量化是一种利用单指令多数据(SIMD)功能的技术,这些功能可在 CPU 或 GPU 上同时执行多个数据操作。

向量化操作可以通过同时处理多个数据项显著提高代码执行效率。

NumPy 的连续内存布局促进了向量化操作。

为什么向量化适用

假设你是一个每天必须向不同家庭送货的送货员。

假设包裹在车辆中按顺序排列,而房屋沿街编号。在这种情况下,送货员可以有效地沿街按顺序送达包裹。

这种高效的方法类似于 NumPy 的内存布局,在向量化中带来了以下好处:

  • 数据对齐:NumPy 数组的连续内存布局确保数据在内存中以向量化友好的方式对齐。这使得 CPU 能够高效地加载和处理 NumPy 中的数据。

  • 顺序访问模式:NumPy 在内存中紧凑的数据有助于提高向量化性能。顺序访问模式还充分利用了 CPU 缓存和预取功能,减少了内存访问延迟。

  • 简化代码:NumPy 提供了一系列函数(例如,np.addnp.multiply)和操作(例如,数组切片),这些函数和操作自动处理向量化操作。你可以编写简洁高效的代码,而无需担心底层实现。

副本和视图:NumPy 在性能优化方面的出色设计

在之前的讨论中,我们讨论了 NumPy 如何利用其连续内存布局来实现性能优势。

现在,让我们讨论 NumPy 如何通过副本和视图获得性能优势。

副本和视图是什么

副本和视图是定义现有数据与原始数组之间关系的两种选项。根据这两种选项的特性,可以总结如下:

  • 副本:使用与原始数组不同的内存空间,但数据内容相同。

  • 视图:引用与原始数组相同的内存地址。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个副本可以有多个视图。图片来源:作者

如果我们将其与书籍进行比较,视图就像书中的书签,而不需要创建书籍的副本。

另一方面,副本是原书的复制品,包含文本和图像的完整副本。当你在这个副本上做笔记时,原书不会受到影响。

充分利用这两种特性

利用视图和副本的特性可以帮助我们编写简洁高效的代码。

以算术操作为例。如果你使用 a = a + 1,将会创建一个新的副本。然而,如果你使用 a += 1np.add,则会应用广播,并且直接在原始数组上进行加法操作。

请看以下代码,该代码测量了执行时间:

执行上述代码将产生类似于以下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用视图进行计算所需时间更少。截图由作者提供

从结果来看,使用视图进行计算所需时间更少。

区分副本和视图

确认每次计算结果是视图还是副本将需要付出很大努力。

不过,还有一种更直接的验证方法:

  • 使用may_share_memory来判断参数中的两个数组是否引用相同的内存空间。这个判断可能不够严格。True 并不一定意味着数组是共享的,但 False 确认数组绝对不共享。

  • 如果你需要更准确的答案,可以使用share_memory函数。然而,这个函数的执行时间比may_share_memory要长。

结论

总结来说,我们了解了 NumPy 数组与原生 Python 列表之间在内存安排上的差异。

由于 NumPy 数组中相同数据类型的连续排列,显著提高了缓存局部性和向量化的性能优势。

在 NumPy 的设计中分离视图和副本,为代码执行性能和内存管理提供了更大的灵活性。

在接下来的系列文章中,我将从基础开始,重申工作中数据科学的最佳实践。如果你有任何建议或问题,请随时评论,我会逐一解答。

除了提高代码执行速度和性能外,使用各种工具提高工作效率也是一种性能提升:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Peng Qian

Python 工具箱

查看列表6 个故事外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 [## 加入 Medium 使用我的推荐链接 - Peng Qian

作为 Medium 的会员,你的部分会员费用将用于你阅读的作者,同时你可以全面访问每个故事……

medium.com](https://medium.com/@qtalen/membership?source=post_page-----a74ce774bc1e--------------------------------)

本文最初发布于:www.dataleadsfuture.com/python-lists-vs-numpy-arrays-a-deep-dive-into-memory-layout-and-performance-benefits/

Python Meets Pawn 2:基于开局的国际象棋大师聚类

原文:towardsdatascience.com/python-meets-pawn-2-clustering-chess-grandmasters-based-on-their-openings-68440fc9f9b1

在这篇博客中,我将引导你通过使用 Python 分析国际象棋大师开局的过程。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Mikayil Ahadli

·发表于 Towards Data Science ·7 分钟阅读·2023 年 12 月 22 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

由 Midjourney 创建的照片

  • 我在回答哪些问题

  • 第一部分:获取数据

  • 第二部分:特征工程

  • 第三部分:聚类

  • 结果与有趣的事实

我在回答哪些问题

我对国际象棋的热情不是什么秘密,这里我分享了自己棋局开局的分析。但今天,我将踏入一个新领域:国际象棋大师的世界。他们通常使用什么开局?他们的选择有多么多样?我对这些开局在不同国际象棋大师中的分布很感兴趣。顶级棋手是否偏爱相似的开局?是否可以根据他们的偏好进行分组?我不知道——让我们来探讨一下!

第一部分:获取数据

国际象棋的一个伟大方面是其数据的可获取性。有许多来源,包括pgnmentor,你可以在这里查看和下载关于开局和棋手的数据(免费)。这些数据每年更新多次,包括 Portable Game Notation (PGN)格式的棋局,这是国际象棋游戏最流行的格式。由于下载是逐个进行的,我选择了 11 位著名的国际象棋大师来下载和分析他们的开局。请注意,这个列表是主观的,包含了一些我最喜欢的国际象棋大师:

  1. Shakhriyar Mamedyarov

  2. Teimour Radjabov

  3. Hikaru Nakamura

  4. Magnus Carlsen

  5. Fabiano Caruana

  6. 丁立人

  7. Ian Nepomniachtchi

  8. Viswanathan Anand

  9. Anish Giri

  10. Vugar Gashimov

  11. Vladimir Kramnik

完整的代码将在博客末尾提供。为了解析 PGN 文件,我使用了名为‘Chess’的 Python 库中的 PGN 模块。

我用于解析数据的函数如下所示:

def parse_pgn_file(file_path):
    """
    Parses a PGN (Portable Game Notation) file containing chess games.

    Args:
        file_path (str): Path to the PGN file.

    Returns:
        pd.DataFrame: A DataFrame containing game information.
    """
    games = []  # Initialize an empty list to store parsed games.
    with open(file_path, "r") as pgn_file:
        while True:
            game = chess.pgn.read_game(pgn_file)  # Read a game from the PGN file.
            if game is None:
                break  # Exit the loop when no more games are found.
            games.append(game)  # Append the parsed game to the list.

    data = []  # Initialize an empty list to store game data.
    for game in games:
        data.append({
            "Event": game.headers.get("Event", ""),
            "Date": game.headers.get("Date", ""),
            "Result": game.headers.get("Result", ""),
            "White": game.headers.get("White", ""),
            "Black": game.headers.get("Black", ""),
            "Moves": " ".join(str(move) for move in game.mainline_moves()),
            "ECO": game.headers.get("ECO", "")
        })  # Extract relevant information from game headers and moves.

    df = pd.DataFrame(data)  # Create a DataFrame from the extracted data.
    return df  # Return the DataFrame containing game information.

以下是我解析和组合数据的表格显示。我将利用现有的“ECO”列,指示每盘棋中使用的开局。棋类中的 ECO 代码指的是“国际象棋开局百科全书”,这是一种用于分类各种开局的系统。每个代码由一个字母和两个数字组成,如 B12 或 E97,独特地标识某一特定开局或变体。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

解析的数据集(图片来源:作者)

特级大师们拥有数千盘棋局,涵盖 484 个独特的组合 ECO 代码。鉴于有 500 个独特的 ECO 代码,这 11 位特级大师几乎使用了职业生涯中的所有范围。然而,每位特级大师玩了多少个独特的开局?让我们查看以下图表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

独特开局图表(图片来源:作者)

这些数字与他们在数据集中的棋局数量高度相关,但总体而言,图表显示特级大师们在棋局中使用了各种各样的开局。

第二部分:特征工程

让我们开始查看每位特级大师最受欢迎的开局:

  • B90 — 西西里防御,Najdorf 变体 : Anand, Giri, Nepomniachtchi

  • D37 — 皇后弃兵 : Carlsen, Mamedyarov, Radjabov

  • C42 — 俄国棋局 : Gashimov, Kramnik

  • A05 — 印度王攻 : Nakamura

  • C65 — 西班牙棋局,柏林防御 : Caruana

  • E60 — 格鲁恩费尔德和印度棋局 : Ding

我猜看到一位俄国特级大师偏好俄国棋局并不奇怪。Gashimov 也偏好俄国棋局,表明苏联棋校在阿塞拜疆的强大影响。基于他们喜欢的开局发现一些模式是很有趣的。然而,为了实现更详细和分隔的分组,我将应用聚类技术,同时考虑其他开局。

让我们检查每位特级大师的开局分布。我将数据集以特级大师为索引,使用独特的 ECO 代码作为列,以棋局数量为值进行了透视。以下图表是马格纳斯·卡尔森的示例:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

马格纳斯的开局分布(图片来源:作者)

尽管特级大师们使用了各种开局,但明显有些开局比其他开局更具优势。大多数特级大师似乎偏好大约五种特定的开局,这影响了我决定集中于一个包含前 5 名开局的数据框。

对于聚类,我选择测试两个数据框:透视比例和前 5 个开局。使用后者取得了最佳结果,我将在下面详细解释。有关更多选项和详细见解,请参阅末尾提供的完整代码。在前 5 个开局数据框中,我使用了独热编码。在 11 位国际象棋大师中,前 5 个选择中有 24 个独特的 ECO 代码。这个数据框中的二进制值指示每位国际象棋大师的前 5 个开局中是否包含特定的 ECO 代码:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Top5 数据框(图片由作者提供)

下表显示了每位国际象棋大师的前 5 个 ECO。我们已经可以看到一些模式,但聚类将帮助我们更有效地区分它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

每位国际象棋大师的前 5 个开局结果(图片由作者提供)

第三部分:聚类

前 5 个最受欢迎的开局数据集包含 24 列。为了简化,我应用了 PCA(主成分分析)。这种方法有助于减少数据维度,同时保留重要信息。虽然第一个主成分提供了不错的结果,但我选择了两个成分。为什么?它们提供了几乎相同的洞察,并且使得可视化更容易。

对于分组国际象棋大师,我使用了 K-means 聚类。这就像把书籍分类到不同的类型中。首先,我选择了聚类的数量或“类型”。然后,将每位国际象棋大师的开局风格匹配到最接近的聚类中,就像将书籍分配到最合适的类型一样。这个过程会不断调整:代表每组共同风格的聚类中心会重新计算,国际象棋大师会相应地重新分配。这个过程会重复,直到聚类准确地表示出不同的游戏风格。通过 K-means,国际象棋开局中的不同模式浮现出来,突显了国际象棋大师们之间的不同策略。

选择正确的聚类数在任何聚类项目中都是关键。为此,我使用了肘部法则。这是一种确定数据分组理想聚类数的简单方法。你绘制一个图表,其中每个点代表一个不同的聚类数,并计算每个聚类的“组内平方和”(WCSS)。WCSS 衡量数据点到聚类中心的距离。在图表上,有一个点,在该点之后增加聚类数不会显著减少 WCSS。这个点类似于一个肘部,指示最佳的聚类数。它确保了聚类数和数据点之间的紧密分组之间的平衡。下面的图表演示了在我们的案例中,最佳聚类数是 4。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

确定最佳聚类数的肘部法则(图片由作者提供)

确定了聚类数量后,我对特级大师进行了聚类。为了评估我的聚类效果,我使用了轮廓系数。这个分数衡量了一个对象与其自身聚类的相似性与其他聚类的相似性。高轮廓系数表明数据聚类效果良好。该分数范围在-1 到 1 之间,我获得了 0.69 的分数,表明聚类效果有效。

最后,我在二维空间中可视化了聚类数据和质心(每个聚类的“中心”)。这一步将复杂的数据转化为易于理解和视觉上吸引人的格式,非常适合一目了然地看到模式和差异:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析结果(图片由作者提供)

结果和有趣的事实

我的分析揭示了国际象棋特级大师在开局方面展现了广泛的 repertoire,但他们之间有些偏好有所不同。基于这些开局对他们进行聚类不仅是可行的,而且得出了有趣的见解。例如,阿塞拜疆象棋传奇人物马梅杰罗夫和拉杰博夫被归为一组。有趣的是,安اند、吉里和卡鲁阿纳也紧密聚集在一起。仔细观察他们的前 5 个最爱开局,确认了这些结果。值得注意的是,安 Anand 和吉里分享了完全相同的前 5 个开局。这是否意味着吉里对安 Anand 的钦佩?确实,在互联网研究后,我发现吉里非常欣赏安 Anand 并从他的棋局中学习。以下是这些开局:

  • B90 — 西西里防御,奈杰多夫变体

  • C50 — 意大利开局

  • C42 — 俄国开局

  • C65 — 西班牙开局,柏林防御

  • C67 — 西班牙开局,柏林防御,其他变体

完整代码及 Jupyter notebook 文件可以在这里找到。

Python 遇见棋子:通过数据分析解码我的棋局开局

原文:towardsdatascience.com/python-meets-pawn-decoding-my-chess-openings-with-data-analysis-097a34cef20a

在这篇博客中,我将引导你通过使用 Python 分析你在 Chess.com 平台上进行的棋局。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 Mikayil Ahadli

·发表于 Towards Data Science ·8 分钟阅读·2023 年 11 月 17 日

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

照片由 DALL·E 创建

国际象棋一直是我的热情所在,这是一款由我的父亲介绍给我的美丽游戏。我早年间常与家人下棋,后来转向了 Chess.com 的数字棋盘。最近,国际象棋的受欢迎程度有所回升,这得益于知名主播和国际象棋大师的教育努力。这股新的兴趣浪潮激发了我在一系列关于棋局开局的思考:‘我经常使用的开局是什么,它们对我有多成功?’意识到我对自己的偏好或成功率一无所知,我决定将我最热爱的两个事物结合起来:国际象棋和 Python。

让我们开始理解这些步骤,学习如何使用 Chess.com API,并了解如何查看你在国际象棋中的开局动作吧!

附注:这篇博客假设你的笔记本电脑上已经安装了 Python,最好还安装了 Jupyter Notebook(或其他 IDE)。

Chess.com API

首先,你需要安装 Chess.com 库来使用其 API。你可以使用终端(或命令提示符)中的 “pip” 命令来安装,也可以在 Jupyter Notebook 中使用“!”符号来执行该语法。

pip install chess.com

你可以在 chesscom.readthedocs.io/en/latest/ 找到所有的说明和详细信息。这里包含了可以使用的每一种方法和参数。

你还需要传统的 pandas 和 numpy 库,你可以像上面一样安装它们。

获取数据

首先,让我们设置好所需的所有库,然后向 API 发出第一次请求。我们将使用一个叫做‘get_player_games_by_month’的方法来查看特定年份和月份玩的所有游戏。为了了解我们获得的数据类型,我们将查看一个示例游戏。通过使用 Python 内置的‘pprint’库,我们可以使 JSON 响应更易于阅读。

# Import necessary libraries
from chessdotcom import get_player_game_archives, get_player_games_by_month, Client
import pandas as pd
import numpy as np
from pprint import pprint

# Configure the user agent for the API requests to Chess.com
# this part is mandatory as per new version of API
Client.request_config["headers"]["User-Agent"] = (
   "My Python Application. "
   "Contact me at xxxx@gmail.com"
)

# get games for the month of November 2023
response_sample = get_player_games_by_month("mikayil94", year=2023, month=11)

# print the JSON
pprint(response_sample.json)

真正酷的部分在于 PGN(可移植棋局记录)部分——它包含了我们所需的一切,如开局名称和更多细节的链接(ECOUrl)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有一个叫做‘get_player_game_archives’的方法,它帮助我们获取平台上旧游戏的列表,这些游戏按我们玩它们的年份和月份排序。日期以链接格式呈现,所以我们只需要从每个链接中提取日期部分。

# Retrieve a list of months during which the player 'mikayil94' has played games
response1 = get_player_game_archives("mikayil94")
list_of_played_months = []
for i in response1.json['archives']:
    list_of_played_months.append(i[-7:])

现在是重点部分!我们可以使用之前找到的年份和月份,将值传递给‘get_player_games_by_month’方法,以获取关于我们游戏的更多信息。每场游戏将衍生出以下列:‘time_class’,‘date’,‘white’,‘black’,‘game_link’,‘opening_code’,‘opening_name’,‘opening_link’,‘result’。‘time_class’部分来源于不同于其他内容的地方,这些内容都在 PGN 部分内。我们真正需要的分析数据是玩家的名称(白方和黑方)和开局名称。每个开局的链接也是非常有用的。这样,我们可以了解更多信息,并提高使用这个开局的能力。此外,链接到游戏本身也很棒,因为它让我们可以回顾并理解我们是如何赢得或输掉每场游戏的。

# Create a DataFrame to store game information
my_games_df = pd.DataFrame(columns = ['time_class', 'date', 'white', 'black', 'game_link', 'opening_code', 'opening_name', 'opening_link', 'result'])

# Loop through each month and retrieve games played in that month
for months in list_of_played_months:
    response2 = get_player_games_by_month("mikayil94", year=months.split("/")[0], month=months.split("/")[1])  

    # Extract relevant information from each game and add it to the DataFrame
    for i in response2.json['games']:
        time_class = i['time_class']
        pgn = i['pgn']
        if "ECOUrl" not in pgn : continue  # Skip the game if it doesn't have an ECO URL

        # Extract various details from the PGN (Portable Game Notation) of the chess game
        date = pgn[pgn.find("Date"):].split(" ")[1].split("]")[0].strip('\"')
        white = pgn[pgn.find("White"):].split(" ")[1].split("]")[0].strip('\"')
        black = pgn[pgn.find("Black"):].split(" ")[1].split("]")[0].strip('\"')
        game_link = pgn[pgn.find("Link"):].split(" ")[1].split("]")[0].strip('\"')
        opening_code = pgn[pgn.find("ECO"):].split(" ")[1].split("]")[0].strip('\"')
        opening_name = pgn[pgn.find("ECOUrl"):].split(" ")[1].split("]")[0].split("/")[-1].strip('\"')    
        opening_link = pgn[pgn.find("ECOUrl"):].split(" ")[1].split("]")[0].strip('\"')    
        result = np.where(pgn[pgn.find("Termination"):].split(" ")[1].split("]")[0].strip('\"') == 'mikayil94', 'Win', 'Loss') # if my username is in this field, it means I was the Winner.

        # Create a new DataFrame for the current game and append it to the main DataFrame
        my_games_df_new = pd.DataFrame({'time_class' : [time_class], 'date' : [date], 'white' : [white], 'black' : [black], \
                        'game_link' : game_link, 'opening_code' : opening_code, 'opening_name' : [opening_name], 'opening_link' : [opening_link], 'result' : [result]})
        my_games_df = pd.concat([my_games_df, my_games_df_new], ignore_index=True) 

创建最终结果的变量

现在我们有了数据,我们需要添加一些内容以使其更清晰、更易于了解发生了什么。了解每场游戏中的开局是谁下的很重要。我是黑方时对这个开局进行了应对,还是白方时使用了它?为了解这个问题,我会检查每场游戏中我所处的一方。然后,通过查看我赢了还是输了每场游戏,我可以计算出每种开局的胜率。

# Add a new column 'opening_side' to the DataFrame. If the player 'mikayil94' is white, set the value to 'white', otherwise 'black'
my_games_df['opening_side'] = np.where(my_games_df.white == 'mikayil94', 'white', 'black')

# Add a new column 'result_binary'. If the result of the game is 'Win', set the value to 1, otherwise 0
my_games_df['result_binary'] = np.where(my_games_df.result == 'Win', 1, 0)

# Group the DataFrame by opening name, link, code, and the side 'mikayil94' played.
# Aggregate the data to count the total number of wins and total games played for each group
my_openings = my_games_df.groupby(["opening_name", "opening_link", "opening_code", "opening_side"], as_index=False).agg(
    games_win = ('result_binary', 'sum'),  # Sum of 'result_binary' to get total wins
    games_count = ('result_binary', 'count')  # Count of 'result_binary' to get total games played
)

# Calculate the win percentage for each opening and add it as a new column 'win_percentage'
# The win percentage is rounded to two decimal places
my_openings['win_percentage'] = round(my_openings.games_win / my_openings.games_count, 2)

结果就在这里!

现在我们可以看到结果了!我使用了 matplotlib 和 seaborn 库(如果没有,使用 pip 安装)来可视化数据。我创建了一个名为“opening_and_side”的新变量,用于可视化,指示哪一方(白方或黑方)使用了这个开局。我只查看了至少玩过 10 次的开局,以确保我的分析是准确的。

import matplotlib.pyplot as plt
import seaborn as sns

# Prepare the data for visualization
# Add new column, to concatenate opening name and opening side, which will be used in visualization
my_openings['opening_and_side'] = my_openings.opening_name + '[as ' + my_openings.opening_side + ']'
# filter data to show only games with at least 10 count
viz_data = my_openings[my_openings.games_count > 10].sort_values("win_percentage", ascending=False)[['opening_and_side', 'win_percentage']]

# Create a bar plot
plt.figure(figsize=(15, 10))
sns.barplot(x='win_percentage', y='opening_and_side', data=viz_data, palette="viridis", ci=None)
plt.title('Win Percentage by Chess Opening')
plt.xlabel('Win Percentage')
plt.ylabel('Opening Name')
plt.xticks(rotation=45)
plt.tight_layout()

# Display the plot
plt.show()

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

经过分析后的关键收获:

  • 奥文防御! 这是我在 2018 年和 2019 年的常用开局,但直到现在我才意识到我其实用得很好。这并不是一个常见的开局,因此对于让对手措手不及非常有效!事实证明,如果查看国际象棋大师的对局,这个开局相当稳固。黑方获胜的概率是 46.3%,而白方的胜率为 34.6%。你可以在国际象棋开局数据库中查看更多信息,地址是:old.chesstempo.com/chess-openings.html

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Barnes 开局-1…d5–2.e4表现不错。我之前不知道这个开局叫做 Barnes 开局,也不知道我在这个开局中的胜率很高。即使国际象棋开局数据库说这对白方并不是最佳开局,因为在下 f3 后,白方的评估值为-0.4,这会削弱王翼。但由于这并不是一个常见的开局,它似乎让我的对手感到意外。在这种情况下,黑方不应该吃掉那个兵,但我的对手大多数情况下都吃了,这让局面变得更均衡。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 范特-克鲁伊斯开局——在用白棋下这个开局时,我总是陷入更糟的局面,而当对手使用这个开局时,我则会获得优势,所以,这开局不太好!国际象棋开局数据库支持这一点:它显示,白方玩家使用这个开局时获胜的概率只有 36.5%,而对手的获胜概率是 45.3%!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 在对抗国王兵开局-离经叛道的皇后攻击时表现不佳。在 2019 年之后对这种开局没有近期的记录,我通常容易犯错误,陷入陷阱,不过,生活就是不断学习!😃

  • 国王兵开局-拿破仑攻击表现不好。幸运的是,我已经很久没用这个开局了!游戏开始时过早地调动皇后通常不是一个好棋步 😃

  • 当我用黑棋对抗皇后兵开局-加速伦敦系统时,通常不会立即陷入困境。但回头看,我用这个开局的胜率并不如我所希望的那么高。看来我需要花些时间更多地学习和练习这个开局。

结论

很高兴 Chess.com 提供了这个公开 API,让我们可以进行这种有趣的分析并发现一些有趣的事情。通过查看我的对局,我发现实际上在开始学习所有著名开局之前,我的表现更好。有时候,使用不寻常的开局可能是个好事。那么,为什么不试试让你的对手惊讶的 Barnes 开局或奥文防御呢?只要小心对手使用离经叛道的皇后攻击时不要犯错误。

感谢你陪伴我读到最后!希望你读得愉快,并且可能对国际象棋、Python,或使用 Python 分析你自己的国际象棋对局感兴趣 😃

Jupyter notebook 文件可以在这里找到。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值