suchen
HeimBackend-EntwicklungPython-Tutorial用Python进行基础的函数式编程的教程

许多函数式文章讲述的是组合,流水线和高阶函数这样的抽象函数式技术。本文不同,它展示了人们每天编写的命令式,非函数式代码示例,以及将这些示例转换为函数式风格。

文章的第一部分将一些短小的数据转换循环重写成函数式的maps和reduces。第二部分选取长一点的循环,把他们分解成单元,然后把每个单元改成函数式的。第三部分选取一个很长的连续数据转换循环,然后把它分解成函数式流水线。

示例都是用Python写的,因为很多人觉得Python易读。为了证明函数式技术对许多语言来说都相同,许多示例避免使用Python特有的语法:map,reduce,pipeline。
导引

当人们谈论函数式编程,他们会提到非常多的“函数式”特性。提到不可变数据1,第一类对象2以及尾调用优化3。这些是帮助函数式编程的语言特征。提到mapping(映射),reducing(归纳),piplining(管道),recursing(递归),currying4(科里化);以及高阶函数的使用。这些是用来写函数式代码的编程技术。提到并行5,惰性计算6以及确定性。这些是有利于函数式编程的属性。

忽略全部这些。可以用一句话来描述函数式代码的特征:避免副作用。它不会依赖也不会改变当前函数以外的数据。所有其他的“函数式”的东西都源于此。当你学习时把它当做指引。

这是一个非函数式方法:

 
a = 0
def increment1():
  global a
  a += 1

这是一个函数式的方法:

 
def increment2(a):
  return a + 1

不要在lists上迭代。使用map和reduce。
Map(映射)

Map接受一个方法和一个集合作为参数。它创建一个新的空集合,以每一个集合中的元素作为参数调用这个传入的方法,然后把返回值插入到新创建的集合中。最后返回那个新集合。

这是一个简单的map,接受一个存放名字的list,并且返回一个存放名字长度的list:

 
name_lengths = map(len, ["Mary", "Isla", "Sam"])
 
print name_lengths
# => [4, 4, 3]

接下来这个map将传入的collection中每个元素都做平方操作:

 
squares = map(lambda x: x * x, [0, 1, 2, 3, 4])
 
print squares
# => [0, 1, 4, 9, 16]

这个map并没有使用一个命名的方法。它是使用了一个匿名并且内联的用lambda定义的方法。lambda的参数定义在冒号左边。方法主体定义在冒号右边。返回值是方法体运行的结果。

下面的非函数式代码接受一个真名列表,然后用随机指定的代号来替换真名。

 
import random
 
names = ['Mary', 'Isla', 'Sam']
code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']
 
for i in range(len(names)):
  names[i] = random.choice(code_names)
 
print names
# => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']

(正如你所见的,这个算法可能会给多个密探同一个秘密代号。希望不会在任务中混淆。)

这个可以用map重写:

 
import random
 
names = ['Mary', 'Isla', 'Sam']
 
secret_names = map(lambda x: random.choice(['Mr. Pink',
                      'Mr. Orange',
                      'Mr. Blonde']),
          names)

练习1.尝试用map重写下面的代码。它接受由真名组成的list作为参数,然后用一个更加稳定的策略产生一个代号来替换这些名字。

 
names = ['Mary', 'Isla', 'Sam']
 
for i in range(len(names)):
  names[i] = hash(names[i])
 
print names
# => [6306819796133686941, 8135353348168144921, -1228887169324443034]

(希望密探记忆力够好,不要在执行任务时把代号忘记了。)

我的解决方案:

 
names = ['Mary', 'Isla', 'Sam']
 
secret_names = map(hash, names)

Reduce(迭代)

Reduce 接受一个方法和一个集合做参数。返回通过这个方法迭代容器中所有元素产生的结果。

这是个简单的reduce。返回集合中所有元素的和。

 
sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])
 
print sum
# => 10

x是迭代的当前元素。a是累加和也就是在之前的元素上执行lambda返回的值。reduce()遍历元素。每次迭代,在当前的a和x上执行lambda然后返回结果作为下一次迭代的a。

第一次迭代的a是什么?在这之前没有迭代结果传进来。reduce() 使用集合中的第一个元素作为第一次迭代的a,然后从第二个元素开始迭代。也就是说,第一个x是第二个元素。

