Python是C ++的终极案例。 第2/2部分

待续。 从Python开始, 作为C ++的终极案例。 第1/2部分 ”。


变量和数据类型


现在我们终于弄清楚了数学,让我们决定在我们的语言中什么变量意味着什么。


在C ++中,程序员可以选择:使用自动变量放置在堆栈上,或将值保留在程序数据存储器中,仅将指向这些值的指针放置在堆栈上。 如果我们仅为Python选择这些选项之一怎么办?


当然,我们不能总是仅使用变量的值,因为大型数据结构无法容纳在堆栈上,否则它们在堆栈上的不断移动将产生性能问题。 因此,我们将仅在Python中使用指针。 这将从概念上简化该语言。


所以表达


a = 3 

这将意味着我们在程序数据存储器(即所谓的“堆”)中创建了一个对象“ 3”,并将名称“ a”作为对其的引用。 和表达


 b = a 

在这种情况下,这意味着我们强制变量“ b”引用内存中与“ a”引用的对象相同的对象,换句话说,就是复制了指针。


如果一切都是指针,那么我们需要用我们的语言实现多少个列表类型? 当然,只有一个是指针列表! 您可以使用它来存储整数,字符串,其他列表,无论如何-毕竟,这些都是指针。


我们需要实现多少种哈希表? (在Python中,这种类型称为“字典” dict 。)一个! 让它将指向键的指针与指向值的指针相关联。


因此,我们不需要用我们的语言来实现C ++规范的很大一部分-模板,因为我们对对象执行所有操作,并且对象始终可以通过指针访问。 当然,用Python编写的程序不必局限于使用指针:像NumPy这样的库可以帮助科学家处理内存中的数据数组,就像在Fortran中一样。 但是语言的基础-像“ a = 3”之类的表达式-始终可以使用指针。


“一切都是指针”的概念也将类型的构成简化到极限。 需要字典列表吗? 只需创建一个列表并将字典放在这里即可! 您不需要征求Python的许可,也不需要声明其他类型,一切都可以直接使用。


但是,如果我们想使用复合对象作为键怎么办? 字典中的键必须具有不可变的值,否则如何通过它搜索值? 列表可能会更改,因此不能以此身份使用。 在这种情况下,Python的数据类型类似于列表,是对象的序列,但是与列表不同,该序列不会改变。 这种类型称为元组或tuple (发音为“ tuple”或“ tuple”)。


Python中的元组解决了一个长期存在的脚本语言问题。 如果您对此功能不满意,那么您可能从未尝试使用脚本语言来处理数据,在这些数据中,您只能使用字符串或原始类型作为哈希表中的键。


元组给我们的另一种可能性是,从函数返回多个值而不必为此声明其他数据类型,就像您在C和C ++中所做的那样。 此外,为了使使用此功能更容易,赋值运算符具有自动将元组解包为单独变量的功能。


 def get_address(): ... return host, port host, port = get_address() 

拆包有几个有用的副作用,例如,变量值的交换可编写如下:


 x, y = y, x 

一切都是指针,这意味着函数和数据类型可以用作数据。 如果您熟悉《四人帮》(The Gang of Four)作者的“设计模式”(Design Patterns)一书,则必须记住它提供了哪些复杂且令人困惑的方法,以便在运行时对由程序创建的对象类型的选择进行参数化。 确实,在许多编程语言中,这很难做到! 在Python中,所有这些困难都消失了,因为我们知道函数可以返回数据类型,函数和数据类型都只是链接,并且链接可以存储在例如字典中。 这将任务简化到了极限。


戴维·惠勒(David Wheeler)说:“所有编程问题都可以通过创建更高级别的间接解决。” 在Python中使用链接是间接级别,该级别通常用于解决许多语言(包括C ++)中的许多问题。 但是,如果在此显式使用它,并使程序复杂化,则在Python中,它隐式地针对所有类型的数据统一使用,并且易于使用。


但是,如果一切都是链接,那么这些链接指的是什么? 像C ++这样的语言有很多类型。 让我们在Python中只保留一种数据类型-一个对象! 类型理论领域的专家不赞成动摇头,但是我认为,一种源数据类型可以确保语言的统一性和易用性,是一个好主意,可以从中衍生出语言中的所有其他类型。


对于特定的内存内容,各种Python实现(PyPy,Jython或MicroPython)可以以不同的方式管理内存。 但是,为了更好地理解Python的简单性和统一性是如何实现的,以形成正确的思维模型,最好使用C语言中称为CPython的Python参考实现,可以从python.org下载。


 struct { struct _typeobject *ob_type; /* followed by object's data */ } 

我们将在CPython源代码中看到的是一个结构,该结构由指向给定变量类型信息的指针和定义变量特定值的有效负载组成。


类型信息如何工作? 让我们再次研究CPython源代码。


 struct _typeobject { /* ... */ getattrfunc tp_getattr; setattrfunc tp_setattr; /* ... */ newfunc tp_new; freefunc tp_free; /* ... */ binaryfunc nb_add; binaryfunc nb_subtract; /* ... */ richcmpfunc tp_richcompare; /* ... */ } 

