python3x: lambda function


ラボをやっていたらlambda functionを見かけたので復習の意味も込めて。

Lambda Function

一言で言うとlambdaを使えば関数を簡単に適当な変数にアサインすることができる。

定義

bind a function to a name using the same syntax and assignment statement.

まずは以下のexpressionについて考えてみる。

>>> x = 10
>>> square = x*x
>>> square
100
>>> square = lamnda x: x*x
>>> square
<function...>
>>> square(4)
16
>> square(10)
100

講義スライドを参考に簡単に図にまとめてみた。

deflambdaの違い

上の例を参考に違いをまとめると:

  • Both create a function with the same domain, range and behavior.
  • Both functions have as their parent the frame in which they were defined.
  • Both bind that function to the name square. (but in a different way: Lambda first creates the function with no name and it's the assignment statement that binds that function value to the name "square" whereas in the def statement, both of those things happen automatically, all as a byproduct of executing the def statement
  • Only the def statement gives the function an intrinsic name.

基本的な動作に関してはそこまで大きい違いはない。では結局何に違いがあるのだというとそれはEnvironmental diagramを書いてみると分かる。以下は講義スライドをスクショしたもの。

defで関数を作った場合その場で名前がふられるがlambdaで関数を作った場合はその関数が出来上がってもassignment statementによって名前をアサインし終わるまでは名前を持っていないということ。つまりどういうことかというとpythonではわざわざdef statementを使って名前付けしなくても必要なときにその場で関数を作り上げてしまうことができる。

A lambda expression evaluates to a function that has a single return expression as its body.

lambda expressionを実行した結果lambda functionとなって関数としての役割を果たす。この関数には先天的な名前を持っていないためpythonはシンプルに<lambda>として表記する。

>>> s = lambda x: x * x
>>> s
<function<lambda> at xxxxxxx>
>>> s(12)
144

それ以外で基本的な違いはない。

Translating between named and anonymous functions

ふと思ったことなのだが(そしてどこかで書いたかもしれないが)lambda x, ylambda x: lambda yという二つのexpressionを比べた時にparameterの数とargument以外で違いが浮かばなかった。

>>> adder = lambda x: lambda y: x + y
>>> adder2 = lambda x, y: x + y
>>> adder(1)(3)
4
>>> adder2(1,3)
4

ただreview seshに参加した時にdeflambdaを比較することによってより違いがクリアになった。

考えてみたらこれは2個めのlambdadefでいうdef helper(y): returnと考えたら分かりやすい。

Higher-Order Functionsもらくらくできてしまう。

def compose1(f,g):
    return lambdax : f(g(x))

f = compose1(lamnda x: x * x,
             lambda y: y + 1)

result = f(12) # 169

PythonTutor.comでより正確な関数の動きがわかる。

compose1関数の中にあるlambda x:f(g(x))関数はglobal frameではなくf1 frameで作られることに注意。理由はfによってcompose1が呼ばれて初めてcompose1の中に入っていくからだ。compose1を呼んだ時には既にf1 frameを作っているので[p=g]なわけだ。もう一つ気づいた点は気づいた点はcompose1関数の引数として渡している2つのラムダについて。個人的にはcompose1関数を呼んだ時に定義されるだろうと思っていたので[p=f1]と書いてしまったのだがどうやら上のcompose1(f,g)と定義した時点で(何を受け取るか関係なく)変数に名前をアサインしているので後からlambdaとして投げ込んでも何を投げ込んでも結果[p=g]なのだと思う。そんなラムダも使い方を間違えると余計にわかりにくくなる。

lambda expressions are notoriously illegible, despite their brevity.

どういうことかというと上の関数をcompose1 = lambda f,g: lambda x: f(g(x))と書くこともできるが完全に理解するのに多少時間がかかることが多いと。

lambdaの起源

どうやらlambdaは当時のライターが数学の$ŷ. y * y$をという式をタイプライターで書けなかったことから生まれたとか。彼らはその表現を$Λy . y × y$と書き直した、というところから$λy . y × y$となり今でも$λ$という記号を観るようになったという。詳しくはPeter Norvigを。

lambdaを使って気づいたことを箇条書き

lab02の問題をやっていて気づいたことを書いていきます。

Question 1: WWPP: Lambda the Free

Q1

>>> c = lambda: 3
>>> c()
3

選択肢としては
1. Noneをvalueとして出す (w/ some part missing?)
2. 3を表示させるがNoneがvalue (=print)
3. 3をvalueとして出す(=return)
くらいだろうか。ただ上の解説を見れば分かるようにlambdaにはreturnが先天的に付属していると考えられているので当然def c(): return 3と同じ。つまり3を返す。

Q2

>>> c = lambda x: lambda: print('123')
>>> c(88)
<function <lambda>.<locals>.<lambda> at 0x1013740d0>

100%あたっているという自信は無いがpythontutor.comから推測するにclambda x:that returns lambda: print('123)という構造になっていると読み取れる。恐らくそもそも二個目のlambdaは関数として呼ばれていないために最初のラムダが二個目のラムダを関数として引き取ってその関数をそのまま返したのだと思われる。これは後から気づいたことだが恐らくlambdaの中にlambdaを入れるという形を取ると最初のlambda関数が二個目のlambda関数を取ってとして返すという特徴があるのではないかと思ってきた。*ちなみにラムダは関数も受け取れる(というかその使い方のほうが頻度が高いのかもしれない)。

>>> d = lambda f: f(4) # They can have functions as arguments as well.
>>> def square(x):
...     return x * x
>>> d(square)
16

つまり二個目も呼んであげればいいのだ。

>>> c = lambda x: lambda: print('123')
>>> c(88)()
123

しかし呼び方にも気をつける必要がある。なぜなら二個目のラムダには引数を受け取る機能がないからだ。def foo(): print("123")と同じである。よって以下を試みると:

>>> c(88)(3333)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() takes 0 positional arguments but 1 was given

エラーを吐いてしまう。改善方法としては単純に引数とを与えてあげればいいのだ。

>>> c = lambda x: lambda y: print("123")
>>> c(88)(333)
123
>>> c(88)()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'y'

しかしこうすると逆にargumentとして受け取るものがないとエラーを吐いてしまうので注意。

Q3

>>> t = lambda f: lambda x: f(f(f(x)))
>>> s = lambda x: x + 1
>>> t(s)(0)
3

詳しくはpythontutor.comを参考にして欲しいが、t(s)(0)(s)(0)片方ずつsから0という順番で呼んでいくことと、lambdaの持っている変数がそれぞれ何を指しているのかということを注意してみると理解しやすいかもしれない。

ちなみにlambda内の変数はxである必要はない。理由は受け取る際に自分が好みで指定した変数として受け取るからである。何もlambdaに限ったことではないが。

>>> t = lambda f: lambda y: f(f(f(y)))
>>> s = lambda x: x+1
>>> t(s)(0)
3

Q4

>>> bar = lambda y: lambda x: pow(x, y)
>>> bar()(15)
TypeError: <lambda>() missing 1 required positional argument: 'y'

これはlambda yに対応するargumentが無いからである。bar(some #)(15)で動く。

Q5

>>> foo = lambda: 32
>>> foobar = lambda x, y: x // y
>>> a = lambda x: foobar(foo(), bar(4)(x))
>>> a(2)
2

これはlambdaというよりHigher-Order functions向けに作られてると思うのでそこまで解説する必要はなさそう。

Q6

>>> b = lambda x, y: print('summer') # When is the body of this function run?
# Nothing gets printed by the interpreter

>>> c = b(4, 'dog')
summer

>>> print(c)
None

これはpure non-pure functionsを理解していればすぐ分かるはず。

Question 2: Question 2: Lambda the Environment Diagram

>>> a = lambda x: x * 2 + 1
>>> def b(b, x):
...     return b(x + a(x))
>>> x = 3
>>> b(a, x)

上のコードをenvironmental diagramでかけるかという問題。

environmental diagramを書くときはframe名は必ずその関数の変数名ではなくその関数のintrinsic nameをつかってフレームを名付けることを忘れずに。

Question 3: Lambdas and Currying

Write a function lambda_curry2 that will curry any two argument function using lambdas. See the doctest if you're not sure what this means.

 """Returns a Curried version of a two argument function func.
>>> from operator import add
>>> x = lambda_curry2(add)
>>> y = x(3)
>>> y(5)
8
"""
"*** YOUR CODE HERE ***"
return 

どの関数がどのvalueを受け取っているのかに注意しながら考えると簡単に解ける問題。例えばlambda_curry2関数は関数を引数として受け取っているということがx = lambda_curry2(add)よりわかる。次に変数yに数字を1つずつ入れて8が返ってきている。ということはlambdaは数字を2を受け取ってあとの処理はlambda_curry2が取ってきた関数を使えばいいでしょという結論にたどり着く。よって答えはreturn lambda x: lambda y: func(x, y)でオッケー。疑問はなぜreturn lambda x, z: func(x, z)ではダメなのかということだったのだが、それはλを呼ぶときにy = x(3)(5)とするかy = (3,5)とするかの違いだけなのではないかと思っている。

>>> def lambda_curry(func):
...     return lambda x, y: func(x,y)
>>> from operator import add
>>> x = lambda_curry(add)
>>> y = x(3)(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: <lambda>() missing 1 required positional argument: 'y'
>>> l = x(3,5)
>>> l
8

残りの問題は一応解き終わっているのだが、higher-order functionsが関わってくるのでそこをもうちょっと深く勉強して人に説明できるレベルにまで達したら更新することにします。

Lambda w/ List Comprehension

lambdaとlist comprehensionを使って中々実用的なことができることを発見したので備忘録がてら。

あまり効率的な探し方ではないが素数を探すときにlambdaを使って以下のように書くことができる。

nums = range(2,30)
for i in range(2,8):
    nums = list(filter(lambda x: x == i or x % i, nums))
print(nums)

もう一つは適当な文章を見付けて来てそれをsentenceとしていれてあとは以下のスクリプトを動かせば単語ごとの文字数をリストにいれて返してくれる。例えば:

sentence = "I was a joke, and my life was a joke."
print(list(map(lambda x: len(x), sentence.split(" ")))) # [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
print(len(list(map(lambda x: len(x), sentence.split(" "))))) # 10 (=# of words)

参考にしたリンク

-Python: Lambda Functions