这段代码记'Sam'这个词在字符串列表中出现的频率:

 
sentences = ['Mary read a story to Sam and Isla.',
       'Isla cuddled Sam.',
       'Sam chortled.']
 
sam_count = 0
for sentence in sentences:
  sam_count += sentence.count('Sam')
 
print sam_count
# => 3

下面这个是用reduce写的:

 
sentences = ['Mary read a story to Sam and Isla.',
       'Isla cuddled Sam.',
       'Sam chortled.']
 
sam_count = reduce(lambda a, x: a + x.count('Sam'),
          sentences,
          0)

这段代码如何初始化a?出现‘Sam'的起始点不能是'Mary read a story to Sam and Isla.' 初始的累加和由第三个参数来指定。这样就允许了集合中元素的类型可以与累加器不同。
为什么map和reduce更好?

首先,它们大多是一行代码。

二、迭代中最重要的部分:集合,操作和返回值,在所有的map和reduce中总是在相同的位置。

三、循环中的代码可能会改变之前定义的变量或之后要用到的变量。照例,map和reduce是函数式的。

四、map和reduce是元素操作。每次有人读到for循环,他们都要逐行读懂逻辑。几乎没有什么规律性的结构可以帮助理解代码。相反,map和reduce都是创建代码块来组织复杂的算法,并且读者也能非常快的理解元素并在脑海中抽象出来。“嗯,代码在转换集合中的每一个元素。然后结合处理的数据成一个输出。”

五、map和reduce有许多提供便利的“好朋友”,它们是基本行为的修订版。例如filter,all,any以及find。

练习2。尝试用map,reduce和filter重写下面的代码。Filter接受一个方法和一个集合。返回集合中使方法返回true的元素。

 
people = [{'name': 'Mary', 'height': 160},
     {'name': 'Isla', 'height': 80},
     {'name': 'Sam'}]
 
height_total = 0
height_count = 0
for person in people:
  if 'height' in person:
    height_total += person['height']
    height_count += 1
 
if height_count > 0:
  average_height = height_total / height_count
 
  print average_height
  # => 120

如果这个比较棘手,试着不要考虑数据上的操作。考虑下数据要经过的状态,从people字典列表到平均高度。不要尝试把多个转换捆绑在一起。把每一个放在独立的一行,并且把结果保存在命名良好的变量中。代码可以运行后,立刻凝练。

我的方案:

 
people = [{'name': 'Mary', 'height': 160},
     {'name': 'Isla', 'height': 80},
     {'name': 'Sam'}]
 
heights = map(lambda x: x['height'],
       filter(lambda x: 'height' in x, people))
 
if len(heights) > 0:
  from operator import add
  average_height = reduce(add, heights) / len(heights)

写声明式代码,而不是命令式

下面的程序演示三辆车比赛。每次移动时间,每辆车可能移动或者不动。每次移动时间程序会打印到目前为止所有车的路径。五次后,比赛结束。

下面是某一次的输出:

 
-
--
--
 
--
--
---
 
---
--
---
 
----
---
----
 
----
----
-----

这是程序:
 

from random import random
 
time = 5
car_positions = [1, 1, 1]
 
while time:
  # decrease time
  time -= 1
 
  print ''
  for i in range(len(car_positions)):
    # move car
    if random() > 0.3:
      car_positions[i] += 1
 
    # draw car
    print '-' * car_positions[i]

代码是命令式的。一个函数式的版本应该是声明式的。应该描述要做什么,而不是怎么做。
使用方法

通过绑定代码片段到方法里,可以使程序更有声明式的味道。
 

from random import random
 
def move_cars():
  for i, _ in enumerate(car_positions):
    if random() > 0.3:
      car_positions[i] += 1
 
def draw_car(car_position):
  print '-' * car_position
 
def run_step_of_race():
  global time
  time -= 1
  move_cars()
 
def draw():
  print ''
  for car_position in car_positions:
    draw_car(car_position)
 
time = 5
car_positions = [1, 1, 1]
 
while time:
  run_step_of_race()
  draw()