我们看到了指向提供给定类型可能的所有操作的函数的指针:加,减,比较,对属性的访问,索引,切片等。这些操作知道如何使用内存中的有效负载在指向类型信息的指针下方,可以是整数,字符串或用户创建的类型的对象。


这与C和C ++根本不同,在C和C ++中,类型信息与名称而不是变量的值相关联。 在Python中,所有名称都与链接关联。 引用的值又是类型。 这是动态语言的本质。


为了实现该语言的所有功能,对于我们而言,在链接上定义两个操作就足够了。 最明显的之一就是复制。 当我们为变量,字典中的插槽或对象的属性分配值时,我们将复制链接。 这是一个简单,快速且完全安全的操作:复制链接不会更改对象的内容。


第二个操作是函数或方法调用。 如上所示,Python程序只能通过内置对象中实现的方法与内存进行交互。 因此,它不会导致与内存访问有关的错误。


您可能有一个问题:如果所有变量都包含引用,那么如何通过将变量作为参数传递给函数来保护变量的值免受更改?


 n = 3 some_function(n) # Q: I just passed a pointer! # Could some_function() have changed “3”? 

答案是Python中的简单类型是不可变的:它们根本没有实现负责更改其值的方法。 不可变(intmutable)的intfloattuplestr以“一切都是指针”之类的语言提供,与自动变量在C中提供的语义效果相同。


统一的类型和方法尽可能地简化了通用程序或泛型的使用。 函数min()max()sum()等是内置函数,无需导入。 并且它们可与实现了min()max()比较操作, sum()加法运算等任何数据类型一起使用。


创建对象


我们大致了解了对象应如何表现。 现在,我们将确定如何创建它们。 这是语言语法的问题。 C ++支持至少三种创建对象的方式:


  1. 自动,方法是声明此类的变量:
     my_class c(arg); 
  2. 使用new运算符:
     my_class *c = new my_class(arg); 
  3. 工厂,通过调用返回指针的任意函数:
     my_class *c = my_factory(arg); 

正如您可能已经猜到的那样,在上述示例中研究了Python创建者的思维方式后,现在我们必须选择其中之一。


从同一本书《四人帮》中,我们了解到工厂是创建对象的最灵活,最通用的方法。 因此,仅在Python中实现此方法。


除了通用性之外,此方法还不错,因为您无需使用不必要的语法来重载该语言以确保它:用我们的语言已经实现了函数调用,而工厂不过是一个函数。


在Python中创建对象的另一条规则是:任何数据类型都是其自己的工厂。 当然,您可以编写任意数量的其他自定义工厂(当然,它们将是普通的函数或方法),但一般规则将保持有效:


 # Let's make type objects # their own type's factories! c = MyClass() i = int('7') f = float(length) s = str(bytes) 

所有类型都称为对象,它们都返回其类型的值,该值由调用中传递的参数确定。


因此,仅使用语言的基本语法,就可以封装创建对象(例如“ Arena”或“ Adaptation”模式)时的任何操作,因为从C ++借鉴的另一个好主意是类型本身决定了它如何发生生成对象, new操作员如何为他工作。


NULL怎么样?


处理空指针会增加程序的复杂性,因此我们禁止使用NULL。 Python语法使创建空指针成为不可能。 我们定义了指针上的两个基本操作,以使任何变量指向某个对象的方式进行定义。


结果,用户无法使用Python创建与内存访问相关的错误,例如分段错误或缓冲区限制。 换句话说,Python程序不受过去20年威胁互联网安全的两种最危险类型的漏洞的影响。


您可能会问:“如果像我们前面所看到的那样,如果对象上的操作结构不变,那么用户将如何创建自己的类,而方法和属性未在此结构中列出?”


神奇之处在于,对于自定义类,Python具有非常简单的“准备”,并且实现了少量方法。 这里是最重要的:


 struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; /* ... */ newfunc tp_new; /* ... */ } 

tp_new()为用户类创建一个哈希表,与dict类型相同。 tp_getattr()从此哈希表中提取内容,而tp_setattr()相反,将内容放置在那里。 因此,不是在C语言结构的级别上而是在更高的级别(哈希表)上提供了任意类存储任何方法和属性的能力。 (当然,除了某些与性能优化有关的情况之外。)


访问修饰符


我们如何处理围绕privateprotected C ++关键字构建的所有这些规则和概念? Python是一种脚本语言,不需要它们。 我们已经有部分“受保护的”语言-这些是内置类型的数据。 例如,在任何情况下,Python都不允许程序操纵浮点数的位! 这种封装级别足以维持语言本身的完整性。 我们,Python的创造者,相信语言完整性是隐藏信息的唯一良好借口。 所有其他结构和用户程序数据都被视为公开的。


您可以在类属性名称的开头添加下划线( _ ),以警告同事:您不应依赖此属性。 但是Python的其余部分吸取了90年代初的教训:然后许多人认为,我们编写肿的,不可读的和有错误的程序的主要原因是缺少私有变量。 我认为接下来的20年已经使编程行业的每个人都信服:私有变量并不是唯一的方法,并且远非肿和错误的程序的最有效补救方法。 因此,Python的创建者决定甚至不用担心私有变量,并且正如您所看到的,它们没有失败。


