Microsoft MVP성태의 닷넷 이야기
닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동 [링크 복사], [링크+제목 복사],
조회: 1034
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 4개 있습니다.)
스크립트: 11. 파이썬(Python) 윈도우 개발 환경
; https://www.sysnet.pe.kr/2/0/1343

닷넷: 2245. C# - IronPython을 이용한 파이썬 소스코드 연동
; https://www.sysnet.pe.kr/2/0/13603

닷넷: 2246. C# - Python.NET을 이용한 파이썬 소스코드 연동
; https://www.sysnet.pe.kr/2/0/13605

닷넷: 2255. C# 배열을 Numpy ndarray 배열과 상호 변환
; https://www.sysnet.pe.kr/2/0/13615




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"가 별도로 추가됩니다.

ironpython_interop_1.png

예상할 수 있듯이, 빌드하면 저 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 패키지를 사용해야 한다면 철저하게 지원 여부를 따져봐야 합니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 4/23/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  42  [43]  44  45  ...
NoWriterDateCnt.TitleFile(s)
12561정성태3/12/20218836VS.NET IDE: 159. Visual Studio에서 개행(\n, \r) 등의 제어 문자를 치환하는 방법 - 정규 표현식 사용
12560정성태3/11/202110154개발 환경 구성: 549. ssh-keygen으로 생성한 개인키/공개키 파일을 각각 PKCS8/PEM 형식으로 변환하는 방법
12559정성태3/11/20219599.NET Framework: 1028. 닷넷 5 환경의 Web API에 OpenAPI 적용을 위한 NSwag 또는 Swashbuckle 패키지 사용 [2]파일 다운로드1
12558정성태3/10/20219078Windows: 192. Power Automate Desktop (Preview) 소개 - Bitvise SSH Client 제어 [1]
12557정성태3/10/20217756Windows: 191. 탐색기의 보안 탭에 있는 "Object name" 경로에 LEFT-TO-RIGHT EMBEDDING 제어 문자가 포함되는 문제
12556정성태3/9/20217010오류 유형: 703. PowerShell ISE의 Debug / Toggle Breakpoint 메뉴가 비활성 상태인 경우
12555정성태3/8/20219053Windows: 190. C# - 레지스트리에 등록된 DigitalProductId로부터 라이선스 키(Product Key)를 알아내는 방법파일 다운로드2
12554정성태3/8/20218856.NET Framework: 1027. 닷넷 응용 프로그램을 위한 PDB 옵션 - full, pdbonly, portable, embedded
12553정성태3/5/20219321개발 환경 구성: 548. 기존 .NET Framework 프로젝트를 .NET Core/5+ 용으로 변환해 주는 upgrade-assistant, try-convert 도구 소개 [4]
12552정성태3/5/20218585개발 환경 구성: 547. github workflow/actions에서 Visual Studio Marketplace 패키지 등록하는 방법
12551정성태3/5/20217490오류 유형: 702. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly. (2)
12550정성태3/5/20217154오류 유형: 701. Live Share 1.0.3713.0 버전을 1.0.3884.0으로 업데이트 이후 ContactServiceModelPackage 오류 발생하는 문제
12549정성태3/4/20217678오류 유형: 700. VsixPublisher를 이용한 등록 시 다양한 오류 유형 해결책
12548정성태3/4/20218444개발 환경 구성: 546. github workflow/actions에서 nuget 패키지 등록하는 방법
12547정성태3/3/20218985오류 유형: 699. 비주얼 스튜디오 - The 'CascadePackage' package did not load correctly.
12546정성태3/3/20218583개발 환경 구성: 545. github workflow/actions에서 빌드시 snk 파일 다루는 방법 - Encrypted secrets
12545정성태3/2/202111311.NET Framework: 1026. 닷넷 5에 추가된 POH (Pinned Object Heap) [10]
12544정성태2/26/202111532.NET Framework: 1025. C# - Control의 Invalidate, Update, Refresh 차이점 [2]
12543정성태2/26/20219872VS.NET IDE: 158. C# - 디자인 타임(design-time)과 런타임(runtime)의 코드 실행 구분
12542정성태2/20/202112219개발 환경 구성: 544. github repo의 Release 활성화 및 Actions를 이용한 자동화 방법 [1]
12541정성태2/18/20219457개발 환경 구성: 543. 애저듣보잡 - Github Workflow/Actions 소개
12540정성태2/17/20219772.NET Framework: 1024. C# - Win32 API에 대한 P/Invoke를 대신하는 Microsoft.Windows.CsWin32 패키지
12539정성태2/16/20219691Windows: 189. WM_TIMER의 동작 방식 개요파일 다운로드1
12538정성태2/15/202110112.NET Framework: 1023. C# - GC 힙이 아닌 Native 힙에 인스턴스 생성 - 0SuperComicLib.LowLevel 라이브러리 소개 [2]
12537정성태2/11/202111165.NET Framework: 1022. UI 요소의 접근은 반드시 그 UI를 만든 스레드에서! - 두 번째 이야기 [2]
12536정성태2/9/202110136개발 환경 구성: 542. BDP(Bandwidth-delay product)와 TCP Receive Window
... 31  32  33  34  35  36  37  38  39  40  41  42  [43]  44  45  ...