想要理解这段代码,读者只需要看主循环。”如果time不为0,运行下run_step_of_race和draw,在检查下time。“如果读者想更多的理解这段代码中的run_step_of_race或draw,可以读方法里的代码。

注释没有了。代码是自描述的。

把代码分解提炼进方法里是非常好且十分简单的提高代码可读性的方法。

这个技术用到了方法,但是只是当做常规的子方法使用,只是简单地将代码打包。根据指导,这些代码不是函数式的。代码中的方法使用了状态,而不是传入参数。方法通过改变外部变量影响了附近的代码,而不是通过返回值。为了搞清楚方法做了什么,读者必须仔细阅读每行。如果发现一个外部变量,必须找他它的出处,找到有哪些方法修改了它。
移除状态

下面是函数式的版本:
 

from random import random
 
def move_cars(car_positions):
  return map(lambda x: x + 1 if random() > 0.3 else x,
        car_positions)
 
def output_car(car_position):
  return '-' * car_position
 
def run_step_of_race(state):
  return {'time': state['time'] - 1,
      'car_positions': move_cars(state['car_positions'])}
 
def draw(state):
  print ''
  print 'n'.join(map(output_car, state['car_positions']))
 
def race(state):
  draw(state)
  if state['time']:
    race(run_step_of_race(state))
 
race({'time': 5,
   'car_positions': [1, 1, 1]})

代码仍然是分割提炼进方法中,但是这个方法是函数式的。函数式方法有三个标志。首先,没有共享变量。time和car_positions直接传进方法race中。第二,方法接受参数。第三,方法里没有实例化变量。所有的数据变化都在返回值中完成。rece() 使用run_step_of_race() 的结果进行递归。每次一个步骤会产生一个状态,这个状态会直接传进下一步中。

现在,有两个方法,zero() 和 one():
 

def zero(s):
  if s[0] == "0":
    return s[1:]
 
def one(s):
  if s[0] == "1":
    return s[1:]

zero() 接受一个字符串 s 作为参数,如果第一个字符是'0′ ,方法返回字符串的其他部分。如果不是,返回None,Python的默认返回值。one() 做的事情相同,除了第一个字符要求是'1′。

想象下一个叫做rule_sequence()的方法。接受一个string和一个用于存放zero()和one()模式的规则方法的list。在string上调用第一个规则。除非返回None,不然它会继续接受返回值并且在string上调用第二个规则。除非返回None,不然它会接受返回值,并且调用第三个规则。等等。如果有哪一个规则返回None,rule_sequence()方法停止,并返回None。不然,返回最后一个规则方法的返回值。

下面是一个示例输出:
 

print rule_sequence('0101', [zero, one, zero])
# => 1
 
print rule_sequence('0101', [zero, zero])
# => None

This is the imperative version of rule_sequence():
这是一个命令式的版本:
 

def rule_sequence(s, rules):
  for rule in rules:
    s = rule(s)
    if s == None:
      break
 
  return s

练习3。上面的代码用循环来完成功能。用递归重写使它更有声明式的味道。

我的方案:
 

def rule_sequence(s, rules):
  if s == None or not rules:
    return s
  else:
    return rule_sequence(rules[0](s), rules[1:])

使用流水线

在之前的章节,一些命令式的循环被重写成递归的形式,并被用以调用辅助方法。在本节中,会用pipline技术重写另一种类型的命令式循环。

下面有个存放三个子典型数据的list,每个字典存放一个乐队相关的三个键值对:姓名,不准确的国籍和激活状态。format_bands方法循环处理这个list。
 

bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False},
     {'name': 'women', 'country': 'Germany', 'active': False},
     {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}]
 
def format_bands(bands):
  for band in bands:
    band['country'] = 'Canada'
    band['name'] = band['name'].replace('.', '')
    band['name'] = band['name'].title()
 
format_bands(bands)
 
print bands
# => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},
#   {'name': 'Women', 'active': False, 'country': 'Canada' },
#   {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]

担心源于方法的名称。”format” 是一个很模糊的词。仔细查看代码,这些担心就变成抓狂了。循环中做三件事。键值为'country'的值被设置为'Canada'。名称中的标点符号被移除了。名称首字母改成了大写。但是很难看出这段代码的目的是什么,是否做了它看上去所做的。并且代码难以重用,难以测试和并行。

