C# - IronPython을 이용한 파이썬 소스코드 연동
"IronPython"을 처음 소개했던 때와는 다르게 이제 사용법도 많이 편리해졌습니다.
단지, 프로젝트에 IronPython 패키지만 추가하고,
Install-Package IronPython
예제로 간단한 "test.py"가 있다면,
def greetings(name):
return 'Hello ' + name + '!'
이후, 다음과 같이 코드를 만들면 됩니다.
namespace ConsoleApp1;
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine($"C# Process ID: {Environment.ProcessId}");
string codeText = File.ReadAllText("test.py");
var eng = IronPython.Hosting.Python.CreateEngine();
var scope = eng.CreateScope();
eng.Execute(codeText, scope);
dynamic greetings = scope.GetVariable("greetings");
System.Console.WriteLine(greetings("world"));
}
}
실행 시 이런 출력이 나옵니다.
C# Process ID: 69284
Hello world!
기본적인 사용은 그렇고, 그렇다면 기본 패키지를 import하는 경우에는 어떻게 될까요? 테스트를 위해 test.py의 내용을 다음과 같이 바꾸고,
import os
def greetings(name):
return str(os.getpid()) + ': Hello ' + name + '!'
실행하면 이제는 os 모듈을 찾을 수 없다는 예외가 발생합니다.
Unhandled exception. IronPython.Runtime.Exceptions.ImportException: No module named 'os'
at Microsoft.Scripting.Runtime.LightExceptions.ThrowException(LightException lightEx)
at Microsoft.Scripting.Runtime.LightExceptions.CheckAndThrow(Object value)
at Microsoft.Scripting.Interpreter.FuncCallInstruction`2.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.Interpreter.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.LightLambda.Run2[T0,T1,TRet](T0 arg0, T1 arg1)
at IronPython.Compiler.PythonScriptCode.RunWorker(CodeContext ctx)
at IronPython.Compiler.PythonScriptCode.Run(Scope scope)
at IronPython.Compiler.RuntimeScriptCode.InvokeTarget(Scope scope)
at IronPython.Compiler.RuntimeScriptCode.Run(Scope scope)
at Microsoft.Scripting.SourceUnit.Execute(Scope scope, ErrorSink errorSink)
at Microsoft.Scripting.SourceUnit.Execute(Scope scope)
at Microsoft.Scripting.Hosting.ScriptSource.Execute(ScriptScope scope)
at Microsoft.Scripting.Hosting.ScriptEngine.Execute(String expression, ScriptScope scope)
at ConsoleApp1.Program.Main(String[] args)
왜냐하면 IronPython은 단순히 Python 문법의 Interpreter 해석기에 불과하기 때문에 외부 모듈에 대해서는 알지 못합니다. 즉, "os" 패키지가 있어야 하는 건데요, 다행히 IronPython은 기본적인 파이썬 패키지를 별도로 패키징해 제공하고 있습니다.
Install-Package IronPython.StdLib
IronPython.StdLib를 참조 추가하면 솔루션 탐색기의 프로젝트 노드에는 "lib"가 별도로 추가됩니다.
예상할 수 있듯이, 빌드하면 저 lib 디렉터리가 그대로 출력 디렉터리에 복사됩니다. 엄밀히 저 lib 디렉터리는 Python을 설치했을 때 생성되는 Lib 디렉터리에 해당합니다. (단지, idlelib, test, tkinter, cmd.py 등이 Python에 더 있고, 반면 clrtype.py, wpf.py 등이 IronPython.StdLib에 추가돼 있습니다.)
이렇게 기본 패키지를 확보했으니, 이제 정상적으로 다음과 같은 출력 결과를 볼 수 있습니다.
C# Process ID: 69840
69840: Hello world!
재미있는 것은, 위의 결과에도 나오지만 IronPython은 외부 python.exe 실행 모듈과 연동하는 것이 아닌, 자체 스크립트 해석 엔진으로 동작하기 때문에 In-process 실행이라는 점이 큰 장점입니다.
자, 그렇다면 외부 라이브러리는, 가령 "pip install ..."로 설치하는 패키지는 어떻게 사용할 수 있을까요?
예를 들어,
beautifulsoup4를 사용하는 코드를 이렇게 작성했다면,
def get_page(url):
import urllib.request
import bs4
web_page = urllib.request.urlopen()
page_html = web_page.read(url) if web_page else ""
return page_html[0:100]
var eng = IronPython.Hosting.Python.CreateEngine();
var scope = eng.CreateScope();
eng.Execute(codeText, scope);
dynamic get_page = scope.GetVariable("get_page");
System.Console.WriteLine(get_page("https://www.naver.com"));
당연히 "bs4"를 import 하는 코드에서 예외가 발생할 것입니다.
Unhandled exception. IronPython.Runtime.Exceptions.ImportException: No module named 'bs4'
at Microsoft.Scripting.Runtime.LightExceptions.ThrowException(LightException lightEx)
at Microsoft.Scripting.Runtime.LightExceptions.CheckAndThrow(Object value)
at Microsoft.Scripting.Interpreter.FuncCallInstruction`2.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.Interpreter.Run(InterpretedFrame frame)
at Microsoft.Scripting.Interpreter.LightLambda.Run2[T0,T1,TRet](T0 arg0, T1 arg1)
at IronPython.Compiler.PythonCallTargets.OriginalCallTarget1(PythonFunction function, Object arg0)
at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
at ConsoleApp1.Program.Main(String[] args)
현재(2024-04-20) IronPython의 최신 버전이 3.4.1인데요, 이것은 IronPython이 파이썬 3.4에 해당하는 구문 분석 기능을 가지고 있다는 것을 의미합니다. 따라서, 위와 같이 외부 모듈을 사용하고 싶다면 파이썬 3.4 인터프리터로 해석 가능한, 쉽게 말해 파이썬 3.4 환경에서 설치한 beautifulsoup4 패키지만을 사용할 수 있습니다.
자, 그럼 해당 모듈을 구하기 위해 우선 로컬에 파이썬 3.4를 설치한 다음,
Python 3.4.0
; https://www.python.org/downloads/release/python-340/
그 버전에 해당하는 pip도 아래의 스크립트를 실행해 설치합니다.
get-pip.py
; https://bootstrap.pypa.io/pip/3.4/get-pip.py
예를 들어 C:\Python34 디렉터리에 설치했다면 이후 pip를 이용해 beautifulsoup4 패키지를 설치합니다.
C:\Python34> python -m pip install beautifulsoup4
DEPRECATION: Python 3.4 support has been deprecated. pip 19.1 will be the last one supporting it. Please upgrade your Python as Python 3.4 won't be maintained after March 2019 (cf PEP 429).
Collecting beautifulsoup4
Using cached https://files.pythonhosted.org/packages/69/bf/f0f194d3379d3f3347478bd267f754fc68c11cbf2fe302a6ab69447b1417/beautifulsoup4-4.10.0-py3-none-any.whl
Requirement already satisfied: soupsieve>1.2 in C:\python34\lib\site-packages (from beautifulsoup4) (2.5)
ERROR: soupsieve requires Python '>=3.8' but the running Python is 3.4.0
WARNING: You are using pip version 19.1, however version 19.1.1 is available.
You should consider upgrading via the 'python -m pip install --upgrade pip' command.
그런데, 문제군요. ^^; 위의 명령어처럼 아마도 많은 패키지들이 3.4 버전의 파이썬은 권장하지 않는다면서 아예 설치를 거부해버립니다. 따라서 3.4 환경을 지원하는 버전을 명시해야 하는데요, 파이썬 3.5가 2015년 9월 13일에 release 되었다고 하니 PyPI의 beautifulsoup4 이력으로 들어가,
beautifulsoup4 4.12.3 - Release history
; https://pypi.org/project/beautifulsoup4/#history
대략 그때 릴리스된 버전이 4.4.0으로 나오므로 그 버전을 명시하시면 됩니다.
C:\Python34> python -m pip install beautifulsoup4==4.4.0
그럼 추가 패키지가 "C:\Python34\Lib\site-packages" 디렉터리 하위에 설치될 것입니다. 예를 들어, beautifulsoup4 패키지는 "C:\Python34\Lib\site-packages\bs4" 디렉터리에 설치가 될 텐데요, 해당 디렉터리를 복사해 닷넷 프로그램의 출력 디렉터리에 복사합니다.
[IronPython.StdLib로 인해 추가된 site-packages]
C:\temp\ConsoleApp1\ConsoleApp1\bin\Debug\net8.0\lib\site-packages
위의 경로 하위에 bs4 디렉터리를 복사
그리고, 저 경로의 패키지를 찾으라고 IronPython에 알리는 코드를 추가하고,
string codeText = File.ReadAllText("test.py");
var eng = IronPython.Hosting.Python.CreateEngine();
string dirPath = Path.GetDirectoryName(typeof(Program).Assembly.Location) ?? Environment.CurrentDirectory;
string sitePackagesPath = Path.Combine(dirPath, "lib", "site-packages");
var oldPath = eng.GetSearchPaths();
oldPath.Add(sitePackagesPath);
eng.SetSearchPaths(oldPath);
var scope = eng.CreateScope();
eng.Execute(codeText, scope);
dynamic get_page = scope.GetVariable("get_page");
System.Console.WriteLine(get_page("https://www.naver.com"));
이후 다시 실행하면, 이렇게 잘 동작하는 것을 확인할 수 있습니다. ^^
C:\temp\ConsoleApp1\ConsoleApp1\bin\Debug\net8.0> ConsoleApp1.exe
b' <!doctype html> <html lang="ko" class="fzoom"> <head> <meta charset="utf-8"> <meta name="Referrer'
일단, 개발은 잘 되었으니 이제 배포를 고민해야 합니다.
결국, 파이썬 통합으로 인해 추가되는 용량은 IronPython.StdLib와 기타 파이썬 패키지들이 될 텐데요, 위의 beautifulsoup4를 추가한 lib 전체 용량이라고 해봐야 12MB 정도밖에 안 됩니다. 가령 .NET 8 콘솔을 Self-Contained 배포 패키지로 만든 경우의 바이너리가 기본적으로 수십 MB에 달하기 때문에 거기에 12MB 더해도 별로 티도 나지 않습니다.
첨부 파일은 (파이썬 3.4 site-packages 제외) 이 글의 예제 코드를 포함>합니다.
위에서 제가, IronPython.StdLib와 Python 3.4가 기본적으로 가지고 있는 Lib 디렉터리가 크게 틀리지 않는다고 했는데요, 그런 점을 이용하면 IronPython.StdLib를 사용하지 않고 그것까지 모두 Python 3.4 디렉터리의 내용을 재사용하고 싶을 수도 있습니다.
List<string> newPath = new List<string>();
newPath.Add(sitePackagesPath);
newPath.Add(@"C:\Python34\Lib");
newPath.Add(@"C:\Python34\Lib\site-packages");
eng.SetSearchPaths(newPath);
그런데, beautifulsoup4를 대상으로 저렇게 실행해 보면 urllib.request 코드 실행 시 "Unhandled exception. System.Exception:
unknown url type: https" 예외가 발생합니다.
그러니까 적어도 Lib 만큼은 (clrtype.py, wpf.py 등도 있으니) 그냥 IronPython.StdLib를 쓰는 것이 더 좋습니다.
// Install-Package IronPython.StdLib
var oldPath = eng.GetSearchPaths();
oldPath.Add(@"C:\Python34\Lib\site-packages");
eng.SetSearchPaths(oldPath);
위와 같이 Python 3.4의 site-packages를 재사용하는 것은 그래도 의미가 좀 있습니다. 가령, 복잡한 의존성을 갖는 패키지의 경우 pip로 설치할 때 site-packages에 함께 구성이 되는데 그 모든 것들을 일일이 찾아 복사해 오는 것도 만만치 않을 수 있습니다.
혹은, 그냥 "C:\Python34\Lib\site-packages" 패키지의 내용을 통째로 C# 프로젝트에 복사해 재사용해도 무방합니다. 약간 용량이 더 커지는 문제가 있을 뿐, 오히려 이거저거 생각할 필요가 없어 더 편할 수 있습니다.
그런 경우, csproj에 일일이 파일 하나씩 추가하는 것이 극단적으로 번거로운 작업이기 때문에 다음과 같이 "**" 기호를 이용한 추가를 하면 간단하게 처리할 수 있습니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="IronPython" Version="3.4.1" />
<PackageReference Include="IronPython.StdLib" Version="3.4.1" />
</ItemGroup>
<ItemGroup>
<Content Include="..\pylib\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<None Update="test.py">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
위의 경우는 (csproj가 아닌) 상위의 솔루션 디렉터리를 기준으로 다음과 같이 site-packages를 복사해 온 것을 가정합니다.
// 파이썬 3.4의 site-packages 디렉터리 내용을 "C:\temp\ConsoleApp1\pylib\lib\site-packages"에 복사한 것으로 가정
C:\temp\ConsoleApp1\pylib\lib\site-packages
그럼 csproj 빌드 시에 CopyToOutputDirectory 명령에 따라 ./bin/Debug/net8.0 디렉터리에 함께 출력되므로 관리가 더 수월해집니다.
참고로, Python 외부 모듈을 사용하는 경우 반드시 Python 3.4 버전에 맞춰 pip install을 해야 합니다. 가령, 이 글의 예제에서 (3.4가 아닌) Python 3.7 환경으로 설치한 beautifulsoup4 패키지를 사용하게 되면 "Unhandled exception. Microsoft.Scripting.SyntaxErrorException: future feature is not defined:annotations" 오류가 발생하게 됩니다.
왜냐하면, beautifulsoup4 4.12.3 버전의 경우 Python 3.7 문법에서만 지원하는 future feature 구문을 사용하고 있기 때문에, 아직 그 문법을 지원하지 않는 IronPython에서는,
future feature is not defined #1781
; https://github.com/IronLanguages/ironpython3/issues/1781
오류가 발생하게 되는 것입니다.
무엇보다 IronPyhton의 이런 면이 참 아쉬운 부분인데요, 현재 github repo를 보면
3.6 브랜치가 열려 있지만 아쉽게도 마지막 commit 날짜가 4개월 정도 지난 상태입니다. 다시 말해 그다지 빠른 속도로 구현을 하고 있지는 않습니다. (어쩌면 조만간 archive 상태로 바뀔 지도... ^^;)
설령 3.6 지원이 되었다고 해도 근래 파이썬의 최신 버전이 3.12로 빠르게 업데이트되고 있는 상황에서 앞으로도 얼마나 뒤처지는 시간이 늘어날지 알 수 없습니다.
더욱 문제는, 3rd-party 패키지들도 빠르게 파이썬의 최신 문법을 지원하면서 지난 패키지는 deprecated 시킨다는 점입니다. 일례로, tensorflow는 파이썬 3.4 버전은 아예 지원조차 안 하는 상태입니다.
따라서, 단순히 파이썬 기본 라이브러리 내에서 동작하는 경우라면 IronPyhton이 좋은 선택일 수 있지만 조금이라도 3rd-party 패키지를 사용해야 한다면 철저하게 지원 여부를 따져봐야 합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]