记忆体管理


我们的对象,数字和字符串在较低级别会发生什么? 它们是如何精确地存储在内存中的,CPython如何在何时以及在什么条件下销毁它们?


在这种情况下,我们选择了最通用,可预测和最有效的内存处理方式:从C程序的角度来看,我们所有的对象都是共享指针


考虑到这一点,我们应该在下面的“变量和数据类型”部分中检查数据结构,以补充这些数据结构:


 struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; /* followed by object's data */ } } 

因此,Python中的每个对象(当然,我们指的是CPython的实现)都有其自己的引用计数器。 一旦变为零,就可以删除该对象。


链接计数机制不依赖于其他计算或后台进程-可以立即销毁一个对象。 此外,它提供了很高的数据局部性:通常,内存在释放后立即开始再次使用。 刚被破坏的对象最有可能在最近使用,这意味着它在处理器缓存中。 因此,新创建的对象将保留在缓存中。 这两个因素-简单性和局部性-使链接计数成为一种非常有效的垃圾收集方式。


(由于实际程序中的对象经常互相引用,因此即使在程序中不再使用对象时,在某些情况下引用计数器也无法降为零。因此,CPython还具有第二种垃圾回收机制-一种基于在几代物体上- 大约翻译


Python开发人员错误


我们试图开发一种对初学者来说足够简单但对专业人士也足够吸引人的语言。 同时,我们无法避免在理解和使用我们自己创建的工具时出现错误。


由于与脚本语言相关的思维惯性,Python 2试图转换字符串类型,就像类型弱的语言一样。 如果您尝试将字节字符串与Unicode中的字符串组合,则解释器会使用系统上可用的代码表将字节字符串隐式转换为Unicode,并以Unicode形式显示结果:


 >>> 'byte string ' + u'unicode string' u'byte string unicode string' 

结果,一些网站在其用户使用英语时运行良好,但在使用其他字母的字符时却产生了隐式错误。


此语言设计错误已在Python 3中修复:


 >>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str 

Python 2中类似的错误与由无与伦比的元素组成的列表的“天真”排序有关:


 >>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b'] 

在这种情况下,Python 3向用户清楚表明他正在尝试做一些不太有意义的事情:


 >>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str() 

滥用行为


用户有时会滥用Python语言的动态特性,然后在90年代,当最佳实践尚未广为人知时,这种情况尤其经常发生:


 class Address(object): def __init__(self, host, port): self.host = host self.port = port 

“但这不是最佳选择!” -有人说-“如果端口与默认值没有区别怎么办? 无论如何,我们在其存储上花费了整个类的属性!” 结果是像


 class Address(object): def __init__(self, host, port=None): self.host = host if port is not None: # so terrible self.port = port 

因此,相同类型的对象会出现在程序中,但是由于其中一些对象具有特定的属性,而另一些对象却没有统一的属性,因此无法统一操作! 而且,如果不事先检查其存在就无法触摸此属性:


 # code was forced to use introspection # (terrible!) if hasattr(addr, 'port'): print(addr.port) 

当前,大量的hasattr()isinstance()和其他自省是错误代码的肯定标志,并且使属性始终存在于对象中被认为是最佳实践。 访问时,这提供了一种更简单的语法:


 # today's best practice: # every atribute always present if addr.port is not None: print(addr.port) 

因此,有关动态添加和删除属性的早期实验结束了,现在我们以与C ++几乎相同的方式查看Python中的类。


早期Python的另一个坏习惯是使用函数,其中参数可以具有完全不同的类型。 例如,您可能认为用户每次创建一个列名列表可能太困难了,应该让他也将它们作为一行传递,其中各个列的名称之间用逗号分隔:


 class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns 

但是这种方法会引起问题。 例如,如果用户不小心给了我们一个不打算用作列名列表的行怎么办? 还是列名应包含逗号?


同样,这样的代码更难以维护,调试,尤其是测试:在测试中,仅可以检查我们支持的两种类型中的一种,但是覆盖率仍为100%,而我们不会测试另一种类型。


结果是,我们得出的结论是Python允许用户将任何类型的参数传递给函数,但是大多数情况下它们中的大多数都将以与C中相同的方式使用函数:将相同类型的参数传递给它。


在程序中需要使用eval()被认为是显式的架构错误计算。 最有可能的是,您只是不知道如何以正常方式进行相同操作。 − , Jupyter notebook - − eval() , Python ! , C++ .


, ( getattr() , hasattr() , isinstance() ) . , , , , : , , , !



: , . 20 , C++ Python. , , . .


, shared_ptr TensorFlow 2016 2018 .


TensorFlow − C++-, Python- ( C++ − TensorFlow, ).


图片


TensorFlow, shared_ptr , . , .


C++? . , ? , , C++ Python!

Source: https://habr.com/ru/post/zh-CN464405/


All Articles