和下面这段代码比较一下:

 
print pipeline_each(bands, [set_canada_as_country,
              strip_punctuation_from_name,
              capitalize_names])

这段代码很容易理解。它去除了副作用,辅助方法是函数式的,因为它们看上去是链在一起的。上次的输出构成下个方法的输入。如果这些方法是函数式的,那么就很容易核实。它们很容易重用,测试并且也很容易并行。

pipeline_each()的工作是传递bands,一次传一个,传到如set_cannada_as_country()这样的转换方法中。当所有的bands都调用过这个方法之后,pipeline_each()将转换后的bands收集起来。然后再依次传入下一个方法中。

我们来看看转换方法。
 

def assoc(_d, key, value):
  from copy import deepcopy
  d = deepcopy(_d)
  d[key] = value
  return d
 
def set_canada_as_country(band):
  return assoc(band, 'country', "Canada")
 
def strip_punctuation_from_name(band):
  return assoc(band, 'name', band['name'].replace('.', ''))
 
def capitalize_names(band):
  return assoc(band, 'name', band['name'].title())

每一个都将band的一个key联系到一个新的value上。在不改变原值的情况下是很难做到的。assoc()通过使用deepcopy()根据传入的dictionary产生一个拷贝来解决这个问题。每个转换方法修改这个拷贝,然后将这个拷贝返回。

似乎这样就很好了。原始Band字典不再担心因为某个键值需要关联新的值而被改变。但是上面的代码有两个潜在的副作用。在方法strip_punctuation_from_name()中,未加标点的名称是通过在原值上调用replace()方法产生的。在capitalize_names()方法中,将名称的首字母大写是通过在原值上调用title()产生的。如果replace()和title()不是函数式的,strip_punctuation_from_name()和capitalize_names()也就不是函数式的。

幸运的是,replace() 和 title()并不改变它们所操作的string。因为Python中的strings是不可变的。例如,当replace()操作band的名称字符串时,是先拷贝原字符串,然后对拷贝的字符串做修改。啧啧。

Python中string和dictionaries的可变性比较阐述了类似Clojure这类语言的吸引力。程序员永远不用担心数据是否可变。数据是不可变的。

练习4。试着重写pipeline_each方法。考虑操作的顺序。每次从数组中拿出一个bands传给第一个转换方法。然后类似的再传给第二个方法。等等。

My solution:
我的方案:
 

def pipeline_each(data, fns):
  return reduce(lambda a, x: map(x, a),
         fns,
         data)

所有的三个转换方法归结于对传入的band的特定字段进行更改。call()可以用来抽取这个功能。call接受一个方法做参数来调用,以及一个值的键用来当这个方法的参数。
 

set_canada_as_country = call(lambda x: 'Canada', 'country')
strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')
capitalize_names = call(str.title, 'name')
 
print pipeline_each(bands, [set_canada_as_country,
              strip_punctuation_from_name,
              capitalize_names])

或者,如果我们希望能满足简洁方面的可读性,那么就:
 

print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
call(lambda x: x.replace('.', ''), 'name'),
call(str.title, 'name')])

call()的代码:
 

def assoc(_d, key, value):
  from copy import deepcopy
  d = deepcopy(_d)
  d[key] = value
  return d
 
def call(fn, key):
  def apply_fn(record):
    return assoc(record, key, fn(record.get(key)))
  return apply_fn

There is a lot going on here. Let's take it piece by piece.

这段代码做了很多事。让我们一点一点的看。

一、call() 是一个高阶函数。高阶函数接受一个函数作为参数,或者返回一个函数。或者像call(),两者都有。

二、apply_fn() 看起来很像那三个转换函数。它接受一个record(一个band),查找在record[key]位置的值,以这个值为参数调用fn,指定fn的结果返回到record的拷贝中,然后返回这个拷贝。

三、call() 没有做任何实际的工作。当call被调用时,apply_fn()会做实际的工作。上面使用pipeline_each()的例子中,一个apply_fn()的实例会将传入的band的country值改为”Canada“。另一个实例会将传入的band的名称首字母大写。

四、当一个apply_fn() 实例运行时,fn和key将不再作用域中。它们既不是apply_fn()的参数,也不是其中的本地变量。但是它们仍然可以被访问。当一个方法被定义时,方法会保存方法所包含的变量的引用:那些定义在方法的作用域外,却在方法中使用的变量。当方法运行并且代码引用一个变量时,Python会查找本地和参数中的变量。如果没找到,就会去找闭包内保存的变量。那就是找到fn和key的地方。

五、在call()代码中没有提到bands。因为不管主题是什么,call()都可以为任何程序生成pipeline。函数式编程部分目的就是构建一个通用,可重用,可组合的函数库。

干的漂亮。闭包,高阶函数和变量作用域都被包含在段落里。喝杯柠檬水。

还需要在band上做一点处理。就是移除band上除了name和country之外的东西。extract_name_and_country()能拉去这样的信息。
 

def extract_name_and_country(band):
  plucked_band = {}
  plucked_band['name'] = band['name']
  plucked_band['country'] = band['country']
  return plucked_band
 
print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
              call(lambda x: x.replace('.', ''), 'name'),
              call(str.title, 'name'),
              extract_name_and_country])
 
# => [{'name': 'Sunset Rubdown', 'country': 'Canada'},
#   {'name': 'Women', 'country': 'Canada'},
#   {'name': 'A Silver Mt Zion', 'country': 'Canada'}]

extract_name_and_country() 可以写成叫做pluck()的通用函数。pluck()可以这样使用:
 

print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),
              call(lambda x: x.replace('.', ''), 'name'),
              call(str.title, 'name'),
              pluck(['name', 'country'])])

练习5。pluck()接受一系列的键值,根据这些键值去record中抽取数据。试着写写。需要用到高阶函数。

我的方案:
 

def pluck(keys):
  def pluck_fn(record):
    return reduce(lambda a, x: assoc(a, x, record[x]),
           keys,
           {})
  return pluck_fn

What now?
还有什么要做的吗?

函数式代码可以很好的和其他风格的代码配合使用。文章中的转换器可以用任何语言实现。试试用你的代码实现它。

想想Mary,Isla 和 Sam。将对list的迭代,转成maps和reduces操作吧。

想想汽车竞赛。将代码分解成方法。把那些方法改成函数式的。把循环处理转成递归。

想想乐队。将一系列的操作改写成pipeline。

标注:

1、一块不可变数据是指不能被改变的数据。一些语言像Clojure的语言,默认所有的值都是不可变的。任何的可变操作都是拷贝值,并对拷贝的值做修改并返回。这样就消除了程序中对未完成状态访问所造成的bugs。

2、支持一等函数的语言允许像处理其他类型的值那样处理函数。意味着方法可以被创建,传给其他方法,从方法中返回以及存储在其他数据结构里。

3、尾调用优化是一个编程语言特性。每次方法递归,会创建一个栈。栈用来存储当前方法需要使用的参数和本地值。如果一个方法递归次数非常多,很可能会让编译器或解释器消耗掉所有的内存。有尾调用优化的语言会通过重用同一个栈来支持整个递归调用的序列。像Python这样的语言不支持尾调用优化的通常都限制方法递归的数量在千次级别。在race()方法中,只有5次,所以很安全。

4、Currying意即分解一个接受多个参数的方法成一个只接受第一个参数并且返回一个接受下一个参数的方法的方法,直到接受完所有参数。

5、并行意即在不同步的情况下同时运行同一段代码。这些并发操作常常运行在不同的处理器上。

6、惰性计算是编译器的技术,为了避免在需要结果之前就运行代码。

7、只有当每次重复都能得出相同的结果,才能说处理是确定性的。

Stellungnahme
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
详细讲解Python之Seaborn(数据可视化)详细讲解Python之Seaborn(数据可视化)Apr 21, 2022 pm 06:08 PM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于Seaborn的相关问题,包括了数据可视化处理的散点图、折线图、条形图等等内容,下面一起来看一下,希望对大家有帮助。

详细了解Python进程池与进程锁详细了解Python进程池与进程锁May 10, 2022 pm 06:11 PM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于进程池与进程锁的相关问题,包括进程池的创建模块,进程池函数等等内容,下面一起来看一下,希望对大家有帮助。

Python自动化实践之筛选简历Python自动化实践之筛选简历Jun 07, 2022 pm 06:59 PM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于简历筛选的相关问题,包括了定义 ReadDoc 类用以读取 word 文件以及定义 search_word 函数用以筛选的相关内容,下面一起来看一下,希望对大家有帮助。

归纳总结Python标准库归纳总结Python标准库May 03, 2022 am 09:00 AM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于标准库总结的相关问题,下面一起来看一下,希望对大家有帮助。

分享10款高效的VSCode插件,总有一款能够惊艳到你!!分享10款高效的VSCode插件,总有一款能够惊艳到你!!Mar 09, 2021 am 10:15 AM

VS Code的确是一款非常热门、有强大用户基础的一款开发工具。本文给大家介绍一下10款高效、好用的插件,能够让原本单薄的VS Code如虎添翼,开发效率顿时提升到一个新的阶段。

python中文是什么意思python中文是什么意思Jun 24, 2019 pm 02:22 PM

pythn的中文意思是巨蟒、蟒蛇。1989年圣诞节期间,Guido van Rossum在家闲的没事干,为了跟朋友庆祝圣诞节,决定发明一种全新的脚本语言。他很喜欢一个肥皂剧叫Monty Python,所以便把这门语言叫做python。

Python数据类型详解之字符串、数字Python数据类型详解之字符串、数字Apr 27, 2022 pm 07:27 PM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于数据类型之字符串、数字的相关问题,下面一起来看一下,希望对大家有帮助。

详细介绍python的numpy模块详细介绍python的numpy模块May 19, 2022 am 11:43 AM

本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于numpy模块的相关问题,Numpy是Numerical Python extensions的缩写,字面意思是Python数值计算扩展,下面一起来看一下,希望对大家有帮助。

See all articles

Heiße KI -Werkzeuge

Undresser.AI Undress

Undresser.AI Undress

KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover

AI Clothes Remover

Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool

Undress AI Tool

Ausziehbilder kostenlos

Clothoff.io

Clothoff.io

KI-Kleiderentferner

AI Hentai Generator

AI Hentai Generator

Erstellen Sie kostenlos Ai Hentai.

Heißer Artikel

Heiße Werkzeuge

mPDF

mPDF

mPDF ist eine PHP-Bibliothek, die PDF-Dateien aus UTF-8-codiertem HTML generieren kann. Der ursprüngliche Autor, Ian Back, hat mPDF geschrieben, um PDF-Dateien „on the fly“ von seiner Website auszugeben und verschiedene Sprachen zu verarbeiten. Es ist langsamer und erzeugt bei der Verwendung von Unicode-Schriftarten größere Dateien als Originalskripte wie HTML2FPDF, unterstützt aber CSS-Stile usw. und verfügt über viele Verbesserungen. Unterstützt fast alle Sprachen, einschließlich RTL (Arabisch und Hebräisch) und CJK (Chinesisch, Japanisch und Koreanisch). Unterstützt verschachtelte Elemente auf Blockebene (wie P, DIV),

SublimeText3 Linux neue Version

SublimeText3 Linux neue Version

SublimeText3 Linux neueste Version

SublimeText3 Mac-Version

SublimeText3 Mac-Version

Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

DVWA

DVWA

Damn Vulnerable Web App (DVWA) ist eine PHP/MySQL-Webanwendung, die sehr anfällig ist. Seine Hauptziele bestehen darin, Sicherheitsexperten dabei zu helfen, ihre Fähigkeiten und Tools in einem rechtlichen Umfeld zu testen, Webentwicklern dabei zu helfen, den Prozess der Sicherung von Webanwendungen besser zu verstehen, und Lehrern/Schülern dabei zu helfen, in einer Unterrichtsumgebung Webanwendungen zu lehren/lernen Sicherheit. Das Ziel von DVWA besteht darin, einige der häufigsten Web-Schwachstellen über eine einfache und unkomplizierte Benutzeroberfläche mit unterschiedlichen Schwierigkeitsgraden zu üben. Bitte beachten Sie, dass diese Software

PHPStorm Mac-Version

PHPStorm Mac-Version

Das neueste (2018.2.1) professionelle, integrierte PHP-Entwicklungstool